The root cause for why its crashing:-
hiltViewModel() under the hood, is defined in androidx.hilt.navigation.compose.
@Composable
inline fun <reified VM : ViewModel, reified VMF> hiltViewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
noinline creationCallback: (VMF) -> VM
): VM { ... }
It does two key things:
Finds the ViewModelStoreOwner for the current composition (usually the NavBackStackEntry or Activity).
Gets a HiltViewModelFactory from the context:
val factory = HiltViewModelFactory(LocalContext.current, owner)
That factory needs:
A Hilt Application (for dependency graph access)
A real SavedStateHandle (from an Android SavedStateRegistryOwner)
The @HiltViewModel annotation and generated components.
In Robolectric or plain Compose unit tests
There is no real HiltTestApplication or DI graph
There is no HiltViewModelFactory bound to your test environment
The composition’s LocalContext points to a Robolectric Application, not a HiltApplication
Thus, calling causing the Crash.
There is many way to fix this issue:-
- Pass Dependencies via Parameters:- Basically instead of creating the viewmodel object in subview create the object at the Top.
@Composable
fun MyParentLayout(viewModel: MyParentViewModel = hiltViewModel()) {
MyParentImpl(
someValue = viewModel.someValue,
subViewModel = hiltViewModel()
)
}
@Composable
fun MyParentImpl(someValue: Int, subViewModel: MySubViewModel) {
MySubLayout(subViewModel)
}
@Composable
fun MySubLayout(viewModel: MySubViewModel) {
// display code
}
Then, in your test:
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun myParentImplDisplaysCorrectly() {
val fakeSubViewModel = FakeSubViewModel()
composeTestRule.setContent {
MyParentImpl(someValue = 10, subViewModel = fakeSubViewModel)
}
}
2. CompositionLocal Override for Testing
val LocalSubViewModel = staticCompositionLocalOf<MySubViewModel> {
error("No SubViewModel provided")
}
@Composable
fun MySubLayout(
viewModel: MySubViewModel = LocalSubViewModel.current
) {
}
In App:
CompositionLocalProvider(LocalSubViewModel provides hiltViewModel<MySubViewModel>()) {
MySubLayout()
}
In tests:
val fakeViewModel = FakeSubViewModel()
composeTestRule.setContent {
CompositionLocalProvider(LocalSubViewModel provides fakeViewModel) {
MyParentImpl(someValue = 10)
}
}
Recommended the 1st approach cause :-
Pure Compose test — no Hilt dependency.
Easy to inject fakes/mocks.
Works for both unit + integration testing.
2nd Approach:-
- This is ideal when refactoring isn't feasible.