Navigation 3: Does ViewModel Still Need to Be a State Holder?
Compose and Navigation 3 are changing what ViewModel is responsible for.

For years, Android developers followed one simple rule:
Put your screen state into a ViewModel.
It wasn’t just a recommendation—it was one of the foundations of Android architecture.
A ViewModel survived configuration changes, SavedStateHandle helped restore state after process death, and Activities and Fragments no longer needed to worry about holding screen state themselves.
As a result, ViewModel naturally became the home for almost every piece of screen state.
But over the last few years, something interesting has happened.
Neither Compose nor Navigation 3 made ViewModel obsolete.
Instead, they gradually moved the responsibility for holding state to more appropriate places.
That raises an interesting question:
Does ViewModel still need to be a State Holder?
ViewModel Used to Own Every Kind of Screen State
Before Compose, it was common for ViewModels to hold almost everything related to a screen.
ViewModel
├── UI State
│ ├── text
│ ├── scrollPosition
│ ├── selectedTab
│ └── dialogState
│
├── Navigation State
│ └── screen arguments
│
└── Presentation Logic
This architecture solved several problems at once.
- Survive configuration changes
- Restore state after process death
- Keep Activities and Fragments lightweight
- Separate business logic from the UI
Whenever a new state variable appeared, the natural answer was simply:
Put it in the ViewModel.
Compose Started Moving UI State Back to the UI
Jetpack Compose introduced a different way of thinking.
Instead of pushing every UI value into a ViewModel, the UI itself could own its own state.
Simple UI state could now be written like this:
var query by remember {
mutableStateOf("")
}
When that state needed to survive configuration changes or process death, Compose introduced rememberSaveable.
var query by rememberSaveable {
mutableStateOf("")
}
This was an important shift.
State such as:
- TextField input
- Selected tab
- Expanded items
- Dialog visibility
- Scroll position
is purely UI State.
It doesn’t represent business logic.
Keeping it inside the Composable makes ownership much clearer because the state lives exactly where it is used.
Compose was the first step toward reducing ViewModel’s responsibility as a state holder.
Navigation 3 Takes the Next Step
Navigation 3 pushes this idea even further.
First, navigation arguments no longer need to be passed through SavedStateHandle.
Instead, every destination owns a NavKey.
data class UserDetail(
val userId: Long
) : NavKey
Second, Navigation 3 introduces rememberSerializable.
Unlike rememberSaveable, which saves UI state using the platform save/restore mechanism, rememberSerializable scopes state to the current NavEntry.
var query by rememberSerializable {
mutableStateOf("")
}
This means UI state naturally travels together with the navigation back stack.
Instead of asking ViewModel to remember screen state, each destination owns its own UI state.
Together, these changes make the ownership much clearer.
UI State
│
rememberSerializable
│
Composable
Navigation State
│
NavKey
Presentation Logic
│
ViewModel
UI state belongs to the UI.
Navigation state belongs to Navigation.
Neither naturally belongs to ViewModel anymore.
So What Is Left for ViewModel?
If UI state belongs to the UI and navigation state belongs to NavKey, what remains for ViewModel?
Quite a lot.
ViewModel
├── Repository Coordination
├── Business Logic
├── Flow Transformation
├── Paging
├── Retry
└── viewModelScope
Notice something interesting.
Almost none of these are pieces of state.
They’re responsibilities.
The ViewModel coordinates data, reacts to user actions, performs business logic, and prepares data for the UI.
ViewModel Is Becoming a State Producer
Consider a typical ViewModel.
val uiState = combine(
userRepository.users,
settingsRepository.settings
) { users, settings ->
HomeUiState(
users = users,
darkMode = settings.darkMode
)
}.stateIn(...)
Where is the actual state?
Not inside the ViewModel.
The source of truth already lives in the repositories.
The ViewModel simply transforms that data into something the UI can render.
Rather than holding state, the ViewModel is producing state.
That may sound like a subtle distinction, but it fundamentally changes how we think about the presentation layer.
The Responsibility Has Moved
Before Compose:
ViewModel
├── UI State
├── Navigation State
└── Presentation Logic
Navigation 3:
rememberSerializable
└── UI State (per NavEntry)
NavKey
└── Navigation State
ViewModel
└── Presentation Logic
Compose started moving UI state back to the UI.
Navigation 3 completed the picture by giving navigation state to NavKey and allowing UI state to stay with each NavEntry.
The ViewModel didn’t disappear.
Its responsibility simply became more focused.
A Simpler Architecture
This naturally leads to a cleaner architecture.
+----------------------+
| Composable |
|----------------------|
| UI State |
| rememberSerializable |
+----------+-----------+
|
v
+----------------------+
| ViewModel |
|----------------------|
| Presentation Logic |
| Flow Transformation |
| Repository Calls |
+----------+-----------+
|
v
+----------------------+
| Repository |
|----------------------|
| Persistent Data |
| DataStore / Database |
+----------------------+
Each layer owns what it understands best.
- Composable owns UI state.
- NavKey owns navigation state.
- Repository owns persistent data.
- ViewModel owns presentation logic.
Each responsibility has a natural home.
Conclusion
Navigation 3 doesn’t eliminate ViewModel.
In fact, ViewModel is still the right place for:
- Business logic
- Repository coordination
- Flow transformations
- Paging
- Long-running coroutines
What has changed is why we use it.
Compose introduced rememberSaveable, making it natural for UI state to remain inside the UI.
Navigation 3 extended that idea with rememberSerializable, allowing UI state to be scoped to each NavEntry while NavKey became the home of navigation state.
As a result, ViewModel no longer needs to be the default home for every piece of screen state.
Perhaps it’s time to stop asking:
How should I store this state in my ViewModel?
and instead ask:
Does this state belong in the ViewModel at all?
ViewModel is still essential.
But in the Navigation 3 era, perhaps its primary role is no longer to be a State Holder.
Instead, it has evolved into the home of Presentation Logic.
One concern is whether moving UI state out of the ViewModel means giving up resilience against configuration changes or process death.
The answer is no.
When each layer uses the appropriate state mechanism—
- UI state with
rememberSaveableorrememberSerializable - Navigation state with
NavKey - Persistent data in repositories
—your application can still survive both configuration changes and process death without making the ViewModel the default home for every piece of state.
The goal isn’t to remove state persistence.
It’s to let each layer own the state that naturally belongs to it.