Skip to main content

Ethereal Eric

Wanderer @ Thamani
Hero Light
Navigation works best when it’s not mysterious but simple & transparent.
Navigation is a core building block of every app. It sits on every user journey—so if it’s hard to reason about, that complexity leaks into every screen and feature.

Why?

In Nav2, navigation relied on imperative commands, which often acted as ā€œfire-and-forgetā€ events. This created a synchronization gap: if a command was dropped during a configuration change or lifecycle shift, your UI state and your actual screen would drift apart. Nav3 eliminates this ā€œsplit-brainā€ issue by treating the backstack as a declarative state—a simple, observable list that serves as the single source of truth. The benefits of this approach are :
  • The backstack is a standard Kotlin list you own and manage, ensuring the UI always reflects your current data.
  • Navigation transitions are reactive rather than command-based, meaning the UI automatically stays in sync with the underlying state.
  • Complex stack manipulations like reordering, filtering, or clearing history use familiar list operations instead of restrictive API calls.
  • State persistence is built-in, making it significantly easier to handle process death and deep linking without losing the user’s place.

How?

1

Define routes

Use a typesafe way to represent each screen (and its arguments)
2

Create a NavHost with a backstack (the backstack owner)

The backstack is your navigation state
3

Emit navigation events from the screen via callbacks

Let’s take a lesson from SOLID principles and ensure navigation related functionality is not known by a particular screen (The UI should be dumb and only be responsible for displaying data - Single responsibility principle)Screens shouldn’t be aware what a backstack is, they only emit events and the owner of the backstack reacts accordingly.
At Thamani we wanted to make navigation more clear, so we added a few extension functions for the team
These two functions (although small) helped a lot in reading through the codebase and understanding what every route does
4

Add NavEntryDecorators for the classic android/viewModel behavior

Introducing the ā€œclassicā€ behavior you’re used to (SavedState + ViewModelStore)
In Nav3, NavEntryDecorators are ā€œwrappersā€ (middleware) that get applied to every NavEntry in your back stack. They let you attach the same behavior to all destinations—without baking that behavior into each screen.

Conclusion

That’s the whole navigation 3 flow
  • Navigation state is a mutable list to represent the backstack (imagine how much you can achieve with this kind of control)
  • Forward navigation is just adding an item at the end of the list.
  • Back navigation is just removing the last item from the list (again you can decide to play around with the list e.g removing 2 items when a user clicks the back button)
  • NavDisplay renders via entryProvider
  • Entry decorators restore the ā€œclassicā€ ViewModel + SavedState behavior
Remember, with great power comes great responsibility.