Share
Embed
Nav2、Nav3 ... はたまた自作?〜 作って理解する Nav3 の設計意図 〜 / Nav2, Nav3 ... or Build Your Own? — Understanding Nav3's design intent by building it from scratch
DroidKaigi 2026
https://2026.droidkaigi.jp/timetable/1235689/
Yuki Anzai
September 03, 2026
Transcript
129 slides
-
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 -
あんざいゆき / Yuki Anzai • Android Engineer • Google Developer
Expert for Android • X (twitter) : @yanzm(やんざむ) • blog : y-anz-m.blogspot.com • 株式会社ウフィカ(uPhyca Inc.) 代表取締役 • 札幌・関東 2拠点生活 -
Nav3 コンポーネント多くない? Aren't there a lot of components? NavDisplay /
NavEntry / NavKey Scene / SceneStrategy NavEntryDecorator / entryProvider ... 5 -
自分で作ったら Nav3 の意図がわかるかも… If we build it ourselves, maybe we'll
understand Nav3's intent... 7 -
これから踏む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 -
Route sealed interface Route { data object MemoList : Route
data class MemoDetail(val id: Long) : Route data class MemoEdit(val id: Long?) : Route } 11 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
v5 手書き Saver を部品に置き換える Swap the hand-written Saver for the
built-in part 29 問題 3/7 つづき -
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 -
自作 @Serializable sealed interface Route { @Serializable data object MemoList
: Route … } package androidx.navigation3.runtime public interface NavKey Nav3 @Serializable data object MemoList : NavKey 32 -
自作 val backStack: SnapshotStateList<Route> = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), )
{ mutableStateListOf<Route>(Route.MemoList) } Nav3 val backStack: NavBackStack<NavKey> = rememberNavBackStack(MemoList) 33 -
自作 val backStack: SnapshotStateList<Route> = rememberSerializable( serializer = SnapshotStateListSerializer(serializer<Route>()), )
{ mutableStateListOf<Route>(Route.MemoList) } Nav3 val backStack: NavBackStack<NavKey> = rememberNavBackStack(MemoList) 34 -
自作 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 -
自作 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 -
自作 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 -
ここまでのまとめ / Recap so far 自作 / DIY Nav3 sealed
interface Route interface NavKey mutableStateListOf() NavBackStack (wraps SnapshotStateList, uses mutableStateListOf() internally) rememberSerializable() rememberNavBackStack() (uses rememberSerializable() internally) 38 -
v5 @Composable fun MemoListScreen( onMemoClick: (Long) -> Unit, onAddClick: ()
-> Unit, modifier: Modifier = Modifier, ) { var query by rememberSaveable { mutableStateOf("") } … 39 -
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 -
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 -
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 -
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 } -
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 } -
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 } -
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 } -
自作 val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() Nav3 @Composable public fun
<T : Any> rememberSaveableStateHolderNavEntryDecorator( saveableStateHolder: SaveableStateHolder = rememberSaveableStateHolder() ): SaveableStateHolderNavEntryDecorator<T> = remember(saveableStateHolder) { SaveableStateHolderNavEntryDecorator(saveableStateHolder) } 49 -
自作 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 -
自作 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 -
自作 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 -
Nav3 @Composable public fun <T : Any> NavDisplay( backStack: List<T>,
…, entryDecorators: List<NavEntryDecorator<T>> = listOf(rememberSaveableStateHolderNavEntryDecorator()), … ) { … } 53 -
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 -
@Composable fun MemoDetailScreen( id: Long, onEditClick: () -> Unit, modifier:
Modifier = Modifier, ) { val viewModel = viewModel { MemoDetailViewModel(id) } この ViewModel どこにスコープされる? What is this ViewModel scoped to? 56 -
@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 -
@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 -
@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 -
@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 -
@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 -
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 -
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 -
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 -
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 -
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 -
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 -
自作 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 -
自作 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 -
自作 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 ) { -
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 -
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 -
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 -
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 -
v8 class RouteEntry( val route: Route, val content: @Composable ()
-> Unit, ) Route と Composable の入れ物を用意 A container pairing a Route with its composable 79 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
自作 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 -
自作 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 -
自作 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 -
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 -
マルチモジュール構成 :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 -
Route を sealed にできない Route can't be sealed anymore ❌
:core:navigation sealed interface Route :feature:setting @Serializable data object SettingRoute : Route 99 interface Route -
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 -
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 -
:core:navigation v9 interface Route class RouteEntry( val route: Route, val
metadata: Map<String, Any> = emptyMap(), val content: @Composable () -> Unit, ) 103 -
:core:navigation v9 interface Scene … class SinglePaneScene … class TwoPaneScene
… interface SceneStrategy … class SinglePaneSceneStrategy … 104 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 -
: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 ), -
: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()) { -
: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()) { -
: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 -
自作 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 -
自作 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 -
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 -
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 -
で、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