← Back to list

SavedStateHandle in the Navigation 3 Era: It’s Time to Rethink Your ViewModel

If you’ve been developing Android apps with Jetpack Compose for a while, you’ve probably written something like this:

chanzmao · 2026-06-18 09:30 · 0 claps · 2.6 min read
#android #jetpack-compose #kotlin #android-development #android-app-development
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

SavedStateHandle in the Navigation 3 Era: It’s Time to Rethink Your ViewModel

Image created by AI

Image created by AI

If you’ve been developing Android apps with Jetpack Compose for a while, you’ve probably written something like this:

class DetailViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    private val id: String = checkNotNull(savedStateHandle["id"])
}

For years, this was considered the standard way to retrieve navigation arguments.

But Navigation 3 changes the conversation.

The Hidden Problem with SavedStateHandle

At first glance, SavedStateHandle seems convenient. Your ViewModel gets everything it needs without additional parameters.

However, there’s a subtle architectural issue.

The ViewModel now knows how it was navigated to.

  • argument names (“id”)
  • key lookups
  • navigation contracts

That means your business logic is coupled to your navigation framework.

If someone renames "id” to "userId”, the compiler won’t help you. The mistake only appears at runtime.

The ViewModel shouldn’t care where its data came from.

It should only care about the data itself.

Navigation 3 Encourages a Different Design

Navigation 3 shifts responsibility.

Instead of hiding arguments inside SavedStateHandle, navigation destinations become typed objects that you own.

Imagine a route like this:

@Serializable 
data class UserDetail( 
    val id: String
)

When navigating, you already have a strongly typed object.

Rather than asking the ViewModel to decode it again, simply pass the value directly.

UserDetailScreen(
    id = route.id
)

Then inject it into the ViewModel.

class UserDetailViewModel(
    private val id: String
) : ViewModel()

Now the ViewModel has exactly what it needs — and nothing more.

No string keys.

No navigation knowledge.

No runtime surprises.

But What About Process Death?

Many developers worry that removing SavedStateHandle means losing process death recovery.

Fortunately, Navigation 3 already solves this.

Your back stack is serializable and can be restored automatically through APIs like rememberNavBackStack().

When the process is recreated, the route object is restored first, and the ViewModel receives the same constructor parameters again. The navigation argument doesn’t need to live inside SavedStateHandle anymore.

In other words:

  • Navigation restores navigation state.
  • ViewModel owns business logic.
  • Each layer has a clear responsibility.

Cleaner Boundaries

This design also improves testing.

Instead of constructing a fake SavedStateHandle:

SavedStateHandle(
    mapOf("id" to "123")
)

You simply write:

UserDetailViewModel("123")

The test focuses on business logic rather than framework behavior.

The same applies to previews and dependency injection.

SavedStateHandle Still Has a Place

None of this means SavedStateHandle is obsolete.

[embed]Ditching SavedStateHandle and rememberSaveable: Embracing State-Aware Routing with Navigation 3 How State-Aware Routing with NavKey handles configuration changes and process death natively.proandroiddev.com

It’s still the correct tool for storing temporary UI state that should survive configuration changes or process recreation.

Examples include:

  • search queries
  • scroll positions
  • partially completed forms
  • filter selections

These values belong to the current screen state, not to navigation itself.

Navigation arguments and UI state are different concerns, and Navigation 3 finally makes that distinction much clearer.

Final Thoughts

SavedStateHandle wasn’t a bad API.

It solved a real limitation in previous versions of the Navigation library.

But Navigation 3 removes the need to treat navigation arguments as hidden state inside a ViewModel.

Passing dependencies explicitly leads to code that’s easier to understand, easier to test, and less tightly coupled to the navigation framework.

Sometimes the biggest architectural improvements aren’t about adding new APIs.

They’re about removing responsibilities that never belonged there in the first place.

[embed]SavedStateHandle in the Navigation 3 Era With Navigation 3, the role of SavedStateHandle has become much clearer. Use NavKey for navigation and screen…chanzmao.benigumo.com

Enjoyed this article? Here’s how you can support my work:

  • 👏 Clap (up to 50 times!) if you found this insightful.
  • 👤 Follow me and turn on notifications (🔔) so you never miss the next deep dive.
  • 📧 Subscribe to get my latest stories delivered directly to your inbox.

You can find all my articles on Android architecture, Jetpack Compose, and Kotlin in this curated reading list.


메타데이터
post_id
c581648781d4
slug
savedstatehandle-in-the-navigation-3-era-its-time-to-rethink-your-viewmodel-c581648781d4
url
https://medium.com/@chanzmao/savedstatehandle-in-the-navigation-3-era-its-time-to-rethink-your-viewmodel-c581648781d4
canonical_url
https://medium.com/@chanzmao/savedstatehandle-in-the-navigation-3-era-its-time-to-rethink-your-viewmodel-c581648781d4
author_url
https://medium.com/@chanzmao
status
ok
fetched_at
2026-06-20 20:29:01