Who Owns Your ViewModel? Lifetime, Scope, and the Three Owners in Navigation 3

Choosing a ViewModel scope is really asking one question: when something dies, should the ViewModel die with it? Get that right and state flows naturally through your app. Get it wrong and you’re either losing state too early or holding onto it long after it’s useful.
In Navigation 2, this was mostly handled for you. In Navigation 3, you’re in charge — and the defaults changed in a way that will surprise you if you’re migrating from Navigation2.
The thing that actually controls a ViewModel’s lifetime
A ViewModel doesn’t manage its own lifecycle. It lives inside a ViewModelStore, and that store is owned by a ViewModelStoreOwner. When the owner is destroyed, the store is cleared, and ViewModel.onCleared() is called.
So the real question is: what can be a ViewModelStoreOwner ?
In a Navigation3 + Compose app, there are three practical answers:
- The Activity — the default in Navigation3. Lives as long as the app is in the foreground.
- A NavEntry — opt-in via a decorator. Lives as long as that entry is on the back stack.
- A composable call site — via
rememberViewModelStoreOwner(). Lives as long as the composable is in composition.
Each one answers the “live and die with what?” question differently.
How Navigation3 handles this by default
In Navigation2, each NavBackStackEntry was a ViewModelStoreOwner. Calling viewModel() inside a screen composable automatically scoped the ViewModel to that destination — created on arrival, destroyed on leaving.
Navigation3 changed this. By default, viewModel() resolves the nearest ViewModelStoreOwner from LocalViewModelStoreOwner. In a typical single-Activity Compose app using Navigation3, that’s the Activity itself.
The practical consequence:
| Navigation2 | Navigation3 (default) | |
|---|---|---|
| Owner | NavBackStackEntry |
Activity |
| ViewModel created | On each navigation to the screen | Once, on first visit |
| ViewModel cleared | When you navigate away | When the Activity is destroyed |
init {} runs |
Every visit | Once per app session |
This is a silent change. Nothing crashes. But if you have code like this:
class DetailViewModel : ViewModel() {
init {
loadData() // ⚠️ only called once in Navigation3 by default
}
}
You’ll see stale data on every revisit. The ViewModel is alive and well — it just never re-fetched anything, because it was never recreated.
Owner 1: The Activity (Navigation3 default)

No setup required. Call viewModel() anywhere inside a Navigation3 destination and you get an Activity-scoped ViewModel.
// build.gradle.kts
dependencies {
implementation("androidx.navigation3:navigation3-ui:1.0.0")
}
@Composable
fun HomeScreen() {
val viewModel = viewModel<HomeViewModel>()
// ...
}
This is a good choice when:
- Multiple destinations share the same state (cart contents, auth info, a multi-step form)
- Initialization is expensive and the result is stable across navigation
- You explicitly want to cache data for the session
Just be aware that init {} is your enemy here if you expect fresh data on each visit. Move triggers to LaunchedEffect instead:
@Composable
fun DetailScreen() {
val viewModel = viewModel<DetailViewModel>()
LaunchedEffect(Unit) {
viewModel.loadData() // runs each time the composable enters composition
}
}
Note that this reruns the logic, but the ViewModel’s existing state persists. If you need a truly clean slate on each visit, the next owner type is what you want.
Owner 2: A NavEntry (opt-in with a decorator)

To scope a ViewModel to a specific navigation destination — so it’s created when you arrive and destroyed when you leave — you need the lifecycle-viewmodel-navigation3 add-on library.
Add the dependency:
// build.gradle.kts
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0")
}
Register the decorators on your NavDisplay:
val backStack = rememberNavBackStack(startDestination)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(
// Saves Compose state (rememberSaveable) per entry
rememberSaveableStateHolderNavEntryDecorator(),
// Creates a ViewModelStoreOwner per entry
rememberViewModelStoreNavEntryDecorator(),
),
entryProvider = entryProvider {
entry<HomeKey> { HomeScreen() }
entry<DetailKey> { key -> DetailScreen(id = key.id) }
}
)
With this in place, each NavEntry becomes a ViewModelStoreOwner. The ViewModel is created when the entry is pushed onto the back stack, and onCleared() is called when it’s popped. This is the Navigation2-style behavior most people expect.
@Composable
fun DetailScreen(id: String) {
// Scoped to this NavEntry — fresh instance on every navigation
val viewModel = viewModel<DetailViewModel>()
// ...
}
Owner 3: A composable call site (rememberViewModelStoreOwner)

Sometimes neither the Activity nor a full navigation entry is the right granularity. Think items in a LazyColumn, or panels that appear and disappear based on state — UI components that are too small to be destinations but still need their own isolated state.
For those cases, rememberViewModelStoreOwner() lets you create a ViewModelStoreOwner scoped directly to a composable’s presence in the composition tree.
import androidx.lifecycle.viewmodel.compose.rememberViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ProductCard(productId: String) {
val owner = rememberViewModelStoreOwner()
val viewModel = viewModel<ProductCardViewModel>(viewModelStoreOwner = owner)
// ...
}
The store is cleared automatically when the composable leaves composition, and it survives configuration changes while it’s present. No navigation system involved — the composable itself is the owner.
Picking the right owner
| I want the ViewModel to live as long as… | Owner to use |
|---|---|
| The whole app session | Activity (Navigation3 default, no setup needed) |
| This specific screen is on the back stack | NavEntry + rememberViewModelStoreNavEntryDecorator() |
| This composable is in the composition tree | rememberViewModelStoreOwner() |
| No side effects, just UI state | remember / rememberSaveable — no ViewModel needed |
Ownership is the design decision
The core insight is that a ViewModel’s lifetime isn’t something you set on the ViewModel itself — it’s a property of whoever owns it. In Navigation3, that owner is the Activity by default, which is a meaningful change from Navigation2’s per-destination behavior.
Once you understand what can be a ViewModelStoreOwner — the Activity, a NavEntry, or a composable call site — the scoping decision becomes straightforward: find the thing whose lifetime matches the data’s natural lifecycle, and make that the owner.
Navigation3 just asks you to be explicit about it, rather than assuming for you.
#Android #Hilt #Jetpack Compose #Markdown #Programming #Syntax