What you’ll study
- What recreation is, and what a ViewModel does and doesn’t survive.
- Owner → store → ViewModel, and get-or-create by key.
- The hand-off on rotation (
NonConfigurationInstances). - When
onCleared()runs, and howviewModelScopeis built. - Owners in Fragments, Navigation and Compose.
- Leaks, sharing between screens, and process death.
How it connects
A ViewModel isn’t special by itself; it survives because of where it is stored. Every owner has a store, a map of instances, and asking for a ViewModel looks it up there: same store, same instance. Rotation hands the same store to the new Activity in memory; a real finish clears it. Fragments, Navigation and Compose only change which owner, and so which store, you get. And because the hand-off happens in memory, process death ends it.
Read it at your level
| Level | Read | You should be able to |
|---|---|---|
| Student | The basics, the high level, the demo | Explain why a ViewModel survives rotation and a field doesn’t |
| Fresher | + the Compose section | Use by viewModels() and viewModel() correctly, and load data in init |
| Junior | + Why it helps, cross questions | Avoid Context leaks, share a ViewModel between fragments, scope one per screen |
| Mid | + The low level, The map | Explain the store hand-off, the clear rule, viewModelScope and CreationExtras |
| Senior | + Edge cases | Reason about process death, configChanges, recreate(), Android 12 Back and Compose owners |
The basics
When the user rotates the phone, switches to dark mode or resizes a window, Android destroys the Activity and creates a new one with the new configuration. Every field in the old Activity is gone. If the screen was halfway through loading a list, that work and its result go with it.
A ViewModel is an object that holds a screen’s state and logic and outlives that
recreation. The new Activity gets back the same ViewModel instance, with its loaded list and
running coroutines intact.
class ProfileViewModel(private val repo: ProfileRepo) : ViewModel() {
private val _ui = MutableStateFlow<ProfileUi>(ProfileUi.Loading)
val ui: StateFlow<ProfileUi> = _ui
init {
viewModelScope.launch { _ui.value = repo.load() }
}
}
// Activity or Fragment
private val vm: ProfileViewModel by viewModels { factory }
// Compose
@Composable
fun ProfileScreen(vm: ProfileViewModel = viewModel()) { /* … */ }
What a ViewModel does not survive: the user finishing the screen (it is cleared), and the system killing the process while the app is in the background (everything in memory is gone). The second case is the subject of the next guide.
The high level
The whole mechanism is three objects:
ViewModelStoreOwner "something with a lifetime that can own ViewModels"
│ Activity, Fragment, NavBackStackEntry, …
│ has one
▼
ViewModelStore a map: key ─► ViewModel instance
│
▼
ViewModel your state + viewModelScope
and two rules:
- Get-or-create by key. Asking for a ViewModel looks up its key in the owner’s store. If it’s there, you get the same instance. If not, a factory creates one and stores it.
- The store outlives a configuration change, not a finish. When an Activity is destroyed only
to be recreated, its store is handed to the new instance. When it is destroyed for good, the
store is cleared and every ViewModel in it gets
onCleared().
Rotation (isChangingConfigurations = true)
Activity#1 ──destroy──┐
store ─────────────┴──► Activity#2 same store, same ViewModels
Back / finish (isChangingConfigurations = false)
Activity#1 ──destroy──► store.clear() ──► onCleared() on each ViewModel
Checkpoint
Before going deeper, you should be able to say:
- A ViewModel lives in its owner’s
ViewModelStore, keyed by its class (or your key). - Rotation reuses the store; a real finish clears it.
- Nothing is serialised: survival is in memory only, so process death ends it.
Try rotate, open detail, back and process death in the demo before reading the code.
The low level
1. by viewModels() → ViewModelProvider.get()
by viewModels() (and Compose’s viewModel()) end up in ViewModelProviderImpl.getViewModel:
// simplified from ViewModelProviderImpl.kt
fun <T : ViewModel> getViewModel(modelClass: KClass<T>, key: String): T =
synchronized(lock) {
val existing = store[key]
if (modelClass.isInstance(existing)) return existing as T
val extras = MutableCreationExtras(defaultExtras)
extras[VIEW_MODEL_KEY] = key
factory.create(modelClass, extras).also { store.put(key, it) }
}
- The default key is
"androidx.lifecycle.ViewModelProvider.DefaultKey:"plus the class name. So one owner holds one instance per class, unless you pass your ownkey. ViewModelStore.put()replaces any old instance under that key, and clears the old one.- The factory receives
CreationExtras.ComponentActivityfills them with theApplication, itself asSavedStateRegistryOwnerandViewModelStoreOwner, and the intent extras as default arguments. That is howcreateSavedStateHandle()and Hilt’s factory get what they need without a reference to the Activity.
2. The hand-off on rotation
This is the part most people never see. In ComponentActivity:
// simplified from ComponentActivity.kt
internal class NonConfigurationInstances {
var custom: Any? = null
var viewModelStore: ViewModelStore? = null
}
final override fun onRetainNonConfigurationInstance(): Any? {
val nci = NonConfigurationInstances()
nci.viewModelStore = _viewModelStore
?: (lastNonConfigurationInstance as? NonConfigurationInstances)
?.viewModelStore
return nci
}
- During a configuration change the platform calls
onRetainNonConfigurationInstance()on the dying Activity and keeps the returned object in memory, outside the Activity. - The new Activity’s
viewModelStoregetter callsensureViewModelStore(), which readslastNonConfigurationInstanceand reuses that store instead of making a new one. - Nothing is serialised. It’s the same object in the same process, which is why any type can live in a ViewModel, and why none of it survives process death.
onRetainNonConfigurationInstance() is final. That’s deliberate: AndroidX owns this hook now.
3. When onCleared() runs
ComponentActivity observes its own lifecycle:
// from ComponentActivity.kt
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations) {
viewModelStore.clear()
}
}
ViewModelStore.clear() calls clear() on every ViewModel, which:
- closes every
AutoCloseableregistered withaddCloseable(), includingviewModelScope, so its coroutines are cancelled; - then calls your
onCleared().
viewModelScope itself is created lazily on first use with a SupervisorJob (one failing
child doesn’t cancel the others) and Dispatchers.Main.immediate (it runs on the main thread,
without an extra hop when you’re already on it).
4. Fragments
A Fragment’s ViewModels live inside a FragmentManagerViewModel, which is itself a ViewModel in
the Activity’s store. It keeps one ViewModelStore per fragment, keyed by the fragment’s internal
id, plus one per child FragmentManager.
- When the Activity rotates, the
FragmentManagerViewModelsurvives with it, so every fragment’s store survives too. - When a fragment is removed for good (not just put on the back stack), its store is cleared.
by activityViewModels()asks the Activity’s store instead: that’s how two fragments share one instance.
5. Navigation
Navigation gives every back-stack entry its own store. NavBackStackEntry is a
ViewModelStoreOwner, and its stores live in a NavViewModelStoreProvider held in the host’s
store, keyed by the entry’s id.
- A destination’s ViewModel lives as long as its entry is on the back stack, and is cleared when the entry is popped.
- To share one between screens, scope it to a parent graph’s entry
(
navGraphViewModels(R.id.flow), orviewModel(navController.getBackStackEntry("flow"))in Compose).
6. Compose
viewModel() is the same get-or-create, with the owner taken from a CompositionLocal:
// from lifecycle-viewmodel-compose
inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner =
checkNotNull(LocalViewModelStoreOwner.current) { … },
key: String? = null,
factory: ViewModelProvider.Factory? = null,
extras: CreationExtras =
viewModelStoreOwner.defaultViewModelCreationExtras,
): VM
LocalViewModelStoreOwner.currentfalls back to the View tree’s owner, which is the hosting Activity or Fragment. In a plainsetContent { }, everyviewModel<Foo>()call in the whole screen returns the same instance, scoped to the Activity.- Inside
NavHost, each destination provides itsNavBackStackEntryas the owner, so each screen gets its own instance, cleared when the screen is popped. - A composable leaving the composition does not clear a ViewModel. Only its owner can. Newer
lifecycle versions add
rememberViewModelStoreOwner()for ViewModels scoped to a single composable; check your version before you rely on it.
Why it helps
- “Data reloads on every rotation.” You created state in the Activity or in
remember, not in the ViewModel, or you started the load inonStart(). Load ininit(or lazily from a flow) so it happens once per ViewModel, not once per Activity. - “Memory leak after rotation.” The ViewModel outlives the Activity, so a reference to an
Activity, View or Fragment from it keeps the dead one alive. Pass the
Application(throughAndroidViewModelorCreationExtras) if you need aContext. - “Two screens got the same ViewModel.” In Compose without Navigation, everything shares the
Activity’s owner. Use per-destination entries or a
key. - “State lost after the app was in the background.” Process death. The ViewModel was never
meant to survive it. Put what the user would miss in
SavedStateHandle, and real data in a database. - “My cleanup in
onCleared()didn’t run.” It only runs on a real finish. On process death, nothing runs at all.
The map
| Idea | Where it lives |
|---|---|
| Owner of ViewModels | ViewModelStoreOwner (ComponentActivity, Fragment, NavBackStackEntry) |
| The map of instances | ViewModelStore (put clears a replaced instance, clear clears all) |
| Get-or-create | ViewModelProvider, ViewModelProviderImpl.getViewModel, default key |
| Creating with dependencies | ViewModelProvider.Factory, CreationExtras, viewModelFactory { } |
| Hand-off on rotation | ComponentActivity.onRetainNonConfigurationInstance, NonConfigurationInstances, ensureViewModelStore |
| Clearing on finish | ComponentActivity ON_DESTROY observer, isChangingConfigurations |
| Coroutines | viewModelScope (SupervisorJob + Dispatchers.Main.immediate), addCloseable |
| Fragments | FragmentManagerViewModel, by viewModels(), by activityViewModels() |
| Navigation | NavBackStackEntry, NavViewModelStoreProvider, navGraphViewModels |
| Compose | viewModel(), LocalViewModelStoreOwner, rememberViewModelStoreOwner() |
| Saved state from a ViewModel | SavedStateHandle, createSavedStateHandle() |
Edge cases
- Process death. The ViewModel, its store and everything in memory are gone. The Activity is
recreated from its saved
Bundle, and a new ViewModel is created. OnlySavedStateHandlevalues come back. android:configChangesin the manifest. If you declare a change yourself, the Activity isn’t recreated for it:onConfigurationChanged()is called instead, and no hand-off happens.recreate(). Recreates the Activity like a configuration change, so ViewModels survive.- Back on a root Activity (Android 12+). For the launcher Activity, Back moves the task to the background instead of finishing it, so its ViewModels are kept.
- Asking too early. Reading
viewModelStorebefore the Activity is attached throws “You can’t request ViewModel before onCreate call.” - Fragment on the back stack. Its view is destroyed but the fragment isn’t, so its ViewModel survives until the fragment is popped.
- Same class, two instances. Pass different
keys, or use different owners.
Cross questions
Where exactly is the ViewModel kept during rotation, if the Activity is destroyed?
In the NonConfigurationInstances object that ComponentActivity.onRetainNonConfigurationInstance() returns. The platform holds that object across the relaunch and hands it to the new Activity as lastNonConfigurationInstance, where ensureViewModelStore() picks up the same ViewModelStore.
Why doesn't a ViewModel survive process death if it survives rotation?
Rotation keeps the same object in memory in the same process. Process death ends the process, so there is no memory to keep it in. Only data the system can store outside the process (the saved Bundle) or data on disk survives.
How does the ViewModel know whether to clear itself on onDestroy()?
It doesn't; the owner decides. ComponentActivity clears its store at ON_DESTROY only when isChangingConfigurations is false. Fragments and Navigation entries clear their stores when they are removed for good.
What runs first when a ViewModel is cleared, viewModelScope cancellation or onCleared()?
The scope. clear() closes every registered AutoCloseable, including viewModelScope, before calling onCleared(). Don't launch new work on viewModelScope from onCleared(); it is already cancelled.
Two fragments in one Activity need to share data. How?
Scope a ViewModel to the Activity with by activityViewModels(), or to a navigation graph with navGraphViewModels(), so both fragments get the same instance from the same store.
In Compose, why did two different screens get the same ViewModel?
Without Navigation, LocalViewModelStoreOwner is the Activity for the whole tree, and the default key is the class name. Use Navigation (each destination is its own owner), pass a key, or provide a narrower owner.
Why shouldn't a ViewModel hold a Context?
It outlives the Activity. A reference to an Activity, View or Fragment keeps the destroyed one in memory after every rotation. Use the Application context, which lives as long as the process.
What is CreationExtras for?
It lets a factory create a ViewModel with what it needs (the Application, saved-state owner, default arguments, the key) without holding the owner. createSavedStateHandle() reads its inputs from there.
Is viewModelScope on the main thread? Is that a problem?
Yes, Dispatchers.Main.immediate. It's fine for updating state; switch with withContext(Dispatchers.IO) (or call suspend functions that do) for blocking work.
Revision: key points
- A ViewModel holds state and logic that outlive a configuration change.
- Owner →
ViewModelStore(a map) → ViewModel; the default key is the class name. - On rotation,
onRetainNonConfigurationInstance()hands the same store to the new Activity. - The store is cleared at
ON_DESTROYonly when!isChangingConfigurations. - Clearing cancels
viewModelScope(SupervisorJob+Dispatchers.Main.immediate) beforeonCleared(). - Fragments’ stores live in
FragmentManagerViewModel;activityViewModels()shares the Activity’s store. - Navigation gives each back-stack entry its own store, cleared when the entry is popped.
- In Compose,
viewModel()usesLocalViewModelStoreOwner; without Navigation that’s the whole Activity. - Never hold an Activity, View or Fragment in a ViewModel.
- Process death ends it: keep what matters in
SavedStateHandleor storage.
Then close the page and try the cross questions from memory.
The fine print
This is the current AndroidX source (October 2026). Details such as the default key and the Compose owner APIs evolve between lifecycle releases, so check the version you ship.
Next in this series: process death, and how SavedStateHandle, Bundle, Parcelable and
Kotlin Serialization fit together.