nowinandroid icon indicating copy to clipboard operation
nowinandroid copied to clipboard

Migrate project to Jetpack Navigation 3

Open dturner opened this issue 3 days ago • 4 comments

This PR migrates the project from using Navigation Compose (aka Nav2) to Navigation 3. It follows the official migration guide.

Implementation details

Navigable content Every piece of navigable content in the app is represented by a navigation key. These are objects that implement the NavKey interface. Each feature has an api module which defines these navigation keys.

Example from the :foryou:api module:

data object ForYouNavKey : NavKey

The content itself is defined using extension functions on EntryProviderScope<NavKey>. These extension functions are defined by each feature in its impl module.

Example from the :foryou:impl module:

fun EntryProviderScope<NavKey>.forYouEntry(navigator: Navigator) {
    entry<ForYouNavKey> {
        ForYouScreen(
            onTopicClick = navigator::navigateToTopic,
        )
    }
}

The app (specifically the NiaApp composable) calls these extension functions directly to create an entryProvider. This is used to resolve the navigation keys to NavEntrys which contain the navigable content:

val entryProvider = entryProvider {
    forYouEntry(navigator)
    bookmarksEntry(navigator)
    interestsEntry(navigator)
    topicEntry(navigator)
    searchEntry(navigator)
}

Modeling state Navigation state is held using the NavigationState class and scoped to NiaApp. This is a state holder, it does not create its own state. The navigation state - a top level back stack and sub stacks for each top level key - is created using rememberNavigationState. This uses Nav3's rememberNavBackStack helper function to allow that state to persist config changes and process death.

Handling navigation events Navigation events - navigate and goBack - are handled using the Navigator class. This modifies the NavigationState in response to these events. The Navigator is also scoped to NiaApp and passed directly to each feature's EntryProviderScope extension functions so that the content can trigger navigation events.

Updating the UI NavigationState has an extension function toEntries that uses the entryProvider to convert its state into NavEntrys. These are then displayed using a NavDisplay inside the NiaApp composable. Any changes to the navigation state will cause NavDisplay to recompose and update its UI.

Notably, toEntries calls rememberDecoratedNavEntries for each sub stack. This allows the state of each sub stack to be retained, even when that sub stack isn't in the current stack being displayed by NavDisplay. This is so that when you tap on a previously visited top level destination, the state of all those screens is retained.

Summary of data flow

  • Navigation events flow into Navigator
  • Navigator changes NavigatorState
  • NavigatorState.toEntries recomposes, recreating the list of NavEntrys
  • NavDisplay recomposes, displaying those NavEntrys

Notable points

  • Dependency injection is not used in this navigation architecture. The number of screens is sufficiently small that the extra complexity does not provide sufficient benefit. This might change in future. If you have a lot of screens, check out these architecture recipes.

This PR builds on the work done by @claraf3 in https://github.com/android/nowinandroid/pull/1902

dturner avatar Dec 02 '25 13:12 dturner