What Really Happens When You Call `hiltViewModel()`

skydovesJaewoong Eum (skydoves)||11 min read

What Really Happens When You Call hiltViewModel()

You write one line, val viewModel: ProfileViewModel = hiltViewModel(), and a ViewModel whose constructor asks for a repository, a SavedStateHandle, and three other dependencies simply appears, fully built. Swap hiltViewModel() for the plain viewModel() that ships with Compose and the same screen crashes the moment it opens, because Compose has no idea how to satisfy that constructor. Something inside that one call knows how to reach your Dagger graph, and it does so without you passing a single argument.

In this article, you'll dive deep into what hiltViewModel() actually does between the moment you call it and the moment your injected ViewModel comes back. You'll follow the real source in androidx.hilt: the composable that reads a ViewModelStoreOwner off a composition local, the factory that walks up the Context chain to find your activity, the delegate that lets Hilt and non-Hilt ViewModels live side by side, and the CreationExtras path that carries runtime arguments into an assisted constructor. By the end, you'll have concrete answers to the two questions that send people to the issue tracker: why it throws Expected an activity context, and how a ViewModel ends up tied to a navigation destination.

The gap between viewModel() and an injected constructor

Compose already has a way to get a ViewModel. The viewModel() function from androidx.lifecycle.viewmodel.compose asks the current ViewModelStoreOwner for an instance, and if none exists yet, it builds one through a ViewModelProvider.Factory. The factory it uses by default can only do so much. It instantiates the class by reflection, which works when the constructor takes nothing, or just a single SavedStateHandle. Anything else is out of reach.

A @HiltViewModel constructor is exactly that: anything else. It lists real dependencies (a repository here, a use case there) that only your Dagger graph knows how to build. The default factory has no path to that graph, so it gives up. The missing piece is a different ViewModelProvider.Factory, one wired into Hilt. Supplying that factory, and nothing more, is the whole job of hiltViewModel().

Reading hiltViewModel() from the top

Here is the current source of the simple overload, in androidx.hilt.lifecycle.viewmodel.compose. It is short, and every line earns its place.

@Composable
public inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner =
        checkNotNull(LocalViewModelStoreOwner.current) {
            "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
        },
    key: String? = null,
): VM {
    val factory = rememberHiltViewModelFactory(viewModelStoreOwner.defaultViewModelProviderFactory)
    return viewModel(viewModelStoreOwner, key, factory = factory)
}

The default value of viewModelStoreOwner is where the scope is decided. LocalViewModelStoreOwner.current returns whatever the nearest provider put there. Inside a NavHost destination, it is the NavBackStackEntry; outside one, it is usually the host ComponentActivity. If nothing set it at all, the checkNotNull fails early with a readable message rather than a confusing crash later.

The body has two steps. It builds a Hilt aware factory, passing viewModelStoreOwner.defaultViewModelProviderFactory as a fallback, and then hands everything to the ordinary Compose viewModel(). That second call is the same one you would use without Hilt. The only thing that changed is the factory it receives.

Binding the factory to a Context

The factory is not created fresh on every recomposition. rememberHiltViewModelFactory holds onto it, keyed by the two inputs that can change its behavior.

@Composable
public fun rememberHiltViewModelFactory(
    delegateFactory: ViewModelProvider.Factory =
        LocalViewModelStoreOwner.current.defaultViewModelProviderFactory
): ViewModelProvider.Factory {
    val context = LocalContext.current
    return remember(context, delegateFactory) { HiltViewModelFactory(context, delegateFactory) }
}

This article continues for subscribers

Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.

Become a Sponsor