2. 回転しても画面を維持したい Survive rotation 3. プロセス死から復元したい Survive process death 4. 離れた画面の状態を保持したい Keep in-screen state 5. ViewModel を画面にスコープしたい Scope ViewModels to screens 6. タブレットで2ペインにしたい Two panes on tablets 7. マルチモジュールに対応したい Multi-module support 9
{ mutableStateListOf<Route>(Route.MemoList) } Nav3 val backStack: NavBackStack<NavKey> = rememberNavBackStack(MemoList) @Serializable(with = NavBackStackSerializer::class) public class NavBackStack<T : NavKey> public constructor(internal val base: SnapshotStateList<T>) : MutableList<T> by base, StateObject by base, RandomAccess by base { public constructor() : this(base = mutableStateListOf()) public constructor(vararg elements: T) : this(base = mutableStateListOf(*elements)) } 35
-> Unit, modifier: Modifier = Modifier, ) { var query by rememberSaveable { mutableStateOf("") } … MemoList の検索ボックス文字列が → MemoDetail → 戻る で消える The search text in MemoList is lost after going to MemoDetail and back 40
Modifier = Modifier, ) { val viewModel = viewModel { MemoDetailViewModel(id) } この ViewModel どこにスコープされる? → MainActivity にスコープされてしまう What is this ViewModel scoped to? → It ends up scoped to MainActivity 57
Modifier = Modifier,この ViewModel どこにスコープされる? ) { → MainActivity にスコープされてしまう val viewModel = viewModel { MemoDetailViewModel(id) } メモ1を開いた後にメモ2を開くとメモ1の内容が 表示される 画面を pop しても onCleared() が呼ばれない What is this ViewModel scoped to? → It ends up scoped to MainActivity Open memo 1 then memo 2 → memo 1's content shows up. Popping never calls onCleared() 58
show Default: show only last() w >= 600dp [List] [List] null [List, Detail] [Detail] [List, Detail] [List, Detail, Edit] [Edit] null Scene : 要素をどう表示するか SceneStrategy : back stack から Scene を計算 how to display the elements computes a Scene from the back stack 77
val content: @Composable () -> Unit, Turn the route-to-screen when into a lambda ) val entryProvider: (Route) -> RouteEntry = { route -> when (route) { Route.MemoList -> RouteEntry(route) { MemoListScreen(…) } is Route.MemoDetail -> RouteEntry(route) { MemoDetailVmScreen(…) } is Route.MemoEdit -> RouteEntry(route) { MemoEditScreen(…) } } } 80
SinglePaneSceneStrategy : SceneStrategy { override fun calculateScene(entries: List<RouteEntry>): Scene { return SinglePaneScene(entries.last()) } } back stack の一番上の Route を SinglePaneScene で表示 Show the top Route in a SinglePaneScene 85
{ override fun calculateScene(entries: List<RouteEntry>): Scene? { if (!isWide) return null val detail = entries.last() .takeIf { it.route is Route.MemoDetail } ?: return null val list = entries .lastOrNull { it.route is Route.MemoList } ?: return null return TwoPaneScene( list = list, detail = detail, ) } } 一番上が MemoDetail で back stack に MemoList があるときだけ TwoPaneScene で表示 TwoPaneScene only when the top is MemoDetail and MemoList is in the stack 87
} Nav3 @Immutable public interface Scene<T : Any> { public val key: Any public val entries: List<NavEntry<T>> public val previousEntries: List<NavEntry<T>> public val content: @Composable () -> Unit public val metadata: Map<String, Any> get() = entries.lastOrNull()?.metadata ?: emptyMap() } 93
polymorphism)なら serializer<Route>() が自動生成される sealed (closed polymorphism) auto-generates serializer<Route>() • open polymorphism では「Route のサブタイプ一覧」をコンパイル時に知る方法がない With open polymorphism, no compile-time list of Route subtypes exists → KSerializer を自作する( = Nav3 の NavKeySerializer) → Hand-write a KSerializer (= Nav3's NavKeySerializer) or → SerializersModule に全サブタイプを登録(:app に登録リストを集める配線 = KMP 版 Nav3 の方法) → Register every subtype in a SerializersModule (collected in :app — how KMP Nav3 works) 100
in :core:navigation • :core:navigation から :feature:memo モジュールの Route は見えない :core:navigation can't see :feature:memo's Routes • RouteEntry に Two pane の役割(List/Detail)を申告させる仕組みが必要 RouteEntry needs a way to declare its two-pane role (List/Detail) 101
onRemoved: (Route) -> Unit, ) { fun goTo(route: Route) { backStack.add(route) } fun back() { val popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { onRemoved(popped) } } } pop 時の掃除ができるように So cleanup can run on pop 109
val navigator = remember(backStack) { Navigator(backStack) { removed -> viewModelStoreProvider.clearKey(removed) stateHolder.removeState(removed.stateKey) } } pop 時に ViewModelStore のクリアと 保存されている状態の削除を行う On pop, clear the ViewModelStore and remove the saved state 110
MemoListRoute : MemoRoute @Serializable data class MemoDetailRoute(val id: Long) : MemoRoute @Serializable data class MemoEditRoute(val id: Long?) : MemoRoute 111
Map<String, Any> = emptyMap(), // v9 val content: @Composable () -> Unit, // v8 ) private val Route.stateKey: String get() = toString() // v6 Nav3 public class NavEntry<T : Any>( private val key: T, public val contentKey: Any = defaultContentKey(key), public val metadata: Map<String, Any> = emptyMap(), private val content: @Composable (T) -> Unit, ) { … } // defaultContentKey(key) // <= 1.1.5: = key.toString() // 1.1.6+: = Pair("$key", "${key::class}") 4つのフィールドすべてに自作の 対応物がある All four elds map to something we built ourselves fi 122
Our DIY relied on two simpli cations: • 「遷移は一瞬で終わる」「back stack の変更は必ず pop() を通る」 "transitions are instant" and "every change goes through pop()" • 実アプリで踏む3つの問題 Three problems real apps hit: • 遷移アニメーション ̶ 前後2画面が同時に composition に存在する瞬間 Transition animations — both screens in the composition at once • Predictive back ̶ ジェスチャで「途中まで戻る」 Predictive back — going "partway back" with a gesture • pop() を通らない back stack 変更 ̶ removeAt、一括置換 … Back stack changes that bypass pop() — removeAt, bulk replacement ... → 全部 Nav3 が部品で吸収している fi → Nav3's components absorb all of these 125
すごいシンプルな遷移だけなら自作も可能(AI もあるし) Very simple navigation? DIY is viable (and AI can help) • アニメーションや Predictive Back に対応するのは大変 Animations and predictive back are a lot of work • Nav2 で困っていないなら、急いで移行しなくていい If Nav2 isn't hurting you, there's no rush to migrate • back stack を自分で持てる Nav3 の強みが欲しいなら移行すべき If you want Nav3's "You own the back stack" strength, migrate • Nav3 を使うなら、Decorator と Scene の役割だけは理解してから If you adopt Nav3, rst understand Decorators and Scenes fi 128