chore: remove Nav 3 research document
This commit is contained in:
parent
40ee348b12
commit
d8effb7ea3
1 changed files with 0 additions and 841 deletions
841
NAV3_RESEARCH.md
841
NAV3_RESEARCH.md
|
|
@ -1,841 +0,0 @@
|
|||
# Navigation Compose 3: Research Report
|
||||
|
||||
> Compiled 2026-04-03. Sources: Android Developer docs, JetBrains Kotlin docs, nav3-recipes repo, Android Developers Blog, community blog posts, and release notes.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#1-overview)
|
||||
2. [Core Concepts & API](#2-core-concepts--api)
|
||||
3. [Best Practices](#3-best-practices)
|
||||
4. [Migration Guide (Nav 2 → Nav 3)](#4-migration-guide-nav-2--nav-3)
|
||||
5. [Gotchas, Footguns & Complications](#5-gotchas-footguns--complications)
|
||||
6. [KMP / Compose Multiplatform Considerations](#6-kmp--compose-multiplatform-considerations)
|
||||
7. [Appendix: Version & Dependency Reference](#7-appendix-version--dependency-reference)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### What is Navigation 3?
|
||||
|
||||
Navigation 3 (Nav3) is a **ground-up rewrite** of Android's Jetpack Navigation, designed specifically for Jetpack Compose. Released as stable **1.0.0 on November 19, 2025**, it replaces the controller-based paradigm of Nav 2 with a **state-based, declarative approach** where the developer owns the back stack directly.
|
||||
|
||||
- **Stable release**: 1.0.1 (Feb 11, 2026)
|
||||
- **Latest**: 1.1.0-rc01 (Mar 25, 2026)
|
||||
- **Min SDK**: 23
|
||||
- **Compile SDK**: 36+
|
||||
|
||||
### How It Differs from Nav 2
|
||||
|
||||
| Aspect | Nav 2 | Nav 3 |
|
||||
|--------|-------|-------|
|
||||
| **State ownership** | Library-owned (`NavController`) | Developer-owned (`SnapshotStateList`) |
|
||||
| **Back stack model** | Hidden internal structure | Transparent mutable list |
|
||||
| **Navigation API** | `navController.navigate()` | `backStack.add(key)` |
|
||||
| **Host component** | `NavHost` | `NavDisplay` |
|
||||
| **Route definition** | `composable<T> { }` in `NavHost` | `entry<T> { }` in `entryProvider` |
|
||||
| **Layout flexibility** | Single pane only | Multi-pane via Scenes API |
|
||||
| **Nested graphs** | `navigation<T> { }` | Not needed — flat entry list |
|
||||
| **Design philosophy** | Opinionated framework | Flexible building blocks |
|
||||
| **Compose alignment** | Bolted-on Compose support | Compose-first from scratch |
|
||||
|
||||
### Founding Principles
|
||||
|
||||
1. **You Own the Back Stack** — Navigation state is a `SnapshotStateList<T>` you control directly. Push = `add()`, pop = `removeLastOrNull()`.
|
||||
2. **Get Out of Your Way** — The library provides open, extensible building blocks rather than a black box.
|
||||
3. **Pick Your Building Blocks** — Composable components you combine for your specific needs, with recipes for complex scenarios.
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Action → modify backStack (add/remove) → NavDisplay observes change → entryProvider resolves key → NavEntry.content rendered
|
||||
```
|
||||
|
||||
This follows **Unidirectional Data Flow (UDF)**, fully aligned with Compose's declarative model.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Concepts & API
|
||||
|
||||
### 2.1 Keys (Routes)
|
||||
|
||||
Routes in Nav 3 are simple Kotlin data types that implement `NavKey`:
|
||||
|
||||
```kotlin
|
||||
@Serializable
|
||||
data object Home : NavKey
|
||||
|
||||
@Serializable
|
||||
data class ProductDetail(val id: String) : NavKey
|
||||
|
||||
@Serializable
|
||||
data object Settings : NavKey
|
||||
```
|
||||
|
||||
Keys must be:
|
||||
- `@Serializable` (for state persistence)
|
||||
- Implement `NavKey` (marker interface for `rememberNavBackStack`)
|
||||
|
||||
### 2.2 Back Stack
|
||||
|
||||
The back stack is a `SnapshotStateList<T>` — you own it completely:
|
||||
|
||||
```kotlin
|
||||
// Simple (no persistence across process death)
|
||||
val backStack = remember { mutableStateListOf<Any>(Home) }
|
||||
|
||||
// Persistent (survives config changes + process death)
|
||||
val backStack = rememberNavBackStack(Home)
|
||||
```
|
||||
|
||||
Navigation is just list manipulation:
|
||||
```kotlin
|
||||
// Navigate forward
|
||||
backStack.add(ProductDetail(id = "123"))
|
||||
|
||||
// Navigate back
|
||||
backStack.removeLastOrNull()
|
||||
```
|
||||
|
||||
### 2.3 NavEntry
|
||||
|
||||
`NavEntry` wraps a composable function with its key and optional metadata:
|
||||
|
||||
```kotlin
|
||||
NavEntry(
|
||||
key = ProductDetail("123"),
|
||||
metadata = mapOf("type" to "detail")
|
||||
) {
|
||||
ProductDetailScreen()
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Entry Provider
|
||||
|
||||
Resolves keys to `NavEntry` objects. Two approaches:
|
||||
|
||||
**Lambda function:**
|
||||
```kotlin
|
||||
entryProvider = { key ->
|
||||
when (key) {
|
||||
is Home -> NavEntry(key) { HomeScreen() }
|
||||
is ProductDetail -> NavEntry(key) { ProductDetailScreen(key.id) }
|
||||
else -> NavEntry(Unit) { Text("Unknown") }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**DSL (recommended):**
|
||||
```kotlin
|
||||
entryProvider = entryProvider {
|
||||
entry<Home> { HomeScreen() }
|
||||
entry<ProductDetail> { key -> ProductDetailScreen(key.id) }
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 NavDisplay
|
||||
|
||||
The primary UI component that observes the back stack and renders content:
|
||||
|
||||
```kotlin
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
sceneStrategies = listOf(DialogSceneStrategy()),
|
||||
entryProvider = entryProvider {
|
||||
entry<Home> { HomeScreen() }
|
||||
entry<ProductDetail> { key -> ProductDetailScreen(key.id) }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 2.6 NavEntry Decorators
|
||||
|
||||
Decorators wrap each `NavEntry` with additional logic (state holders, ViewModel stores, logging):
|
||||
|
||||
| Decorator | Purpose |
|
||||
|-----------|---------|
|
||||
| `rememberSaveableStateHolderNavEntryDecorator()` | Persists `rememberSaveable` state across config changes and process death. **Should be first in the list.** |
|
||||
| `rememberViewModelStoreNavEntryDecorator()` | Scopes `ViewModel`s to individual `NavEntry` lifecycle. Requires `androidx.lifecycle:lifecycle-viewmodel-navigation3` dependency. |
|
||||
| `rememberSharedViewModelStoreNavEntryDecorator()` | Enables ViewModel sharing between parent/child entries. |
|
||||
|
||||
Custom decorator example:
|
||||
```kotlin
|
||||
class LoggingDecorator<T : Any> : NavEntryDecorator<T>(
|
||||
decorate = { entry ->
|
||||
Log.d("Nav", "Entered ${entry.contentKey}")
|
||||
entry.Content()
|
||||
},
|
||||
onPop = { key -> Log.d("Nav", "Popped $key") }
|
||||
)
|
||||
```
|
||||
|
||||
### 2.7 Scenes & SceneStrategy
|
||||
|
||||
**Scenes** render one or more `NavEntry` instances. **SceneStrategies** determine which Scene to use based on the current entries.
|
||||
|
||||
**Built-in strategies:**
|
||||
- `SinglePaneSceneStrategy` — Default, shows topmost entry
|
||||
- `DialogSceneStrategy` — Renders entries with dialog metadata as overlays
|
||||
- `ListDetailSceneStrategy` (from `material3-adaptive-navigation3`) — Adaptive list-detail layout
|
||||
|
||||
**Custom strategy example (list-detail):**
|
||||
```kotlin
|
||||
class ListDetailSceneStrategy<T : Any>(
|
||||
val windowSizeClass: WindowSizeClass
|
||||
) : SceneStrategy<T> {
|
||||
override fun SceneStrategyScope<T>.calculateScene(
|
||||
entries: List<NavEntry<T>>
|
||||
): Scene<T>? {
|
||||
if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND))
|
||||
return null
|
||||
val detailEntry = entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null
|
||||
val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null
|
||||
return ListDetailScene(
|
||||
key = listEntry.contentKey,
|
||||
previousEntries = entries.dropLast(1),
|
||||
listEntry = listEntry,
|
||||
detailEntry = detailEntry
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.8 Metadata
|
||||
|
||||
Arbitrary `Map<String, Any>` attached to `NavEntry` or `Scene` for communication between components:
|
||||
|
||||
```kotlin
|
||||
// Type-safe metadata DSL (1.1.0-beta01+)
|
||||
entry<Home>(
|
||||
metadata = metadata {
|
||||
put(NavDisplay.TransitionKey) { fadeIn() togetherWith fadeOut() }
|
||||
put(ListDetailSceneStrategy.ListKey, true)
|
||||
}
|
||||
) { HomeScreen() }
|
||||
```
|
||||
|
||||
### 2.9 Animations
|
||||
|
||||
NavDisplay supports built-in transition animations:
|
||||
|
||||
```kotlin
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
// Global defaults
|
||||
transitionSpec = {
|
||||
slideInHorizontally(initialOffsetX = { it }) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { -it })
|
||||
},
|
||||
popTransitionSpec = {
|
||||
slideInHorizontally(initialOffsetX = { -it }) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { it })
|
||||
},
|
||||
predictivePopTransitionSpec = {
|
||||
slideInHorizontally(initialOffsetX = { -it }) togetherWith
|
||||
slideOutHorizontally(targetOffsetX = { it })
|
||||
},
|
||||
entryProvider = entryProvider {
|
||||
// Per-entry override via metadata
|
||||
entry<ScreenC>(
|
||||
metadata = metadata {
|
||||
put(NavDisplay.TransitionKey) {
|
||||
slideInVertically(initialOffsetY = { it }) togetherWith
|
||||
ExitTransition.KeepUntilTransitionsFinished
|
||||
}
|
||||
}
|
||||
) { ScreenC() }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Shared element transitions:
|
||||
```kotlin
|
||||
SharedTransitionLayout {
|
||||
NavDisplay(
|
||||
sharedTransitionScope = this,
|
||||
// ...
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.10 Deep Linking
|
||||
|
||||
Nav 3 does **not** have built-in deep link support like Nav 2. Instead, you parse intents manually:
|
||||
|
||||
1. Define `DeepLinkPattern` objects mapping URIs to `NavKey` classes
|
||||
2. Parse incoming `Intent.data` into a `DeepLinkRequest`
|
||||
3. Match against patterns to produce a `NavKey`
|
||||
4. Build a synthetic back stack for proper Up navigation
|
||||
|
||||
```kotlin
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val uri: Uri? = intent.data
|
||||
val key: NavKey = uri?.let { parseDeepLink(it) } ?: HomeKey
|
||||
setContent {
|
||||
val backStack = rememberNavBackStack(key)
|
||||
NavDisplay(backStack = backStack, ...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Recipes available: [Basic deep links](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplinks), [Advanced deep links](https://github.com/android/nav3-recipes/tree/main/advanceddeeplinkapp).
|
||||
|
||||
---
|
||||
|
||||
## 3. Best Practices
|
||||
|
||||
### 3.1 Architecture Pattern
|
||||
|
||||
**Recommended: Navigator + NavigationState pattern** (from official migration guide):
|
||||
|
||||
```kotlin
|
||||
// NavigationState — holds all navigation state
|
||||
class NavigationState(
|
||||
val startRoute: NavKey,
|
||||
topLevelRoute: MutableState<NavKey>,
|
||||
val backStacks: Map<NavKey, NavBackStack<NavKey>>
|
||||
) {
|
||||
var topLevelRoute: NavKey by topLevelRoute
|
||||
}
|
||||
|
||||
// Navigator — handles navigation events
|
||||
class Navigator(val state: NavigationState) {
|
||||
fun navigate(route: NavKey) {
|
||||
if (route in state.backStacks.keys) {
|
||||
state.topLevelRoute = route
|
||||
} else {
|
||||
state.backStacks[state.topLevelRoute]?.add(route)
|
||||
}
|
||||
}
|
||||
fun goBack() { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Multi-Module Navigation
|
||||
|
||||
Nav 3 is designed for modular navigation. Each feature module exposes:
|
||||
- **`api` module**: Navigation keys (routes)
|
||||
- **`impl` module**: Entry builders (content)
|
||||
|
||||
```kotlin
|
||||
// feature/chat/impl
|
||||
fun EntryProviderScope<NavKey>.chatEntryBuilder() {
|
||||
entry<ChatRoute> { key -> ChatScreen(key.conversationId) }
|
||||
entry<ChatSettings> { ChatSettingsScreen() }
|
||||
}
|
||||
|
||||
// app module
|
||||
NavDisplay(
|
||||
entryProvider = entryProvider {
|
||||
chatEntryBuilder()
|
||||
authEntryBuilder()
|
||||
settingsEntryBuilder()
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**With Koin DI** (from nav3-recipes):
|
||||
```kotlin
|
||||
// Each feature module provides its entry builder
|
||||
val chatNavigationModule = module {
|
||||
factory<EntryProviderScope<NavKey>.() -> Unit>(named("chat")) {
|
||||
{ chatEntryBuilder(get()) }
|
||||
}
|
||||
}
|
||||
|
||||
// App module collects all builders
|
||||
val entryBuilders: List<EntryProviderScope<NavKey>.() -> Unit> = getAll()
|
||||
NavDisplay(
|
||||
entryProvider = entryProvider {
|
||||
entryBuilders.forEach { builder -> this.builder() }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 3.3 ViewModel Scoping
|
||||
|
||||
ViewModels are scoped to `NavEntry` lifecycle via decorators:
|
||||
|
||||
```kotlin
|
||||
NavDisplay(
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(), // Always first
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
With this setup:
|
||||
- `viewModel()` calls inside NavEntry content are scoped to that entry
|
||||
- ViewModels survive config changes
|
||||
- ViewModels are cleared when the entry is popped
|
||||
|
||||
**With Koin:**
|
||||
```kotlin
|
||||
entry<ChatRoute> { key ->
|
||||
val viewModel = koinViewModel<ChatViewModel>()
|
||||
ChatScreen(viewModel)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Multiple Back Stacks (Bottom Navigation)
|
||||
|
||||
For apps with bottom navigation / top-level routes:
|
||||
|
||||
```kotlin
|
||||
val topLevelRoutes = setOf(Home, Search, Profile)
|
||||
|
||||
// Each top-level route gets its own persistent back stack
|
||||
val backStacks = topLevelRoutes.associateWith { rememberNavBackStack(it) }
|
||||
|
||||
// Switch between stacks (state is retained)
|
||||
var currentTopLevel by remember { mutableStateOf(Home) }
|
||||
```
|
||||
|
||||
Recipe: [Multiple back stacks](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/multiplestacks)
|
||||
|
||||
### 3.5 Adaptive / Multi-Pane Layouts
|
||||
|
||||
Use Material 3 Adaptive for responsive list-detail:
|
||||
|
||||
```kotlin
|
||||
val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>()
|
||||
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
sceneStrategies = listOf(listDetailStrategy),
|
||||
entryProvider = entryProvider {
|
||||
entry<ConversationList>(
|
||||
metadata = ListDetailSceneStrategy.listPane(
|
||||
detailPlaceholder = { Text("Select a conversation") }
|
||||
)
|
||||
) { ConversationListScreen() }
|
||||
|
||||
entry<ConversationDetail>(
|
||||
metadata = ListDetailSceneStrategy.detailPane()
|
||||
) { key -> ConversationDetailScreen(key.id) }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 3.6 Testing Navigation
|
||||
|
||||
**Nav 3 has no dedicated testing library like Nav 2's `navigation-testing`.** However, since navigation state is just a list, testing is simpler:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun testNavigation() {
|
||||
val backStack = mutableStateListOf<NavKey>(Home)
|
||||
|
||||
// Simulate navigation
|
||||
backStack.add(ProductDetail("123"))
|
||||
assertEquals(ProductDetail("123"), backStack.last())
|
||||
|
||||
// Simulate back
|
||||
backStack.removeLastOrNull()
|
||||
assertEquals(Home, backStack.last())
|
||||
}
|
||||
```
|
||||
|
||||
For UI testing, you can use standard Compose testing:
|
||||
```kotlin
|
||||
@Test
|
||||
fun testNavigationUI() {
|
||||
composeTestRule.setContent {
|
||||
val backStack = remember { mutableStateListOf<NavKey>(Home) }
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
entryProvider = entryProvider { /* ... */ }
|
||||
)
|
||||
}
|
||||
// Assert on rendered content
|
||||
}
|
||||
```
|
||||
|
||||
The `SceneStrategyScope` has a no-arg public constructor specifically for testing (added in 1.0.0-beta01).
|
||||
|
||||
---
|
||||
|
||||
## 4. Migration Guide (Nav 2 → Nav 3)
|
||||
|
||||
### 4.1 Prerequisites
|
||||
|
||||
- `compileSdk` 36+
|
||||
- `minSdk` 23+
|
||||
- All destinations must be composable functions
|
||||
- Routes must implement `NavKey`
|
||||
|
||||
### 4.2 Can Nav 2 and Nav 3 Coexist?
|
||||
|
||||
**Yes.** The official migration guide mentions **atomic migration** as the recommended approach, but community sources confirm **incremental migration** is possible:
|
||||
|
||||
- Nav 2 and Nav 3 can operate simultaneously during migration
|
||||
- Features can be migrated one at a time
|
||||
- Business logic and ViewModels don't need rewriting
|
||||
|
||||
However, the official migration guide assumes atomic migration. For incremental, you'd maintain a Nav 2 `NavHost` for unmigrated features and a Nav 3 `NavDisplay` for migrated ones, coordinating between them.
|
||||
|
||||
### 4.3 Step-by-Step Migration
|
||||
|
||||
#### Step 1: Add Dependencies
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
nav3Core = "1.0.1"
|
||||
lifecycleViewmodelNav3 = "2.11.0-alpha03"
|
||||
|
||||
[libraries]
|
||||
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
|
||||
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
|
||||
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
|
||||
```
|
||||
|
||||
#### Step 2: Update Routes to Implement NavKey
|
||||
|
||||
```kotlin
|
||||
// Before
|
||||
@Serializable data object Home
|
||||
@Serializable data class Chat(val conversationId: String)
|
||||
|
||||
// After
|
||||
@Serializable data object Home : NavKey
|
||||
@Serializable data class Chat(val conversationId: String) : NavKey
|
||||
```
|
||||
|
||||
#### Step 3: Create NavigationState & Navigator
|
||||
|
||||
See [Section 3.1](#31-architecture-pattern) for the full pattern.
|
||||
|
||||
#### Step 4: Replace NavController Calls
|
||||
|
||||
| Nav 2 | Nav 3 |
|
||||
|-------|-------|
|
||||
| `navController.navigate(route)` | `navigator.navigate(route)` or `backStack.add(route)` |
|
||||
| `navController.popBackStack()` | `navigator.goBack()` or `backStack.removeLastOrNull()` |
|
||||
| `navController.currentDestination` | `backStack.last()` |
|
||||
| `currentBackStackEntryAsState()` | Direct state observation of `backStack` |
|
||||
| `destination.isRouteInHierarchy()` | `key == navigationState.topLevelRoute` |
|
||||
|
||||
#### Step 5: Convert Destinations
|
||||
|
||||
| Nav 2 | Nav 3 |
|
||||
|-------|-------|
|
||||
| `composable<T> { }` | `entry<T> { }` |
|
||||
| `dialog<T> { }` | `entry<T>(metadata = DialogSceneStrategy.dialog()) { }` |
|
||||
| `navigation<T>(startDestination = ...) { }` | **Delete** — replace references with first child route |
|
||||
| `NavGraphBuilder` extension | `EntryProviderScope<NavKey>` extension |
|
||||
| `entry.toRoute<T>()` | `key` parameter: `entry<T> { key -> ... }` |
|
||||
|
||||
**Before:**
|
||||
```kotlin
|
||||
NavHost(navController = navController, startDestination = Home) {
|
||||
composable<Home> { HomeScreen() }
|
||||
composable<Chat> { entry ->
|
||||
val route = entry.toRoute<Chat>()
|
||||
ChatScreen(route.conversationId)
|
||||
}
|
||||
dialog<ConfirmDialog> { ConfirmDialogScreen() }
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```kotlin
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
sceneStrategies = listOf(DialogSceneStrategy()),
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider {
|
||||
entry<Home> { HomeScreen() }
|
||||
entry<Chat> { key -> ChatScreen(key.conversationId) }
|
||||
entry<ConfirmDialog>(
|
||||
metadata = DialogSceneStrategy.dialog()
|
||||
) { ConfirmDialogScreen() }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Step 6: Remove Nav 2 Dependencies
|
||||
|
||||
Remove all Nav 2 imports, `NavController`, `NavHost`, `NavGraphBuilder`, and gradle dependencies.
|
||||
|
||||
### 4.4 Key Differences to Watch
|
||||
|
||||
1. **No NavController** — State is a mutable list, not a controller object
|
||||
2. **No nested graphs** — Delete `navigation<T>` blocks entirely
|
||||
3. **No built-in deep links** — Must implement manually (see recipes)
|
||||
4. **Route args via key param** — `entry<T> { key -> ... }` instead of `toRoute<T>()`
|
||||
5. **Dialogs via metadata** — `DialogSceneStrategy.dialog()` metadata instead of `dialog<T>`
|
||||
6. **Use `rememberSerializable`, not `rememberSaveable`** — For top-level route persistence
|
||||
7. **Decorators are explicit** — Must add state/ViewModel decorators manually
|
||||
|
||||
---
|
||||
|
||||
## 5. Gotchas, Footguns & Complications
|
||||
|
||||
### 5.1 Known Issues (from release notes)
|
||||
|
||||
| Issue | Status | Versions |
|
||||
|-------|--------|----------|
|
||||
| z-order bug during animation interruptions ([b/459419800](https://issuetracker.google.com/issues/459419800)) | Fixed in 1.1.0-rc01 | |
|
||||
| Lifecycle not capped at STARTED for entries under overlays ([b/483966071](https://issuetracker.google.com/issues/483966071)) | Fixed in 1.1.0-rc01 | |
|
||||
| Crash with SharedTransitionLayout + OverlayScene ([b/478664101](https://issuetracker.google.com/issues/478664101)) | Fixed in 1.1.0-alpha04 | |
|
||||
| IllegalStateException in Android Studio Previews ([b/477149762](https://issuetracker.google.com/issues/477149762)) | Fixed in 1.0.1 / NavigationEvent 1.0.2 | |
|
||||
| Infinite scene recalculation ([b/418153031](https://issuetracker.google.com/issues/418153031)) | Fixed in 1.0.0-alpha07 | |
|
||||
| Screen flicker when swapping backStack with animations ([b/450967248](https://issuetracker.google.com/issues/450967248)) | Fixed in 1.0.0-beta01 | |
|
||||
|
||||
### 5.2 Footguns
|
||||
|
||||
1. **Decorator order matters** — `rememberSaveableStateHolderNavEntryDecorator()` must be **first** in the decorators list. Otherwise, `rememberSaveable` won't work inside NavEntry content.
|
||||
|
||||
2. **Animations in adaptive layouts** — When using `ListDetailSceneStrategy` in two-pane mode (>=600dp), transition animations between top-level destinations may not work. The screen "jumps" without animation.
|
||||
|
||||
3. **No built-in deep link handling** — Unlike Nav 2's `deepLinks { }` DSL, you must parse intents and build synthetic back stacks yourself. See the recipes repo for patterns.
|
||||
|
||||
4. **`rememberNavBackStack` requires NavKey** — If you want persistence across process death, all keys must implement `NavKey` and be `@Serializable`. Simple `data object` without `NavKey` will work but won't survive process death.
|
||||
|
||||
5. **compileSdk 36 required** — This is a hard requirement. If your project is on an older compileSdk, you must upgrade.
|
||||
|
||||
6. **No nested navigation (>1 level)** — The migration guide explicitly states: only one level of nesting is supported. Shared destinations (screens moving between different back stacks) are also unsupported.
|
||||
|
||||
7. **`calculateScene` is no longer `@Composable` (since 1.0.0-alpha11)** — Custom SceneStrategies cannot call composable functions in `calculateScene`. Use `remember` in a factory function instead.
|
||||
|
||||
8. **NavBackStack serialization is Android-only** — The built-in `NavBackStackSerializer` is restricted to Android ([b/420443609](https://issuetracker.google.com/issues/420443609)). For KMP, you need polymorphic serialization (see Section 6).
|
||||
|
||||
9. **API churn in 1.1.x** — The 1.1 branch is still in RC. APIs like `SceneStrategy` receiving `List<SceneStrategy>` instead of chained strategies changed in alpha05. Pin to 1.0.1 for stability.
|
||||
|
||||
### 5.3 Things Harder in Nav 3
|
||||
|
||||
- **Deep linking**: Significantly more manual work required
|
||||
- **Bottom sheet destinations**: No built-in support; recipe available but requires custom SceneStrategy
|
||||
- **Fragment/View interop**: Must use Views-in-Compose wrappers; Nav 3 is Compose-only
|
||||
- **Shared destinations between back stacks**: Not supported
|
||||
- **Testing**: No dedicated testing artifact; testing is easier conceptually (just a list) but lacks the convenience APIs of `TestNavHostController`
|
||||
|
||||
### 5.4 Things Easier in Nav 3
|
||||
|
||||
- **Back stack inspection**: It's just a list — fully observable and debuggable
|
||||
- **Multi-pane/adaptive layouts**: First-class support via Scenes
|
||||
- **Custom navigation logic**: No fighting the NavController
|
||||
- **State management**: Aligns perfectly with Compose state
|
||||
- **ViewModel scoping**: Explicit, clear, and configurable via decorators
|
||||
|
||||
---
|
||||
|
||||
## 6. KMP / Compose Multiplatform Considerations
|
||||
|
||||
### 6.1 Is Nav 3 Available for Compose Multiplatform?
|
||||
|
||||
**Yes.** JetBrains added Nav 3 support in **Compose Multiplatform 1.10.0** (January 2026) for all platforms: Android, iOS, desktop, and web.
|
||||
|
||||
### 6.2 Dependencies (KMP)
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
multiplatform-nav3-ui = "1.0.0-alpha05"
|
||||
|
||||
[libraries]
|
||||
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "multiplatform-nav3-ui" }
|
||||
```
|
||||
|
||||
Nav 3 ships as two artifacts:
|
||||
- `navigation3-common` — Core (transitive dependency)
|
||||
- `navigation3-ui` — NavDisplay and UI components (separate CMP implementation)
|
||||
|
||||
Additional KMP libraries:
|
||||
```toml
|
||||
# Material 3 Adaptive
|
||||
jetbrains-material3-adaptiveNavigation3 = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "compose-multiplatform-adaptive" }
|
||||
|
||||
# ViewModel support
|
||||
jetbrains-lifecycle-viewmodelNavigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "compose-multiplatform-lifecycle" }
|
||||
```
|
||||
|
||||
### 6.3 Critical KMP Requirement: Polymorphic Serialization
|
||||
|
||||
**On Android**, Nav 3 uses reflection-based serialization for `NavKey` types. **This does NOT work on non-JVM platforms (iOS, web).** You must use polymorphic serialization.
|
||||
|
||||
`rememberNavBackStack()` has two overloads:
|
||||
1. **Android-only**: `rememberNavBackStack(startKey)` — uses reflection
|
||||
2. **Multiplatform**: `rememberNavBackStack(config, startKey)` — requires `SavedStateConfiguration`
|
||||
|
||||
**For KMP, you MUST use the second overload:**
|
||||
|
||||
```kotlin
|
||||
@Serializable
|
||||
data object Home : NavKey
|
||||
|
||||
@Serializable
|
||||
data class Chat(val id: String) : NavKey
|
||||
|
||||
// Required for non-JVM platforms
|
||||
private val config = SavedStateConfiguration {
|
||||
serializersModule = SerializersModule {
|
||||
polymorphic(NavKey::class) {
|
||||
subclass(Home::class, Home.serializer())
|
||||
subclass(Chat::class, Chat.serializer())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun App() {
|
||||
val backStack = rememberNavBackStack(config, Home)
|
||||
NavDisplay(backStack = backStack, ...)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Multi-Module KMP Pattern
|
||||
|
||||
For our multi-module setup, use sealed interfaces per feature module:
|
||||
|
||||
```kotlin
|
||||
// feature/auth/api
|
||||
@Serializable
|
||||
sealed interface AuthRoute : NavKey
|
||||
|
||||
@Serializable data object Login : AuthRoute
|
||||
@Serializable data object Register : AuthRoute
|
||||
|
||||
// feature/chat/api
|
||||
@Serializable
|
||||
sealed interface ChatRoute : NavKey
|
||||
|
||||
@Serializable data class Conversation(val id: String) : ChatRoute
|
||||
|
||||
// app module — aggregate all feature routes
|
||||
private val config = SavedStateConfiguration {
|
||||
serializersModule = SerializersModule {
|
||||
polymorphic(NavKey::class) {
|
||||
subclassesOfSealed<AuthRoute>()
|
||||
subclassesOfSealed<ChatRoute>()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, with Koin DI, each module can expose a `SerializersModule` and the app module combines them:
|
||||
|
||||
```kotlin
|
||||
// Each feature module
|
||||
val chatSerializerModule = SerializersModule {
|
||||
polymorphic(NavKey::class) {
|
||||
subclass(Conversation::class, Conversation.serializer())
|
||||
}
|
||||
}
|
||||
|
||||
// App module
|
||||
val combinedConfig = SavedStateConfiguration {
|
||||
serializersModule = chatSerializerModule + authSerializerModule + settingsSerializerModule
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Impact on Our Current Setup
|
||||
|
||||
We currently use `org.jetbrains.androidx.navigation:navigation-compose:2.9.2`. For Nav 3:
|
||||
|
||||
| Current (Nav 2 KMP) | Nav 3 KMP |
|
||||
|---------------------|-----------|
|
||||
| `org.jetbrains.androidx.navigation:navigation-compose:2.9.2` | `org.jetbrains.androidx.navigation3:navigation3-ui:1.0.0-alpha05` |
|
||||
| `NavHost` | `NavDisplay` |
|
||||
| `NavController` | `SnapshotStateList` + custom Navigator |
|
||||
| Reflection-based routing | **Must use** `SavedStateConfiguration` with polymorphic serialization |
|
||||
| Direct route access | Key-based entry access |
|
||||
|
||||
**Key considerations:**
|
||||
- The JetBrains Nav 3 artifact is at **1.0.0-alpha05** (not yet stable for KMP)
|
||||
- Android-side Nav 3 is stable at 1.0.1
|
||||
- We must implement polymorphic serialization for all routes
|
||||
- Our Koin-based DI can provide serializer modules per feature
|
||||
- Browser history navigation (for web target) is expected in Nav 3 KMP 1.1.0
|
||||
|
||||
### 6.6 CMP Nav 3 Recipes
|
||||
|
||||
JetBrains maintains a Compose Multiplatform fork of the nav3-recipes:
|
||||
- [github.com/terrakok/nav3-recipes](https://github.com/terrakok/nav3-recipes)
|
||||
|
||||
---
|
||||
|
||||
## 7. Appendix: Version & Dependency Reference
|
||||
|
||||
### AndroidX Dependencies (Android-only)
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
nav3Core = "1.0.1" # Stable
|
||||
# nav3Core = "1.1.0-rc01" # Latest RC
|
||||
lifecycleViewmodelNav3 = "2.11.0-alpha03"
|
||||
material3AdaptiveNav3 = "1.3.0-alpha09"
|
||||
kotlinxSerializationCore = "1.9.0"
|
||||
|
||||
[libraries]
|
||||
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
|
||||
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
|
||||
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
|
||||
androidx-material3-adaptive-navigation3 = { module = "androidx.compose.material3.adaptive:adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
|
||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
|
||||
```
|
||||
|
||||
### Compose Multiplatform Dependencies (KMP)
|
||||
|
||||
```toml
|
||||
[versions]
|
||||
multiplatform-nav3-ui = "1.0.0-alpha05"
|
||||
compose-multiplatform-adaptive = "1.3.0-alpha02"
|
||||
compose-multiplatform-lifecycle = "2.10.0-alpha05"
|
||||
|
||||
[libraries]
|
||||
jetbrains-navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "multiplatform-nav3-ui" }
|
||||
jetbrains-material3-adaptiveNavigation3 = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation3", version.ref = "compose-multiplatform-adaptive" }
|
||||
jetbrains-lifecycle-viewmodelNavigation3 = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "compose-multiplatform-lifecycle" }
|
||||
```
|
||||
|
||||
### Nav 3 Recipes Repository
|
||||
|
||||
- Android: [github.com/android/nav3-recipes](https://github.com/android/nav3-recipes) (1.2k stars)
|
||||
- CMP: [github.com/terrakok/nav3-recipes](https://github.com/terrakok/nav3-recipes)
|
||||
|
||||
Available recipes:
|
||||
- Basic, Saveable back stack, Entry provider DSL
|
||||
- Deep links (basic + advanced)
|
||||
- Dialog, BottomSheet, List-Detail, Two-pane, Supporting Pane
|
||||
- Animations
|
||||
- Navigation UI, Multiple back stacks
|
||||
- Conditional navigation
|
||||
- Modularized navigation (Hilt + Koin)
|
||||
- ViewModel patterns (basic, Hilt, Koin, shared)
|
||||
- Retain values for back stack composables
|
||||
- Returning results (events + state)
|
||||
- Fragment/View interop
|
||||
|
||||
### Sources
|
||||
|
||||
- [Navigation 3 Overview](https://developer.android.com/guide/navigation/navigation-3)
|
||||
- [Navigation 3 Basics](https://developer.android.com/guide/navigation/navigation-3/basics)
|
||||
- [Navigation 3 Migration Guide](https://developer.android.com/guide/navigation/navigation-3/migration-guide)
|
||||
- [Navigation 3 Save State](https://developer.android.com/guide/navigation/navigation-3/save-state)
|
||||
- [Navigation 3 Metadata](https://developer.android.com/guide/navigation/navigation-3/metadata)
|
||||
- [Navigation 3 Modularize](https://developer.android.com/guide/navigation/navigation-3/modularize)
|
||||
- [Navigation 3 Scenes](https://developer.android.com/guide/navigation/navigation-3/scenes)
|
||||
- [Navigation 3 Animations](https://developer.android.com/guide/navigation/navigation-3/animate-destinations)
|
||||
- [Navigation 3 Entry Decorators](https://developer.android.com/guide/navigation/navigation-3/naventrydecorators)
|
||||
- [Navigation 3 Get Started](https://developer.android.com/guide/navigation/navigation-3/get-started)
|
||||
- [Navigation 3 Release Notes](https://developer.android.com/jetpack/androidx/releases/navigation3)
|
||||
- [Navigation 3 in Compose Multiplatform](https://kotlinlang.org/docs/multiplatform/compose-navigation-3.html)
|
||||
- [Compose Multiplatform 1.10.0 Blog Post](https://blog.jetbrains.com/kotlin/2026/01/compose-multiplatform-1-10-0/)
|
||||
- [Jetpack Navigation 3 is Stable (Blog)](https://android-developers.googleblog.com/2025/11/jetpack-navigation-3-is-stable.html)
|
||||
- [Announcing Jetpack Navigation 3 (Blog)](https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html)
|
||||
- [nav3-recipes GitHub](https://github.com/android/nav3-recipes)
|
||||
- [Deep Link Guide](https://github.com/android/nav3-recipes/blob/main/docs/deeplink-guide.md)
|
||||
- [Shared ViewModel Recipe](https://developer.android.com/guide/navigation/navigation-3/recipes/sharedviewmodel)
|
||||
Loading…
Reference in a new issue