Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Nav2、Nav3 ... はたまた自作?〜 作って理解する Nav3 の設計意図 〜 / N...

Avatar for Yuki Anzai Yuki Anzai
September 03, 2026

Nav2、Nav3 ... はたまた自作?〜 作って理解する Nav3 の設計意図 〜 / Nav2, Nav3 ... or Build Your Own? — Understanding Nav3's design intent by building it from scratch

Avatar for Yuki Anzai

Yuki Anzai

September 03, 2026

More Decks by Yuki Anzai

Other Decks in Technology

Transcript

  1. Nav2、Nav3 ... はたまた自作? 〜 作って理解する Nav3 の設計意図 〜 Nav2, Nav3

    ... or Build Your Own? — Understanding Nav3's design intent by building it from scratch あんざいゆき / Yuki Anzai (@yanzm) DroidKaigi 2026 1
  2. あんざいゆき / Yuki Anzai • Android Engineer • Google Developer

    Expert for Android • X (twitter) : @yanzm(やんざむ) • blog : y-anz-m.blogspot.com • 株式会社ウフィカ(uPhyca Inc.) 代表取締役 • 札幌・関東 2拠点生活
  3. Nav3 コンポーネント多くない? Aren't there a lot of components? NavDisplay /

    NavEntry / NavKey Scene / SceneStrategy NavEntryDecorator / entryProvider ... 5
  4. これから踏む7つの問題 The seven problems we'll hit 1. 前の画面に戻りたい Go back

    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
  5. Route sealed interface Route { data object MemoList : Route

    data class MemoDetail(val id: Long) : Route data class MemoEdit(val id: Long?) : Route } 11
  6. v1 var route by remember { mutableStateOf<Route>(Route.MemoList) } when (val

    r = route) { Route.MemoList -> MemoListScreen( onMemoClick = { id -> route = Route.MemoDetail(id) }, onAddClick = { route = Route.MemoEdit(null) }, ) is Route.MemoDetail -> MemoDetailScreen( id = r.id, onEditClick = { route = Route.MemoEdit(r.id) }, ) is Route.MemoEdit -> MemoEditScreen( id = r.id, onSaved = { route = Route.MemoList }, ) } 12
  7. v1 var route by remember { mutableStateOf<Route>(Route.MemoList) } when (val

    r = route) { Route.MemoList -> MemoListScreen( onMemoClick = { id -> route = Route.MemoDetail(id) }, onAddClick = { route = Route.MemoEdit(null) }, back で前の画面に戻れない • ) • メモを保存したときに前の画面に戻れない is Route.MemoDetail -> MemoDetailScreen( id = r.id, → 画面遷移の履歴を管理していないから前の画面がわ onEditClick = { route = Route.MemoEdit(r.id) }, ) からない } is Route.MemoEdit -> MemoEditScreen( id = r.id, → backstack で「画面遷移の履歴」を保持すればいい onSaved = { route = Route.MemoList }, ) Back doesn't work — we don't track navigation history, so we don't know the previous screen → keep the history as a back stack 13
  8. v2 val backStack = remember { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() } when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, onAddClick = { backStack.add(Route.MemoEdit(null)) }, ) is Route.MemoDetail -> MemoDetailScreen( id = route.id, onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, ) is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { backStack.removeLastOrNull() }, ) } 15 前のバージョンからの差分 changed since prev ver
  9. v2 val backStack = remember { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() Route の履歴を SnapshotStateList (変更された } ときに recompose が走る List )として保持 when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( Keep the Route history as a SnapshotStateList (a list that onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, triggers recomposition on change) onAddClick = { backStack.add(Route.MemoEdit(null)) }, ) MemoList is Route.MemoDetail -> MemoDetailScreen( id = route.id, onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, SnapshotStateList ) is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { backStack.removeLastOrNull() }, ) } 16
  10. v2 val backStack = remember { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() } when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, onAddClick = { backStack.add(Route.MemoEdit(null)) }, backStack (= Route のリスト)の一番最後の MemoList ) Route(= 現在の画面)を表示 is Route.MemoDetail -> MemoDetailScreen( last() id = route.id, Show the last Route in the backStack (= the current onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, screen) ) is Route.MemoEdit -> MemoEditScreen( id =MemoList route.id, onSaved = { backStack.removeLastOrNull() }, ) } 17
  11. v2 val backStack = remember { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() } when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, onAddClick = { backStack.add(Route.MemoEdit(null)) }, ) backStack に Route を追加 is Route.MemoDetail -> MemoDetailScreen( → recompose last() id = route.id, → 追加した Route が last に onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, なるので画面が切り替わる ) MemoDetail (1) add(MemoDetail(1)) MemoDetail (1) add() → recompose → the new is Route.MemoEdit -> MemoEditScreen( Route is now last, so the screen id = route.id, onSaved = { backStack.removeLastOrNull() }, switches MemoList MemoList ) } 18
  12. v2 val backStack = remember { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() } when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( backStack に前の画面がある MemoList onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, ときは、backStack の最後の onAddClick = { backStack.add(Route.MemoEdit(null)) }, ) エントリーを削除することで前 last() の画面に戻れる is Route.MemoDetail -> MemoDetailScreen( id = route.id, removeLastOrNull() MemoDetail (1) Removing the last entry navigates onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, back (when there is a previous ) screen) MemoList MemoList is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { backStack.removeLastOrNull() }, ) } 19
  13. v2 val backStack = remember { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() } when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, onAddClick = { backStack.add(Route.MemoEdit(null)) }, 画面回転したときに MemoList に戻ってしまう ) is Route.MemoDetail -> MemoDetailScreen( → rememberSaveable で保存すればよさそう id = route.id, onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, Rotation resets us to MemoList → let's persist it with ) rememberSaveable is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { backStack.removeLastOrNull() }, ) } 20
  14. v3 val backStack = rememberSaveable { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() } when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, onAddClick = { backStack.add(Route.MemoEdit(null)) }, ) is Route.MemoDetail -> MemoDetailScreen( id = route.id, onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, ) is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { backStack.removeLastOrNull() }, ) } 22
  15. v3 val backStack = rememberSaveable { mutableStateListOf<Route>(Route.MemoList) } BackHandler(enabled =

    backStack.size > 1) { backStack.removeLastOrNull() } when (val route = backStack.last()) { is Route.MemoList -> MemoListScreen( ホームボタンを押すとクラッシュ onMemoClick = { id -> backStack.add(Route.MemoDetail(id)) }, onAddClick = { backStack.add(Route.MemoEdit(null)) }, java.lang.IllegalArgumentException: ) Parcel: unknown type for value MemoList is Route.MemoDetail -> MemoDetailScreen( → 保存できるのは Bundle に入るものだけ id = route.id, onEditClick = { backStack.add(Route.MemoEdit(route.id)) }, → )Json 文字列化して保存 Pressing Home crashes — -> onlyMemoEditScreen( Bundle-compatible values can is Route.MemoEdit be savedid →= serialize Routes to JSON strings route.id, onSaved = { backStack.removeLastOrNull() }, ) } 23
  16. Route + Kotlin Serialization import kotlinx.serialization.Serializable @Serializable sealed interface Route

    { @Serializable data object MemoList : Route Kotlin Serialization を使って @Serializable data class MemoDetail(val id: Long) : Route @Serializable data class MemoEdit(val id: Long?) : Route } 25 Route を Json 文字列化 Encode Routes as JSON strings with Kotlin Serialization v4
  17. v4 val RouteListSaver = listSaver<SnapshotStateList<Route>, String>( save = { backStack

    -> backStack.map { Json.encodeToString<Route>(it) } }, restore = { saved -> saved.map { Json.decodeFromString<Route>(it) }.toMutableStateList() }, ) val backStack = rememberSaveable(saver = RouteListSaver) { mutableStateListOf<Route>(Route.MemoList) } Json 文字列化して保存する Saver を用意し、 rememberSaveable でそれを使うようにする Write a Saver that stores the list as JSON strings and pass it to rememberSaveable 26
  18. v4 val RouteListSaver = listSaver<SnapshotStateList<Route>, String>( save = { backStack

    -> backStack.map { Json.encodeToString<Route>(it) } }, restore = { saved -> saved.map { Json.decodeFromString<Route>(it) }.toMutableStateList() }, ) val backStack = rememberSaveable(saver = RouteListSaver) { mutableStateListOf<Route>(Route.MemoList) } Json 文字列化して保存する Saver を用意し、 rememberSaveable でそれを使うようにする Write a Saver that stores the list as JSON strings and pass it to rememberSaveable
  19. v4 val RouteListSaver = listSaver<SnapshotStateList<Route>, String>( save = { backStack

    -> backStack.map { Json.encodeToString<Route>(it) } }, restore = { saved -> saved.map { Json.decodeFromString<Route>(it) }.toMutableStateList() }, ) val backStack = rememberSaveable(saver = RouteListSaver) { mutableStateListOf<Route>(Route.MemoList) } 自作の Saver の処理は KSerializer にそのまま 委譲できる The hand-written Saver logic can be delegated to a KSerializer 28
  20. v5 val backStack = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), ) {

    mutableStateListOf<Route>(Route.MemoList) } @Composable public fun <T : Any> rememberSerializable( serializer: KSerializer<T>, … ): T { val saver = serializableSaver(serializer, configuration) @Suppress("DEPRECATION") return rememberSaveable(*inputs, saver = saver, key = null, init = init) } rememberSerializable = rememberSaveable の KSerializer 版 the KSerializer avor of rememberSaveable fl 30
  21. 自作 @Serializable sealed interface Route { @Serializable data object MemoList

    : Route … } package androidx.navigation3.runtime public interface NavKey Nav3 @Serializable data object MemoList : NavKey 32
  22. 自作 val backStack: SnapshotStateList<Route> = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), )

    { mutableStateListOf<Route>(Route.MemoList) } Nav3 val backStack: NavBackStack<NavKey> = rememberNavBackStack(MemoList) 33
  23. 自作 val backStack: SnapshotStateList<Route> = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), )

    { mutableStateListOf<Route>(Route.MemoList) } Nav3 val backStack: NavBackStack<NavKey> = rememberNavBackStack(MemoList) 34
  24. 自作 val backStack: SnapshotStateList<Route> = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), )

    { 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
  25. 自作 val backStack: SnapshotStateList<Route> = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), )

    { mutableStateListOf<Route>(Route.MemoList) } Nav3 val backStack: NavBackStack<NavKey> = rememberNavBackStack(MemoList) @Composable public fun rememberNavBackStack(vararg elements: NavKey): NavBackStack<NavKey> { return rememberSerializable( serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()) ) { NavBackStack(*elements) } A wrapper around SnapshotStateListSerializer } 36
  26. 自作 val backStack: SnapshotStateList<Route> = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), )

    { mutableStateListOf<Route>(Route.MemoList) } Nav3 val backStack: NavBackStack<NavKey> = rememberNavBackStack(MemoList) @Composable public fun rememberNavBackStack(vararg elements: NavKey): NavBackStack<NavKey> { return rememberSerializable( serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer()) ) { NavBackStack(*elements) } } 37
  27. ここまでのまとめ / Recap so far 自作 / DIY Nav3 sealed

    interface Route interface NavKey mutableStateListOf() NavBackStack (wraps SnapshotStateList, uses mutableStateListOf() internally) rememberSerializable() rememberNavBackStack() (uses rememberSerializable() internally) 38
  28. v5 @Composable fun MemoListScreen( onMemoClick: (Long) -> Unit, onAddClick: ()

    -> Unit, modifier: Modifier = Modifier, ) { var query by rememberSaveable { mutableStateOf("") } … 39
  29. v5 @Composable fun MemoListScreen( onMemoClick: (Long) -> Unit, onAddClick: ()

    -> 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
  30. v5 when (val route = backStack.last()) { is Route.MemoList ->

    MemoListScreen( … ❌ ) is Route.MemoDetail -> MemoDetailScreen( … ) is Route.MemoEdit -> MemoEditScreen( backStack.last() が MemoDetail になる … ) } → MemoListScreen() を呼び出すパスは通らなくなる = MemoListScreen が composition から完全に消える → rememberSaveable の値も一緒に消える backStack.last() becomes MemoDetail → the code path that calls MemoListScreen() no longer runs = MemoListScreen leaves the composition entirely → its rememberSaveable values are discarded with it 41
  31. v6 private val Route.stateKey: String get() = toString() 各画面の状態を保存するための key(Bundle

    に 保存される)を定義 De ne a key for saving each screen's state (the key itself is stored in the Bundle) fi 43
  32. v6 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() fun pop() { val

    popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stateHolder.removeState(popped.stateKey) } } BackHandler(enabled = backStack.size > 1) { pop() } val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { when (route) { is Route.MemoList -> MemoListScreen(…) is Route.MemoDetail -> MemoDetailScreen(…) is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { pop() }, ) 44 }
  33. v6 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() fun pop() { val

    popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stateHolder.removeState(popped.stateKey) } } BackHandler(enabled = backStack.size > 1) { pop() } val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { when (route) { is Route.MemoList -> MemoListScreen(…) is Route.MemoDetail -> MemoDetailScreen(…) is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { pop() }, ) 45 }
  34. v6 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() fun pop() { val

    popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stateHolder.removeState(popped.stateKey) } } BackHandler(enabled = backStack.size > 1) { pop() } val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { when (route) { is Route.MemoList -> MemoListScreen(…) is Route.MemoDetail -> MemoDetailScreen(…) is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { pop() }, ) 46 }
  35. v6 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() fun pop() { val

    popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stateHolder.removeState(popped.stateKey) } } BackHandler(enabled = backStack.size > 1) { pop() } val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { when (route) { is Route.MemoList -> MemoListScreen(…) is Route.MemoDetail -> MemoDetailScreen(…) is Route.MemoEdit -> MemoEditScreen( id = route.id, onSaved = { pop() }, ) 47 }
  36. 自作 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() Nav3 @Composable public fun

    <T : Any> rememberSaveableStateHolderNavEntryDecorator( saveableStateHolder: SaveableStateHolder = rememberSaveableStateHolder() ): SaveableStateHolderNavEntryDecorator<T> = remember(saveableStateHolder) { SaveableStateHolderNavEntryDecorator(saveableStateHolder) } 49
  37. 自作 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() @Immutable public open class

    NavEntryDecorator<T : Any>( internal val onPop: (key: Any) -> Unit = {}, internal val decorate: @Composable (entry: NavEntry<T>) -> Unit, ) Nav3 public class SaveableStateHolderNavEntryDecorator<T : Any>( saveableStateHolder: SaveableStateHolder ) : NavEntryDecorator<T>( onPop = { contentKey -> saveableStateHolder.removeState(contentKey) }, decorate = { entry -> saveableStateHolder.SaveableStateProvider(entry.contentKey) { entry.Content() } }, ) 50
  38. 自作 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() fun pop() { val

    popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stateHolder.removeState(popped.stateKey) } } Nav3 public class SaveableStateHolderNavEntryDecorator<T : Any>( saveableStateHolder: SaveableStateHolder ) : NavEntryDecorator<T>( onPop = { contentKey -> saveableStateHolder.removeState(contentKey) }, decorate = { entry -> saveableStateHolder.SaveableStateProvider(entry.contentKey) { entry.Content() } }, ) 51
  39. 自作 val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { when

    (route) { … } } Nav3 public class SaveableStateHolderNavEntryDecorator<T : Any>( saveableStateHolder: SaveableStateHolder ) : NavEntryDecorator<T>( onPop = { contentKey -> saveableStateHolder.removeState(contentKey) }, decorate = { entry -> saveableStateHolder.SaveableStateProvider(entry.contentKey) { entry.Content() } }, ) 52
  40. Nav3 @Composable public fun <T : Any> NavDisplay( backStack: List<T>,

    …, entryDecorators: List<NavEntryDecorator<T>> = listOf(rememberSaveableStateHolderNavEntryDecorator()), … ) { … } 53
  41. class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) setContent { MyTheme { MyApp() } } } } @Composable fun MyApp() { … stateHolder.SaveableStateProvider(key = route.stateKey) { when (route) { is Route.MemoList -> MemoListScreen(…) is Route.MemoDetail -> MemoDetailScreen(…) is Route.MemoEdit -> MemoEditScreen(…) } } } 55
  42. @Composable fun MemoDetailScreen( id: Long, onEditClick: () -> Unit, modifier:

    Modifier = Modifier, ) { val viewModel = viewModel { MemoDetailViewModel(id) } この ViewModel どこにスコープされる? What is this ViewModel scoped to? 56
  43. @Composable fun MemoDetailScreen( id: Long, onEditClick: () -> Unit, modifier:

    Modifier = Modifier, ) { val viewModel = viewModel { MemoDetailViewModel(id) } この ViewModel どこにスコープされる? → MainActivity にスコープされてしまう What is this ViewModel scoped to? → It ends up scoped to MainActivity 57
  44. @Composable fun MemoDetailScreen( id: Long, onEditClick: () -> Unit, modifier:

    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
  45. @Composable public fun <VM : ViewModel> viewModel( modelClass: KClass<VM>, viewModelStoreOwner:

    ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { … }, … ): VM { … val provider = ViewModelProvider.create(viewModelStoreOwner, …) return if (key != null) { provider[key, modelClass] } else { provider[modelClass] } } 59
  46. @Composable public fun <VM : ViewModel> viewModel( modelClass: KClass<VM>, viewModelStoreOwner:

    ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { … }, … ): VM { … val provider = ViewModelProvider.create(viewModelStoreOwner, …) LocalViewModelStoreOwner を上書きしていない return if (key != null) { provider[key, modelClass]場合、親の Activity が ViewModelStoreOwner } else { として返る provider[modelClass] } } Without an override, LocalViewModelStoreOwner falls back to the parent Activity 60
  47. @Composable public fun <VM : ViewModel> viewModel( modelClass: KClass<VM>, viewModelStoreOwner:

    ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { … }, … ): VM { … val provider = ViewModelProvider.create(viewModelStoreOwner, …) LocalViewModelStoreOwner を上書きしていない return if (key != null) { provider[key, modelClass]場合、親の Activity が ViewModelStoreOwner } else { として返る provider[modelClass] } } → back stack のライフサイクルで LocalViewModelStoreOwner を上書きすればよい → Override LocalViewModelStoreOwner with a back-stackscoped owner 61
  48. v7 private class EntryViewModelStores : ViewModel() { private val stores

    = mutableMapOf<Route, ViewModelStore>() fun storeFor(route: Route): ViewModelStore { return stores.getOrPut(route) { ViewModelStore() } } fun clear(route: Route) { stores.remove(route)?.clear() } back stack の Route ごとに ViewModelStore を持つ override fun onCleared() { stores.values.forEach { it.clear() } stores.clear() } } 63 ViewModel A ViewModel holding one ViewModelStore per Route in the back stack
  49. val stores: EntryViewModelStores = viewModel { EntryViewModelStores() } … fun

    pop() { val popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stores.clear(popped) stateHolder.removeState(popped.stateKey) } } v7 Route ごとの ViewModelStore 置き場は Activity スコープで保持 The per-Route store holder itself is Activity-scoped … val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { val owner = remember(route) { EntryViewModelStoreOwner(stores.storeFor(route)) } CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { when (route) { … } } } 64
  50. val stores: EntryViewModelStores = viewModel { EntryViewModelStores() } … v7

    fun pop() { val popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stores.clear(popped) stateHolder.removeState(popped.stateKey) } back stack から消えたら対応す } る ViewModelStore をクリア … val route = backStack.last() Clear the ViewModelStore when its Route leaves the back stack stateHolder.SaveableStateProvider(key = route.stateKey) { val owner = remember(route) { EntryViewModelStoreOwner(stores.storeFor(route)) } CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { when (route) { … } } } 65
  51. val stores: EntryViewModelStores = viewModel { EntryViewModelStores() } … v7

    fun pop() { val popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stores.clear(popped) stateHolder.removeState(popped.stateKey) private class EntryViewModelStoreOwner( } override val viewModelStore: ViewModelStore, } ) : ViewModelStoreOwner … val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { val owner = remember(route) { EntryViewModelStoreOwner(stores.storeFor(route)) } CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { when (route) { … Route に対応する ViewModelStoreOwner セット } } Provide the ViewModelStoreOwner for the current Route } 66
  52. val stores: EntryViewModelStores = viewModel { EntryViewModelStores() } … v7

    fun pop() { val popped = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { stores.clear(popped) stateHolder.removeState(popped.stateKey) } } … val route = backStack.last() stateHolder.SaveableStateProvider(key = route.stateKey) { val owner = remember(route) { EntryViewModelStoreOwner(stores.storeFor(route)) } CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { when (route) { … } } } 67
  53. val viewModelStoreProvider = rememberViewModelStoreProvider() … fun pop() { val popped

    = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { viewModelStoreProvider.clearKey(popped) stateHolder.removeState(popped.stateKey) } 自作した EntryViewModelStores・owner・clear を } lifecycle-viewmodel 2.11 の公式 API に置き換え … val route = backStack.last() Our hand-rolled EntryViewModelStores, owner, and clear — replaced with o cial lifecycle-viewmodel 2.11 APIs ffi stateHolder.SaveableStateProvider(key = route.stateKey) { val owner = rememberViewModelStoreOwner(route, viewModelStoreProvider) CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { when (route) { … } } lifecycle 2.11.0 } 68 v7
  54. 自作 val viewModelStoreProvider = rememberViewModelStoreProvider() fun pop() { val popped

    = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { viewModelStoreProvider.clearKey(popped) stateHolder.removeState(popped.stateKey) } } Nav3 public class ViewModelStoreNavEntryDecorator<T : Any> public constructor(viewModelStoreProvider: ViewModelStoreProvider) : NavEntryDecorator<T>( onPop = { key -> viewModelStoreProvider.clearKey(key) }, decorate = { entry -> … }, ) { } 70
  55. 自作 val viewModelStoreProvider = rememberViewModelStoreProvider() fun pop() { val popped

    = backStack.removeLastOrNull() if (popped != null && popped !in backStack) { viewModelStoreProvider.clearKey(popped) stateHolder.removeState(popped.stateKey) } } Nav3 public class ViewModelStoreNavEntryDecorator<T : Any> public constructor(viewModelStoreProvider: ViewModelStoreProvider) : NavEntryDecorator<T>( onPop = { key -> viewModelStoreProvider.clearKey(key) }, decorate = { entry -> … }, ) { } 71
  56. 自作 stateHolder.SaveableStateProvider(key = route.stateKey) { val owner = rememberViewModelStoreOwner(route, viewModelStoreProvider)

    CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { when (route) { … } } } Nav3 public class ViewModelStoreNavEntryDecorator<T : Any> public constructor(viewModelStoreProvider: ViewModelStoreProvider) : NavEntryDecorator<T>( onPop = { … }, decorate = { entry -> val owner = rememberViewModelStoreOwner( entry.contentKey, viewModelStoreProvider, savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, ) CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { entry.Content() } }, 72 ) {
  57. Nav3 @Composable public fun <T : Any> rememberViewModelStoreNavEntryDecorator( viewModelStoreOwner: ViewModelStoreOwner

    = … ): ViewModelStoreNavEntryDecorator<T> { val viewModelStoreProvider = rememberViewModelStoreProvider(parent = viewModelStoreOwner) return rememberViewModelStoreNavEntryDecorator(viewModelStoreProvider) } Nav3 で NavDisplay( … entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator(), ), … ) 73 ViewModel を使う 場合は必ず追加 Must be added whenever you use ViewModels with Nav3
  58. back stack のどの要素を表示するか back stack Which back stack elements to

    show w < 600dp w >= 600dp [List] [List] [List] [List, Detail] [Detail] [List, Detail] [List, Detail, Edit] [Edit] [Edit] 75
  59. back stack のどの要素を表示するか back stack Which back stack elements to

    show Default: show only last() w >= 600dp [List] [List] null [List, Detail] [Detail] [List, Detail] [List, Detail, Edit] [Edit] null
  60. back stack のどの要素を表示するか back stack Which back stack elements to

    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
  61. v8 class RouteEntry( val route: Route, val content: @Composable ()

    -> Unit, ) Route と Composable の入れ物を用意 A container pairing a Route with its composable 79
  62. v8 class RouteEntry( val route: Route, Route に応じた画面の出し分けを lambda 化

    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
  63. v8 @Composable private fun EntryContent( route: Route, viewModelStoreProvider: ViewModelStoreProvider, stateHolder:

    SaveableStateHolder, content: @Composable () -> Unit, ) { stateHolder.SaveableStateProvider(key = route.stateKey) { val owner = rememberViewModelStoreOwner(route, viewModelStoreProvider) CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { content() } } } 状態保存と ViewModel の対応部分を wrapper とし て Composable 化 Extract the state-saving + ViewModel wiring into a wrapper composable 81
  64. v8 val entryProvider: (Route) -> RouteEntry = { route ->

    backStack のままでは key が変 when (route) { 化しない … } toList() は内容が変わったとき } だけ別インスタンス val entries = remember(backStack.toList()) { backStack never changes instance — backStack.map { route -> toList() does, exactly when the content RouteEntry(route) { changes EntryContent(route, viewModelStoreProvider, stateHolder) { entryProvider(route).content() } } } 状態保存、ViewModel 対応の Composable } で wrap Wrap with the state-saving / ViewModel composable 82
  65. v8 interface Scene { val entries: List<RouteEntry> @Composable fun Content()

    } class SinglePaneScene( private val entry: RouteEntry ) : Scene { override val entries: List<RouteEntry> get() = listOf(entry) @Composable override fun Content() { entry.content() } } 83
  66. v8 class TwoPaneScene( private val list: RouteEntry, private val detail:

    RouteEntry, ) : Scene { override val entries: List<RouteEntry> get() = listOf(list, detail) @Composable override fun Content() { Row(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.weight(1f)) { list.content() } Box(modifier = Modifier.weight(1f)) { detail.content() } } } } 左右に半分ずつ表示 Side by side, half width each 84
  67. v8 interface SceneStrategy { fun calculateScene(entries: List<RouteEntry>): Scene? } class

    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
  68. v8 class TwoPaneSceneStrategy( private val isWide: Boolean ) : SceneStrategy

    { 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, ) } } 86
  69. v8 class TwoPaneSceneStrategy( private val isWide: Boolean ) : SceneStrategy

    { 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
  70. v8 class TwoPaneSceneStrategy( private val isWide: Boolean ) : SceneStrategy

    { 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, ) } } Nav3 では NavEntry の metadata を使って 汎用化 Nav3 generalizes this with NavEntry metadata 88
  71. v8 val entries = remember(backStack.toList()) { backStack.map { route ->

    RouteEntry(route) { 横幅が 600dp 以上なら wide というロジックで EntryContent(route, viewModelStoreProvider, stateHolder) { TwoPaneSceneStrategy を生成 entryProvider(route).content() } wide = width >= 600dp → create TwoPaneSceneStrategy } } } val containerWidthPx = LocalWindowInfo.current.containerSize.width val isWide = with(LocalDensity.current) { containerWidthPx.toDp() } >= 600.dp val twoPaneSceneStrategy = remember(isWide) { TwoPaneSceneStrategy(isWide) } val defaultStrategy = remember { SinglePaneSceneStrategy() } val scene = twoPaneSceneStrategy.calculateScene(entries) ?: defaultStrategy.calculateScene(entries) scene.Content() 89
  72. v8 val entries = remember(backStack.toList()) { backStack.map { route ->

    RouteEntry(route) { TwoPaneSceneStrategy が null を返すなら EntryContent(route, viewModelStoreProvider, stateHolder) { SinglePaneSceneStrategy にフォールバック entryProvider(route).content() } If it returns null, fall back to SinglePaneSceneStrategy } } } val containerWidthPx = LocalWindowInfo.current.containerSize.width val isWide = with(LocalDensity.current) { containerWidthPx.toDp() } >= 600.dp val twoPaneSceneStrategy = remember(isWide) { TwoPaneSceneStrategy(isWide) } val defaultStrategy = remember { SinglePaneSceneStrategy() } val scene = twoPaneSceneStrategy.calculateScene(entries) ?: defaultStrategy.calculateScene(entries) scene.Content() 90
  73. 91

  74. 自作 interface Scene { val entries: List<RouteEntry> @Composable fun Content()

    } 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
  75. 自作 interface SceneStrategy { fun calculateScene(entries: List<RouteEntry>): Scene? } Nav3

    @Immutable public fun interface SceneStrategy<T : Any> { public fun SceneStrategyScope<T>.calculateScene( entries: List<NavEntry<T>> ): Scene<T>? } 94
  76. 自作 class SinglePaneSceneStrategy : SceneStrategy { override fun calculateScene(entries: List<RouteEntry>):

    Scene { return SinglePaneScene(entries.last()) } } Nav3 public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene( entries: List<NavEntry<T>> ): Scene<T> { return SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1), ) } } 95
  77. Nav3 @Composable public fun <T : Any> NavDisplay( backStack: List<T>,

    … sceneStrategies: List<SceneStrategy<T>> = …, … ) { … } internal fun <T : Any> calculateSceneWithSinglePaneFallback( sceneStrategies: List<SceneStrategy<T>>, scope: SceneStrategyScope<T>, entries: List<NavEntry<T>>, ): Scene<T> { var scene: Scene<T>? = null for (index in sceneStrategies.indices) { scene = with(sceneStrategies[index]) { scope.calculateScene(entries) } if (scene != null) break } return scene ?: with(SinglePaneSceneStrategy<T>()) { scope.calculateScene(entries) } } 96
  78. マルチモジュール構成 :app Multi-module structure • :app • :core:navigation :feature:memo :feature:setting

    List / Detail / Edit Setting • Route, … • :feature:memo :core:navigation Route / RouteEntry / Scene/ Navigator • List, Detail, Edit • :feature:setting • Setting 各画面に対応する Route はそれぞれのモジュールに定義したい Each screen's Route should live in its own module •… 98
  79. Route を sealed にできない Route can't be sealed anymore ❌

    :core:navigation sealed interface Route :feature:setting @Serializable data object SettingRoute : Route 99 interface Route
  80. Route の復元対応が必要 We need to restore Routes ourselves • sealed(closed

    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
  81. TwoPaneSceneStrategy の汎用化 Generalizing TwoPaneSceneStrategy • :core:navigation に定義したい We want it

    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
  82. :core:navigation v9 interface Route class RouteEntry( val route: Route, val

    metadata: Map<String, Any> = emptyMap(), val content: @Composable () -> Unit, ) 103
  83. :core:navigation v9 interface Scene … class SinglePaneScene … class TwoPaneScene

    … interface SceneStrategy … class SinglePaneSceneStrategy … 104
  84. :core:navigation v9 class TwoPaneSceneStrategy( private val isWide: Boolean ) :

    SceneStrategy { private enum class Role { List, Detail } companion object { private const val ROLE_KEY = "TwoPaneSceneStrategy.role" fun listPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.List) fun detailPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.Detail) } override fun calculateScene(entries: List<RouteEntry>): Scene? { if (!isWide) return null val detail = entries.last() .takeIf { it.metadata[ROLE_KEY] == Role.Detail } ?: return null val list = entries .lastOrNull { it.metadata[ROLE_KEY] == Role.List } ?: return null return TwoPaneScene(list, detail) } } 105
  85. :core:navigation v9 class TwoPaneSceneStrategy( private val isWide: Boolean ) :

    SceneStrategy { private enum class Role { List, Detail } companion object { private const val ROLE_KEY = "TwoPaneSceneStrategy.role" fun listPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.List) fun detailPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.Detail) } override fun calculateScene(entries: List<RouteEntry>): Scene? { if (!isWide) return null val detail = entries.last() .takeIf { it.metadata[ROLE_KEY] == Role.Detail } ?: return null val list = entries .lastOrNull { it.metadata[ROLE_KEY] == Role.List } ?: return null return TwoPaneScene(list, detail) } } 106
  86. :core:navigation v9 class TwoPaneSceneStrategy( private val isWide: Boolean ) :

    SceneStrategy { private enum class Role { List, Detail } companion object { private const val ROLE_KEY = "TwoPaneSceneStrategy.role" fun listPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.List) fun detailPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.Detail) } override fun calculateScene(entries: List<RouteEntry>): Scene? { if (!isWide) return null val detail = entries.last() .takeIf { it.metadata[ROLE_KEY] == Role.Detail } ?: return null val list = entries .lastOrNull { it.metadata[ROLE_KEY] == Role.List } ?: return null return TwoPaneScene(list, detail) } } 107
  87. :core:navigation v9 class TwoPaneSceneStrategy( private val isWide: Boolean ) :

    SceneStrategy { private enum class Role { List, Detail } companion object { private const val ROLE_KEY = "TwoPaneSceneStrategy.role" fun listPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.List) fun detailPane(): Map<String, Any> = mapOf(ROLE_KEY to Role.Detail) } override fun calculateScene(entries: List<RouteEntry>): Scene? { if (!isWide) return null val detail = entries.last() .takeIf { it.metadata[ROLE_KEY] == Role.Detail } ?: return null val list = entries .lastOrNull { it.metadata[ROLE_KEY] == Role.List } ?: return null return TwoPaneScene(list, detail) } } 108
  88. :core:navigation v9 class Navigator( private val backStack: SnapshotStateList<Route>, private val

    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
  89. :app v9 val viewModelStoreProvider = rememberViewModelStoreProvider() val stateHolder = rememberSaveableStateHolder()

    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
  90. :feature:memo v9 sealed interface MemoRoute : Route @Serializable data object

    MemoListRoute : MemoRoute @Serializable data class MemoDetailRoute(val id: Long) : MemoRoute @Serializable data class MemoEditRoute(val id: Long?) : MemoRoute 111
  91. :feature:memo v9 fun memoEntryProvider( navigator: Navigator, onSettingClick: () -> Unit,

    ): (Route) -> RouteEntry? = { route -> if (route is MemoRoute) { when (route) { is MemoListRoute -> RouteEntry(route, …) { MemoListScreen( onMemoClick = { id -> navigator.goTo(MemoDetailRoute(id)) }, onAddClick = { navigator.goTo(MemoEditRoute(null)) }, onSettingClick = onSettingClick, ) } is MemoDetailRoute -> … is MemoEditRoute -> … } } else { null } } 112
  92. :feature:memo v9 fun memoEntryProvider( navigator: Navigator, onSettingClick: () -> Unit,

    ): (Route) -> RouteEntry? = { route -> if (route is MemoRoute) { when (route) { is MemoListRoute -> RouteEntry(route, …) { MemoListScreen( onMemoClick = { id -> navigator.goTo(MemoDetailRoute(id)) }, onAddClick = { navigator.goTo(MemoEditRoute(null)) }, onSettingClick = onSettingClick, ) } is MemoDetailRoute -> … is MemoEditRoute -> … } } else { null } } 113
  93. :feature:memo v9 fun memoEntryProvider( navigator: Navigator, onSettingClick: () -> Unit,

    ): (Route) -> RouteEntry? = { route -> if (route is MemoRoute) { when (route) { is MemoListRoute -> RouteEntry( route, metadata = TwoPaneSceneStrategy.listPane() ) { … } is MemoDetailRoute -> RouteEntry( route, metadata = TwoPaneSceneStrategy.detailPane() ) { … } is MemoEditRoute -> … } } else { null } } 114
  94. :feature:memo v9 Navigator によってメモ画面間の fun memoEntryProvider( 遷移はモジュール内で完結できる navigator: Navigator, onSettingClick:

    () -> Unit, With the Navigator, memo-internal ): (Route) -> RouteEntry? = { route -> navigation stays inside the module if (route is MemoRoute) { when (route) { is MemoListRoute -> RouteEntry(route, …) { MemoListScreen( onMemoClick = { id -> navigator.goTo(MemoDetailRoute(id)) }, onAddClick = { navigator.goTo(MemoEditRoute(null)) }, onSettingClick = onSettingClick, ) } is MemoDetailRoute -> … is MemoEditRoute -> … } } else { null } } 115
  95. :feature:setting v9 @Serializable data object SettingRoute : Route fun settingEntryProvider(

    navigator: Navigator, ): (Route) -> RouteEntry? = { route -> if (route is SettingRoute) { RouteEntry(route) { SettingScreen( onBack = { navigator.back() } ) } } else { null } 前の画面に戻る遷移も Navigator で } Going back also goes through the Navigator 116
  96. :app v9 val backStack = rememberSerializable( serializer = SnapshotStateListSerializer(RouteSerializer), )

    { mutableStateListOf<Route>(MemoListRoute) } … val navigator = remember(backStack) { Navigator(backStack) { … } } 自作 KSerializer NavKeySerializer の Route 版 Our own KSerializer — the Route version of NavKeySerializer BackHandler(enabled = backStack.size > 1) { navigator.back() } val providers: List<(Route) -> RouteEntry?> = listOf( memoEntryProvider( navigator = navigator, onSettingClick = { navigator.goTo(SettingRoute) }, 117 ),
  97. :app v9 val backStack = … … val navigator =

    remember(backStack) { Navigator(backStack) { … } } BackHandler(enabled = backStack.size > 1) { navigator.back() } val providers: List<(Route) -> RouteEntry?> = listOf( memoEntryProvider( navigator = navigator, onSettingClick = { navigator.goTo(SettingRoute) }, ), settingEntryProvider( navigator = navigator, ), ) 118 val entries = remember(backStack.toList()) {
  98. :app v9 val backStack = … … val navigator =

    remember(backStack) { Navigator(backStack) { … } } 各 feature module の lambda を集約 BackHandler(enabled = backStack.size > 1) { navigator.back() } Aggregate each feature module's lambda val providers: List<(Route) -> RouteEntry?> = listOf( memoEntryProvider( navigator = navigator, onSettingClick = { navigator.goTo(SettingRoute) }, ), settingEntryProvider( navigator = navigator, ), ) 119 val entries = remember(backStack.toList()) {
  99. :app v9 val providers: List<(Route) -> RouteEntry?> = listOf( …

    ) val entries = remember(backStack.toList()) { backStack.map { route -> val entry = providers.firstNotNullOfOrNull { it(route) } ?: error("Unknown route: $route") RouteEntry(route, metadata = entry.metadata) { EntryContent(route, viewModelStoreProvider, stateHolder) { entry.content() } } } Route に対応する RouteEntry を取得 } Resolve the RouteEntry for each Route … scene.Content() 120
  100. 自作 class RouteEntry( val route: Route, // v8 val metadata:

    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
  101. 自作 Nav3 // :app — feature の lambda を並べて合成 //

    :app — feature の登録関数を並べるだけ val providers = listOf( memoEntryProvider(…), settingEntryProvider(…), ) NavDisplay( backStack = backStack, entryProvider = entryProvider { memoEntries(…) settingEntries(…) }, ) val entry = providers .firstNotNullOfOrNull { it(route) } ?: error("Unknown route: $route") // :feature:memo — 自分の画面群を登録 fun EntryProviderScope<NavKey>.memoEntries(…) { entry<MemoList> { MemoListScreen(…) } … } feature ごとに定義して :app で合成するのは自作と同じ形 Each feature registers its own screens and :app composes them — same shape as ours 123
  102. Nav3 が黙って面倒を見ていること What Nav3 quietly takes care of • ここまでの自作は、2つの単純化の上に成り立っていた

    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
  103. Nav3 全体マップ The Nav3 map 回転・プロセス死 NavKey / rememberNavBackStack マルチモジュール

    NavKeySerializer / NavEntry / entryProvider DSL 画面内の状態 SaveableStateHolderNavEntryDecorator ViewModel の scope ViewModelStoreNavEntryDecorator アニメ中の lifecycle 自動適用 Decorator rotation & process death multi-module in-screen state ViewModel scoping lifecycle during animations (SceneSetup / BackStackAwareLifecycle) multi-pane Scene / SceneStrategy 127
  104. で、Nav2、Nav3、はたまた自作? So — Nav2, Nav3, or build your own? •

    すごいシンプルな遷移だけなら自作も可能(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