Demystifying val viewModel: MyViewModel = viewModel() in Jetpack Compose
Understanding How Jetpack Compose's viewModel() Works Behind the Scenes
If you are transitioning to Jetpack Compose from traditional Android development, or if you are relatively new to Kotlin, you might encounter syntax that feels like pure magic. One common example is this line:
val myViewModel: MyViewModel = viewModel()At first glance, it looks like calling an empty function or a constructor, yet it somehow returns an instance of MyViewModel without explicitly passing any class token or reflection object (like MyViewModel::class.java). How does it know what type to create? Which overloaded function does it call?
In this guide, we will break down the underlying Kotlin features—specifically reified type parameters and type inference—that make this seamless syntax possible.
The Core Mechanism: Inline Functions and Reified Type Parameters
In standard Java and Kotlin, generic types are subject to type erasure. This means that at runtime, generic type information like <T> is stripped away, making it impossible to perform checks like T::class.java directly inside a generic method.
However, Kotlin solves this with a powerful combination: inline functions with reified type parameters.
If you look into the source code of the androidx.lifecycle.viewmodel.compose library, you will find a definition that looks similar to this:
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { ... },
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras = checkNotNull(LocalViewModelStoreOwner.current?.currentBackStackEntry) { ... }
): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory, extras)How Reification Works
inline: When a function is marked asinline, the Kotlin compiler copies the function's bytecode directly into the call site rather than generating a typical method call.reified: Because the code is inlined at the exact spot where it is called, the compiler knows the exact concrete type being requested. Marking the parameter asreifiedallows the compiler to access the type metadata (likeVM::class.java) directly at runtime.
How Kotlin Chooses the Right Type (Type Inference)
When you write:
val myViewModel: MyViewModel = viewModel()The Kotlin compiler performs bidirectional type inference:
- The variable
myViewModelis explicitly typed asMyViewModel. - The function
viewModel()returns generic typeVMwhereVM : ViewModel. - The compiler matches the expected return type (
MyViewModel) with the generic parameterVM, inferring thatVM = MyViewModel.
Alternatively, you can write the invocation explicitly as:
val myViewModel = viewModel<MyViewModel>()Both versions generate identical bytecode under the hood. You can choose the explicit variable type or the type argument based on your team's code style preference.
How Does Kotlin Pick the Correct Overload?
The documentation lists multiple overloads for viewModel(). Which one gets called?
In Kotlin, overloads are resolved by comparing parameter signatures and default arguments:
- One overload requires a non-reified
Class<VM>orKClass<VM>as its first parameter (e.g.,viewModel(modelClass = MyViewModel::class.java)). - The inline version takes zero required arguments because all of its parameters (
viewModelStoreOwner,key,factory,extras) have default parameter values.
Because you supplied zero arguments and did not pass a Class object, the compiler selects the inline fun <reified VM : ViewModel> viewModel(...) signature and populates the remaining parameters with their defaults (such as pulling the current ViewModelStoreOwner from composition locals).
Why Not Just Write val myViewModel = MyViewModel()?
Instantiating a ViewModel directly via its constructor destroys the architectural benefits of Android's ViewModel:
- Lifecycle Awareness: The
viewModel()utility accesses theLocalViewModelStoreOwner(such as the NavBackStackEntry or Activity). It retains the instance across configuration changes (e.g., screen rotations) and recompositions. - Memory Management: Calling
MyViewModel()manually creates a new instance on every recomposition and fails to triggeronCleared()when the screen is dismissed.
Summary
The val myViewModel: MyViewModel = viewModel() syntax is not magic; it is a textbook example of modern Kotlin capabilities:
- Reified generics retain the type argument across compilation.
- Inlining allows the retrieval of the underlying Java Class token (
VM::class.java). - Default parameter values provide a clean, zero-argument Composable API.