Вопрос или проблема
У меня есть фрагмент, экземпляр ViewModel которого предоставляется компонентом, привязанным к графу навигации Главной активности. Компонент приложения Dagger был.injected в единственную Главную активность и захвачен внутри фрагмента как ленивый делегируемый переменная.
private val myViewModel: MyViewModel by lazy {
ViewModelProvider(findNavController().getBackStackEntry(R.id.nav_graph), Factory(this) { stateHandle ->
(activity as MainActivity).myComponent.myViewModel()
.create(stateHandle)}).get(MyViewModel::class.java)
}
Параметр ViewModelProvider.Factory метода ViewModelProvider использует метод create из интерфейса Factory, определенного в классе MyViewModel; этот интерфейс аннотирован @AssistedFactory, в то время как savedStateHandle аннотирован @AssistInject в конструкторе MyViewModel.
class MyViewModel @AssistedInject constructor(
private val myRepository: MyRepository,
@Assisted val savedStateHandle: SavedStateHandle
) : ViewModel() {
@AssistedFactory
interface MyViewModelFactory : ViewModelProvider.Factory {
fun create(savedStateHandle: SavedStateHandle) : MyViewModel
}
// Другой код ViewModel
}
Приложение работает нормально, и компонент приложения Dagger получает предоставленные экземпляры MyViewModel через свои ленивые делегаты, но теперь, когда я пытаюсь написать инструментальный тест для проверки некоторых полей интерфейса на самом фрагменте, я получаю исключение NullPointerException. В частности:
java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.lifecycle.ViewModelProvider.<init>, parameter owner
at androidx.lifecycle.ViewModelProvider.<init>(Unknown Source:2)
Это говорит мне о том, что параметр owner, findNavController().getBackStackEntry(R.id.nav_graph)
вызывает ошибку NPE, но я не знаю, как это решить, чтобы мой тест мог скомпилироваться и выполниться.
Вот мой тестовый код на данный момент, пожалуйста, помогите, где я что-то упускаю; в противном случае я попытаюсь удалить Dagger и создать все с нуля только для того, чтобы провести тестирование, что кажется обратным решением. Я даже не уверен, правильно ли я пишу этот тестовый сценарий, так как пробую множество решений из разных форумов и этого сайта, чтобы решить свои проблемы со временем компиляции и неполадками при сборке.
@RunWith(AndroidJUnit4::class)
class MyFragmentTest {
@get:Rule
var activityRule = ActivityScenarioRule(MainActivity::class.java)
private lateinit var scenario: FragmentScenario<MyFragment>
private lateinit var binding: FragmentMyBinding
private lateinit var mockNavController : NavController
@Test
fun setUp() {
mockNavController = mock(NavController::class.java)
scenario = launchFragmentInContainer<MyFragment> {
MyFragment().also { fragment ->
fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
if (viewLifecycleOwner != null) {
// Вид фрагмента только что был создан
mockNavController.setGraph(R.navigation.nav_graph)
Navigation.setViewNavController(fragment.requireView(), mockNavController)
}
}
}
}
}
Любая помощь или руководство в правильном направлении будут очень признательны, так как я застрял здесь на некоторое время.
Ответ или решение
Для решения проблемы с NullPointerException при тестировании фрагментов с использованием ViewModel, предоставляемых Dagger, необходимо учесть несколько аспектов.
Проблема
Ошибка java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.lifecycle.ViewModelProvider.<init>, parameter owner
указывает на то, что при вызове ViewModelProvider
параметр owner
(в данном случае, findNavController().getBackStackEntry(R.id.nav_graph)
) является null. Это говорит о том, что навигационный контроллер и его график не были должным образом настроены в вашем тесте.
Решение
-
Настройка навигационного контроллера: Вам необходимо убедиться, что навигационный график и контроллер настроены до вызова
ViewModelProvider
. - Изменение теста: Вам не нужно напрямую использовать
viewLifecycleOwnerLiveData
. Вместо этого вы можете сразу установить навигационный контроллер черезlaunchFragmentInContainer
и убедиться, что он корректно настроен.
Вот пример вашего теста с изменениями:
@RunWith(AndroidJUnit4::class)
class MyFragmentTest {
@get:Rule
var activityRule = ActivityScenarioRule(MainActivity::class.java)
private lateinit var scenario: FragmentScenario<MyFragment>
private lateinit var mockNavController: NavController
@Before
fun setUp() {
mockNavController = mock(NavController::class.java)
`when`(mockNavController.graph).thenReturn(mock(NavGraph::class.java))
scenario = launchFragmentInContainer<MyFragment> {
MyFragment().also { fragment ->
val navHostFragment = mock(NavHostFragment::class.java)
navHostFragment.setGraph(R.navigation.nav_graph)
Navigation.setViewNavController(fragment.requireView(), mockNavController)
fragment.requireActivity().supportFragmentManager.beginTransaction()
.replace(android.R.id.content, navHostFragment)
.commitNow()
}
}
}
@Test
fun testMyFragmentUI() {
// Ваши тесты UI для MyFragment
}
}
Объяснение изменений
-
Передача
mockNavController
: Теперь перед созданиемMyFragment
мы устанавливаемmockNavController
прямо в фрагмент, тем самым устраняя возможность возникновения NPE. -
Использование
@Before
: Перенос настройки в метод@Before
, чтобы код выполнялся перед каждым тестом, обеспечивая чистое состояние. - Настройка графа навигации: Убедитесь, что граф навигации правильно инициализирован и установлен перед обращением к ViewModel.
Заключение
Проблема возникла из-за отсутствия корректной инициализации навигационного контроллера в тестах. Убедитесь, что навигационные компоненты настроены правильно, и ваше тестирование не будет сталкиваться с NullPointerExceptions, что позволит вам продолжить работу с Dagger и сохранить структуру кода.