Files
vnidrop/.codex/skills/compose-skill/references/navigation-migration.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

4.9 KiB

Migrating from Nav 2 to Nav 3

Nav 2 → Nav 3 migration based on official docs. Nav 2 is not deprecated — migration is optional.

For Nav 3 full reference, see navigation-3.md. For Nav 2 full reference, see navigation-2.md. For shared concepts and decision guide, see navigation.md.

Key Conceptual Shifts

Nav 2 Nav 3
NavController owns the back stack You own the back stack (SnapshotStateList)
NavHost renders composable destinations NavDisplay observes the back stack and renders entries
Routes are strings or @Serializable types Keys are @Serializable types implementing NavKey
Imperative navigation (navController.navigate()) List manipulation (backStack.add(), backStack.removeLastOrNull())
NavGraph groups destinations No separate graph — entries are resolved by the entryProvider
Deep links parsed by Navigation library Deep links parsed by your code — you construct the back stack
Graph-scoped ViewModels via getBackStackEntry() Entry-scoped ViewModels via rememberViewModelStoreNavEntryDecorator()
currentBackStackEntryAsState() for selected tab Direct back stack inspection (backStack.last())
saveState/restoreState for tab persistence Persistent per-tab stacks or root swap pattern

Migration Steps

1. Replace route types with NavKey

// Nav 2
@Serializable data object Home
@Serializable data class Detail(val id: String)

// Nav 3
@Serializable data object Home : NavKey
@Serializable data class Detail(val id: String) : NavKey

2. Replace NavController with a SnapshotStateList back stack

// Nav 2
val navController = rememberNavController()
navController.navigate(Detail(id))

// Nav 3
val backStack = rememberNavBackStack(Home)
backStack.add(Detail(id))

3. Replace NavHost with NavDisplay

Replace NavHost + composable<T> with NavDisplay + entryProvider + entry<T>. Each composable block becomes an entry block; navController.navigate() becomes backStack.add(). For full NavDisplay API, decorators, and DI wiring, see navigation-3.md and navigation-3-di.md.

4. Replace graph-scoped ViewModels with entry decorators

Nav 3 scopes ViewModels to entries automatically via rememberViewModelStoreNavEntryDecorator(). For shared state across entries, lift state to a parent composable or use a shared ViewModel at the Activity/App scope.

Nav 2 graph-scoped pattern:

val parentEntry = remember(entry) { navController.getBackStackEntry("checkout") }
val sharedViewModel: CheckoutViewModel = hiltViewModel(parentEntry)

Nav 3 equivalent — lift to parent or share via DI:

// Option 1: shared ViewModel at a higher scope
val sharedViewModel: CheckoutViewModel = viewModel() // Activity-scoped

// Option 2: state hoisting in a parent composable
// The parent composable holds shared state, passes it to child entries

Nav 3 does not parse deep links — parse URIs in your platform entry point and construct the back stack manually:

// Nav 2
composable<Detail>(
    deepLinks = listOf(navDeepLink<Detail>(basePath = "https://example.com/detail"))
) { /* ... */ }

// Nav 3
LaunchedEffect(deepLinkId) {
    if (deepLinkId != null) {
        backStack.clear()
        backStack.addAll(listOf(Home, Detail(deepLinkId)))
    }
}

6. Replace tab navigation

// Nav 2 — NavigationBar + currentBackStackEntryAsState + saveState/restoreState
navController.navigate(tab.route) {
    popUpTo(startDest) { saveState = true }
    launchSingleTop = true
    restoreState = true
}

// Nav 3 — direct back stack manipulation
while (backStack.size > 1) backStack.removeLast()
backStack[0] = targetTopLevelKey

Incremental Migration

You do not have to migrate everything at once. The official docs recommend:

  1. Start with leaf screens that have simple navigation — they are the easiest to convert since they have few navigation dependencies
  2. Move shared/graph-scoped ViewModels last — these require the most restructuring (entry decorators replace graph scoping)
  3. Keep Nav 2 running alongside Nav 3 during transition if needed — they can coexist in the same app
  4. Convert navigation effects — update ViewModel effect handlers from navController.navigate() calls to backStack.add() calls one screen at a time
  5. Test each migrated screen independently before moving to the next

Coexistence strategy

During migration, Nav 2 and Nav 3 can coexist in the same app. Use Nav 3 for new feature modules while keeping Nav 2 for existing screens. Bridge between them at the Activity level — a Nav 2 destination can launch an Activity/Fragment that hosts Nav 3, or vice versa.