Migrate to Navigation 3

Navigation 3 represents a fundamental shift in how Jetpack Compose handles navigation state and offers significant architectural advantages over Navigation 2.

Understand the architectural changes and steps required to migrate a Wear Compose app from Navigation 2 to Navigation 3.

Key advantages of Navigation 3

  • Direct Back Stack Control: The NavBackStack is fundamentally just a mutable list of NavKey objects, representing the history of screens the user has visited. You control it exactly like you would any Kotlin MutableList (add, removeLast, clear). You directly manipulate the list to perform navigation actions, such as adding a key to go forward or removing a key to go back.
  • Compose-First Design: The back stack is modeled as standard observable state. Modifying your navigation history behaves exactly like updating any other Compose state, automatically triggering recomposition to display the current screen.
  • Type-Safe by Default: String-based routes are eliminated entirely. Navigation utilizes serializable data objects and data classes.
  • Decoupled Presentations (Scene Strategies): The UI transition layer (NavDisplay and SwipeDismissableSceneStrategy) is entirely separated from the state tracking (NavBackStack), enabling simpler integration of built-in Wear OS navigation transitions.

Migration steps

1. Update dependencies

Remove the old androidx.wear.compose:compose-navigation dependency and introduce the new split Navigation 3 dependencies, along with Kotlin serialization support.

Remove:

implementation("androidx.wear.compose:compose-navigation:...")

Add:

implementation("androidx.navigation3:navigation3-runtime:...") // State logic
implementation("androidx.navigation3:navigation3-ui:...")      // Display logic
implementation("androidx.wear.compose:compose-navigation3:...") // Wear gestures
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:...") // Requires compiler plugin

2. Update destinations to implement NavKey

In Navigation 2, you might have used strings or generic objects for routing. In Navigation 3, you must implement the NavKey marker interface and annotate every screen object with @Serializable.

Why is this required? To guarantee that the back stack can be saved and restored across process death, the underlying navigation3-runtime relies on kotlinx-serialization to serialize the state.

Before (Navigation 2 - Generic Type-Safe Routes):

sealed class Nav2Screen {
    data object Landing : Nav2Screen()
    data object List : Nav2Screen()
}

After (Navigation 3 - NavKey + Serializable):

@Serializable
sealed interface MigrationScreen : NavKey {
    @Serializable
    data object Landing : MigrationScreen

    @Serializable
    data object List : MigrationScreen
}

3. Replace the Routing Logic (NavController to NavBackStack)

Replace your NavController with a NavBackStack initialized via rememberNavBackStack. You also need to instantiate the SwipeDismissableSceneStrategy specifically for Wear OS.

Before (Navigation 2):

val navController = rememberSwipeDismissableNavController()

After (Navigation 3):

val backStack = rememberNavBackStack(MigrationScreen.Landing as NavKey)
val strategy = rememberSwipeDismissableSceneStrategy<NavKey>()

4. Replace NavHost with NavDisplay and the entryProvider DSL

The NavHost container and its internal composable("route") { ... } builder DSL are replaced by NavDisplay and the entryProvider { entry<Key> { ... } } DSL.

Before (Navigation 2):

SwipeDismissableNavHost(navController = navController, startDestination = "menu") {
    composable("menu") {
        GreetingScreen(
            onShowList = { navController.navigate("list") }
        )
    }
    composable("list") {
        ListScreen()
    }
}

After (Navigation 3):

NavDisplay(
    backStack = backStack,
    sceneStrategies = listOf(strategy),
    entryProvider = entryProvider {
        entry<MigrationScreen.Landing> {
            GreetingScreen(
                onShowList = { backStack.add(MigrationScreen.List) }
            )
        }
        entry<MigrationScreen.List> {
            ListScreen()
        }
    }
)