Shared navigation concepts for Nav 2 and Nav 3. Load first, then see version-specific references.
References:
| Criterion | Nav 3 (NavDisplay) | Nav 2 (NavHost / NavController) |
|---|---|---|
| Back stack ownership | You own it (SnapshotStateList) |
Library owns it (NavController) |
| Navigation model | List manipulation — add(), removeLastOrNull() |
Imperative — navigate(), popBackStack() |
| MVI alignment | Natural — back stack is state you mutate | Requires bridging — controller calls in effect handlers |
| Deep link parsing | You parse URIs, construct back stack manually | Built-in NavDeepLink parsing |
| Scenes / adaptive layouts | First-class: dialog, bottom sheet, list-detail | Manual: separate composable overlays |
| CMP support | Full (Android, iOS, Desktop, Web) | Android-only (JetBrains forks exist but differ) |
| Maturity | Newer — verify artifact stability for production | Stable, battle-tested |
| Fragment interop | None | Full Fragment/Activity integration |
When to use Nav 3:
When to use Nav 2:
NavHost/NavControllerNavDeepLinkNavGraph DSLThe architectural rule: ViewModels emit semantic effects; the route layer handles navigation. This rule applies identically to both Nav 2 and Nav 3.
sealed interface ItemEffect {
data object NavigateBack : ItemEffect
data class OpenDetails(val id: String) : ItemEffect
}
// Nav 3 route layer — manipulates back stack
CollectEffect(viewModel.effect) { effect ->
when (effect) {
is ItemEffect.NavigateBack -> backStack.removeLastOrNull()
is ItemEffect.OpenDetails -> backStack.add(Details(effect.id))
}
}
// Nav 2 route layer — calls NavController
CollectEffect(viewModel.effect) { effect ->
when (effect) {
is ItemEffect.NavigateBack -> navController.navigateUp()
is ItemEffect.OpenDetails -> navController.navigate(Detail(effect.id))
}
}
LaunchedEffect or event handler callbacksNavController (Nav 2) to the ViewModel or leaf composablesNavigateBack, OpenDetails(id))| Anti-pattern | Applies to | Why it hurts | Better replacement |
|---|---|---|---|
| Navigating during composition | Both | Triggers on every recomposition, causes infinite loops | Navigate in LaunchedEffect or event handler callbacks |
| Passing NavController/back stack to ViewModel | Both | Violates MVI boundary, navigation becomes business logic | ViewModel emits semantic effects; route handles navigation |
| String-based routes without type safety | Both | No compile-time checking, argument mismatch at runtime | @Serializable data classes/objects |
Missing onBack handler |
Nav 3 | System back gesture does nothing | Always provide onBack = { backStack.removeLastOrNull() } |
| Globally-scoped ViewModel for per-screen data | Both | Data leaks across screens, not cleared on pop | Entry-scoped VMs (Nav 3 decorators) or destination-scoped VMs (Nav 2) |
| Recreating back stacks on tab switch | Both | Loses user navigation history within tabs | Persistent per-tab stacks (Nav 3) or saveState/restoreState (Nav 2) |
| Missing entry decorators | Nav 3 | ViewModels leak, saveable state lost | Always include both rememberSaveableStateHolderNavEntryDecorator and rememberViewModelStoreNavEntryDecorator |
| Using Nav 2 in new MVI codebases | Nav 3 preferred | Nav 3's user-owned back stack aligns better with MVI state ownership | Prefer Nav 3 NavDisplay for new MVI-first projects; Nav 2 remains valid for existing codebases |
Load the file that matches your task: