Compose UI and the death of androidx.lifecycle.ViewModel
The
ViewModel
remains in memory until theLifecycle
it's scoped to goes away permanently: in the case of an activity, when it finishes, while in the case of a fragment, when it's detached. — Android Documentation
If you follow the Google recommended architecture for pure Compose apps, you might no longer need to use androidx.lifecycle.ViewModel
, even if you still use MVVM. Why? Because the main reason to use androidx.lifecycle.ViewModel
is for the lifecycle scoping.
The androidx.lifecycle.ViewModel
scoping takes care of scoping the ViewModel instance as a singleton within the nearest lifecycle (ViewModelStoreOwner
), excluding recreation due to configuration change. Most of the time, that’s the nearest Fragment or Activity. However, in a pure Compose app, there should be no Fragments, and only 1 Activity. This means that, by default, this scoping mechanism will scope all your ViewModels to the same single Activity.
In other words, you might as well make your ViewModels vanilla classes (which don’t extend from androidx.lifecycle.ViewModel
), and just hold these ViewModel instances in a Kotlin object
, or inject them as simple singletons via your favourite Dependency-Injection framework. In Koin, that would simply look like: single { MyViewModel() }
This setup would be roughly equivalent to the implicit scoping that androidx.lifecycle.ViewModel
would perform in this situation. However, depending on the specifics of your DI, this might scope your ViewModel instances to the life of the Process, rather than the life of the Activity. Since there’s only 1 Activity this doesn’t make much of a difference, but it does mean your ViewModel instances would live outside the Activity lifecycle, so will survive if your Activity is recreated. If you wanted to have your ViewModel instances be bound to the lifecycle of the Activity, exactly as androidx.lifecycle.ViewModel
would perform, you’d simply initialise your ViewModel instances in onCreate
instead.
This is bad
Obviously, we don’t want to actually do this. Regardless of whether you use androidx.lifecycle.ViewModel
or manually tie your ViewModels to the lifecycle of your single Activity or Process, you’ll end up with the same problem: your ViewModel instances will effectively be singletons for the duration of the Activity or Process. This is not great, because it means all those ViewModel instances will be there, in memory, for as long as your Activity or Process lives. Even when a ViewModel instance is no longer needed, it will hang around, creating unnecessary memory overhead.
This means that you’ll need to manually handle your ViewModel instances in a smarter way than simply creating singletons. The upside is that this gives you complete control over your ViewModel instances — we can choose whether to initialise each instance lazily or eagerly, or even have multiple instances of each ViewModel class (with androidx.lifecycle.ViewModel
you may have only 1 instance of each class). It also allows us to purge the androidx.lifecycle.ViewMode
boilerplate — things like ViewModel factories and ViewModel providers are no longer needed. However, the huge downside is that you now have to handle this extra complexity yourself.
AndroidX Navigation Component
I mentioned earlier that, most of the time, ViewModels will be scoped to the nearest Fragment or Activity. The ‘most of the time’ caveat hinges on the fact that Fragment and Activity are not the only ViewModelStoreOwner
s. Google have in fact provided a third type which ViewModels can scope to — as long as you’re also using the AndroidX Navigation Component. I’d to thank various commentors on the original article for pointing this out to me.
The third ViewModelStoreOwner
is NavBackStackEntry
. Here’s a snippet of documentation:
The
Lifecycle
,ViewModelStore
, andSavedStateRegistry
provided via this object are valid for the lifetime of this destination on the back stack: when this destination is popped off the back stack, the lifecycle will be destroyed, state will no longer be saved, and ViewModels will be cleared.
There is also an example of how to use this mechanism with Compose in the Google article Integrating Compose with your existing app architecture:
As navigation graphs also scope
ViewModel
elements, composables that are a destination in a navigation graph have a different instance of theViewModel
. In this case, theViewModel
is scoped to the lifecycle of the destination and it will be cleared when the destination is removed from the backstack.
Conclusion
Your options for sensible ViewModel scoping in Compose therefore look like this:
- Continue to use
androidx.lifecycle.ViewModel
, but only if you’re also using the AndroidX Navigation Component.
This will automatically scope your ViewModels to navigation destinations, so you don’t have to worry about handling lifecycles manually. The downside is that you have to use the Compose API for the AndroidX Navigation Component, which I find lacking (no type-safety, for example). - Leave
androidx.lifecycle.ViewModel
behind, write your ViewModels as vanilla classes, and handle ViewModel scoping manually.
This gives you complete control over scoping, at the cost of having to handle all the extra complexity yourself. It also means you’re not forced to use the AndroidX Navigation Component, so you can write your own navigation logic (trivial to do in Compose) — this gives you complete control over navigation and allows you to build type-safe navigation.
Until next time 👋