Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
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
5. Replace deep link integration
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:
- Start with leaf screens that have simple navigation — they are the easiest to convert since they have few navigation dependencies
- Move shared/graph-scoped ViewModels last — these require the most restructuring (entry decorators replace graph scoping)
- Keep Nav 2 running alongside Nav 3 during transition if needed — they can coexist in the same app
- Convert navigation effects — update ViewModel effect handlers from
navController.navigate()calls tobackStack.add()calls one screen at a time - 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.