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

Navigation3でやりとりした画面引数をSavedStateHandleから引き出せない理由

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for mikan mikan
July 22, 2026

 Navigation3でやりとりした画面引数をSavedStateHandleから引き出せない理由

Avatar for mikan

mikan

July 22, 2026

More Decks by mikan

Other Decks in Technology

Transcript

  1. 目次 Agenda に移行したら SavedStateHandle から画面引数が取れなくなった 1 Nav3 2 そもそも なぜ今までは取れていたのか

    を追う 3 犯人は 4 Nav3 defaultViewModelCreationExtras だった はその配管を捨てた。だから書き方が変わる 02 / 41
  2. これまでの"いつもの"書き方 Navigation Component Fragment + Safe Args ナビゲーショングラフに <argument> を定義

    遷移先の ViewModel は SavedStateHandle から取り 出す Fragment 側は by viewModels() のみ。特別な設定 は不要 class UserDetailViewModel( savedStateHandle: SavedStateHandle, ) : ViewModel() { // キーを指定して取得 private val userId: Long = checkNotNull(savedStateHandle["userId"]) // Safe Args なら型安全に取得できる private val args = UserDetailFragmentArgs .fromSavedStateHandle(savedStateHandle) } 04 / 41
  3. でも同じだった Compose 型安全ルート (Navigation 2.8.0+) @Serializable なクラスがルート兼引数になる ViewModel 側はやはり SavedStateHandle

    から ルートオブジェクトごと復元できる navigation-compose @Serializable data class UserDetail(val userId: Long) // 遷移 navController.navigate(UserDetail(userId)) // 遷移先 class UserDetailViewModel( savedStateHandle: SavedStateHandle, ) : ViewModel() { private val route = savedStateHandle.toRoute<UserDetail>() } 05 / 41
  4. ところがNav3では取れない 同じつもりで書くと null が返ってくる キー名を間違えたわけでもない DI 設定が漏れているわけでもない は設定済み (未設定なら ViewModel

    のスコープの話になり、別問 題) ViewModelStoreNavEntryDecorator Navigation 3 data class RouteB(val id: String) @HiltViewModel class RouteBViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { // null が返る private val id: String? = savedStateHandle["id"] } 06 / 41
  5. 公式レシピはこう書いている nav3-recipes HiltViewModelsActivity.kt — android/nav3-recipes 1 entry<RouteB> { key ->

    2 val viewModel = hiltViewModel<RouteBViewModel, RouteBViewModel.Factory>( 3 creationCallback = { factory -> 5 画面引数はインスタンス生成時に // 実行コンテキストから動的に注入する 6 factory.create(key) 4 // 7 } 8 ) 9 ScreenB(viewModel = viewModel) 10 } 07 / 41
  6. 公式レシピはこう書いている nav3-recipes HiltViewModelsActivity.kt — android/nav3-recipes 1 entry<RouteB> { key ->

    2 val viewModel = hiltViewModel<RouteBViewModel, RouteBViewModel.Factory>( 3 creationCallback = { factory -> 5 画面引数はインスタンス生成時に // 実行コンテキストから動的に注入する 6 factory.create(key) 4 // 7 } 8 ) 9 ScreenB(viewModel = viewModel) 10 } 07 / 41
  7. 画面引数はコンストラクタで受け取る nav3-recipes RouteBViewModel.kt — android/nav3-recipes 1 @HiltViewModel(assistedFactory = RouteBViewModel.Factory::class) 2

    class RouteBViewModel @AssistedInject constructor( 3 4 @Assisted val navKey: RouteB, // 画面引数がコンストラクタで渡される ) : ViewModel() { 5 6 @AssistedFactory 7 interface Factory { 8 fun create(navKey: RouteB): RouteBViewModel 9 10 } } 08 / 41
  8. 画面引数はコンストラクタで受け取る nav3-recipes RouteBViewModel.kt — android/nav3-recipes 1 @HiltViewModel(assistedFactory = RouteBViewModel.Factory::class) 2

    class RouteBViewModel @AssistedInject constructor( 3 4 @Assisted val navKey: RouteB, // 画面引数がコンストラクタで渡される ) : ViewModel() { 5 6 @AssistedFactory 7 interface Factory { 8 fun create(navKey: RouteB): RouteBViewModel 9 10 } } 08 / 41
  9. の本来の仕事 SavedStateHandle 前提の確認 システムによるプロセス終了(プロセスキル)から状態を復元するための API Jetpack SavedState ライブラリを ViewModel から扱いやすくしたもの

    「画面が破棄されても消えてほしくない状態」を預ける場所 ここで一度立ち止まりたい 復元用の保存領域に、なぜ画面遷移の引数が入っていたのだろう? 09 / 41
  10. の中身を開く viewModel() ViewModel の生成経路 ViewModel.kt — lifecycle-viewmodel-compose 2.11.0 1 public

    fun <VM : ViewModel> viewModel( 2 modelClass: KClass<VM>, 3 viewModelStoreOwner: ViewModelStoreOwner = /* LocalViewModelStoreOwner */, 4 key: String? = null, 5 factory: ViewModelProvider.Factory? = null, 6 extras: CreationExtras = viewModelStoreOwner.defaultViewModelCreationExtras, 7 ): VM { 8 val resolvedFactory = factory ?: viewModelStoreOwner.defaultViewModelProviderFactory 9 val provider = ViewModelProvider.create(viewModelStoreOwner, resolvedFactory, extras) 10 return if (key != null) provider[key, modelClass] else provider[modelClass] 11 } 12 / 41
  11. の中身を開く viewModel() ViewModel の生成経路 ViewModel.kt — lifecycle-viewmodel-compose 2.11.0 1 public

    fun <VM : ViewModel> viewModel( 2 modelClass: KClass<VM>, 3 viewModelStoreOwner: ViewModelStoreOwner = /* LocalViewModelStoreOwner */, 4 key: String? = null, 5 factory: ViewModelProvider.Factory? = null, 6 extras: CreationExtras = viewModelStoreOwner.defaultViewModelCreationExtras, 7 ): VM { 8 val resolvedFactory = factory ?: viewModelStoreOwner.defaultViewModelProviderFactory 9 val provider = ViewModelProvider.create(viewModelStoreOwner, resolvedFactory, extras) 10 return if (key != null) provider[key, modelClass] else provider[modelClass] 11 } 12 / 41
  12. の中身を開く viewModel() ViewModel の生成経路 ViewModel.kt — lifecycle-viewmodel-compose 2.11.0 1 public

    fun <VM : ViewModel> viewModel( 2 modelClass: KClass<VM>, 3 viewModelStoreOwner: ViewModelStoreOwner = /* LocalViewModelStoreOwner */, 4 key: String? = null, 5 factory: ViewModelProvider.Factory? = null, 6 extras: CreationExtras = viewModelStoreOwner.defaultViewModelCreationExtras, 7 ): VM { 8 val resolvedFactory = factory ?: viewModelStoreOwner.defaultViewModelProviderFactory 9 val provider = ViewModelProvider.create(viewModelStoreOwner, resolvedFactory, extras) 10 return if (key != null) provider[key, modelClass] else provider[modelClass] 11 } 12 / 41
  13. 人の役者 登場人物 3 役者 役割 ViewModelProvider 既存インスタンスを返すか、新規生成するかを判断する ViewModelProvider.Factory 生成方法の具体を知っている CreationExtras

    生成に必要な追加情報の入れ物 今日の話で押さえるのは1点だけ Factory#create(modelClass, extras) に CreationExtras が渡される 13 / 41
  14. 生成する人と、材料を運ぶ人 Factory は create を持つだけのインターフェース public interface Factory { public

    fun <T : ViewModel> create( modelClass: KClass<T>, Factory / CreationExtras CreationExtras public inline fun <reified VM : ViewModel> ComponentActivity.viewModels( noinline extrasProducer: extras: CreationExtras, ): T } は実は普段から目にしている (() -> CreationExtras)? = null, // ~~~~~~~~~~~~~~ noinline factoryProducer: (() -> Factory)? = null, ): Lazy<VM> 14 / 41
  15. はFactoryの中で生まれる SavedStateHandle HiltViewModelFactory HiltViewModelFactory.java — google/dagger (hilt-android 2.57) @AndroidEntryPoint な

    Activity / Fragment のデフォルト Factory。 @HiltViewModel の生成部がこれ 1 @Override 2 public <T extends ViewModel> T create(Class<T> modelClass, CreationExtras extras) { 3 RetainedLifecycleImpl lifecycle = new RetainedLifecycleImpl(); 4 ViewModelComponent component = 5 viewModelComponentBuilder 6 .savedStateHandle(createSavedStateHandle(extras)) // ← 7 .viewModelLifecycle(lifecycle) 8 .build(); 9 T viewModel = createViewModel(component, modelClass, extras); 10 viewModel.addCloseable(lifecycle::dispatchOnCleared); 11 return viewModel; 12 ここで組み立てる } は androidx.lifecycle.SavedStateHandleSupport の static import。 つまり Hilt も androidx の関数で組 15 / 41 み立てている。中身を開くと…… createSavedStateHandle
  16. はFactoryの中で生まれる SavedStateHandle HiltViewModelFactory HiltViewModelFactory.java — google/dagger (hilt-android 2.57) @AndroidEntryPoint な

    Activity / Fragment のデフォルト Factory。 @HiltViewModel の生成部がこれ 1 @Override 2 public <T extends ViewModel> T create(Class<T> modelClass, CreationExtras extras) { 3 RetainedLifecycleImpl lifecycle = new RetainedLifecycleImpl(); 4 ViewModelComponent component = 5 viewModelComponentBuilder 6 .savedStateHandle(createSavedStateHandle(extras)) // ← 7 .viewModelLifecycle(lifecycle) 8 .build(); 9 T viewModel = createViewModel(component, modelClass, extras); 10 viewModel.addCloseable(lifecycle::dispatchOnCleared); 11 return viewModel; 12 ここで組み立てる } は androidx.lifecycle.SavedStateHandleSupport の static import。 つまり Hilt も androidx の関数で組 15 / 41 み立てている。中身を開くと…… createSavedStateHandle
  17. はFactoryの中で生まれる SavedStateHandle HiltViewModelFactory HiltViewModelFactory.java — google/dagger (hilt-android 2.57) @AndroidEntryPoint な

    Activity / Fragment のデフォルト Factory。 @HiltViewModel の生成部がこれ 1 @Override 2 public <T extends ViewModel> T create(Class<T> modelClass, CreationExtras extras) { 3 RetainedLifecycleImpl lifecycle = new RetainedLifecycleImpl(); 4 ViewModelComponent component = 5 viewModelComponentBuilder 6 .savedStateHandle(createSavedStateHandle(extras)) // ← 7 .viewModelLifecycle(lifecycle) 8 .build(); 9 T viewModel = createViewModel(component, modelClass, extras); 10 viewModel.addCloseable(lifecycle::dispatchOnCleared); 11 return viewModel; 12 ここで組み立てる } は androidx.lifecycle.SavedStateHandleSupport の static import。 つまり Hilt も androidx の関数で組 15 / 41 み立てている。中身を開くと…… createSavedStateHandle
  18. 初期値はCreationExtrasから来る SavedStateHandle の組み立て SavedStateHandleSupport.kt — lifecycle-viewmodel-savedstate 2.11.0 1 public fun

    CreationExtras.createSavedStateHandle(): SavedStateHandle { 2 val savedStateRegistryOwner = this[SAVED_STATE_REGISTRY_OWNER_KEY] 3 ?: throw IllegalArgumentException(/* ... */) 4 val viewModelStateRegistryOwner = this[VIEW_MODEL_STORE_OWNER_KEY] 5 ?: throw IllegalArgumentException(/* ... */) 6 7 val defaultArgs = this[DEFAULT_ARGS_KEY] // ← SavedStateHandle の初期値はここから来る 8 9 val key = this[VIEW_MODEL_KEY] 10 ?: throw IllegalArgumentException(/* ... */) 11 return createSavedStateHandle( 12 savedStateRegistryOwner, viewModelStateRegistryOwner, key, defaultArgs, 13 14 ) } 16 / 41
  19. 初期値はCreationExtrasから来る SavedStateHandle の組み立て SavedStateHandleSupport.kt — lifecycle-viewmodel-savedstate 2.11.0 1 public fun

    CreationExtras.createSavedStateHandle(): SavedStateHandle { 2 val savedStateRegistryOwner = this[SAVED_STATE_REGISTRY_OWNER_KEY] 3 ?: throw IllegalArgumentException(/* ... */) 4 val viewModelStateRegistryOwner = this[VIEW_MODEL_STORE_OWNER_KEY] 5 ?: throw IllegalArgumentException(/* ... */) 6 7 val defaultArgs = this[DEFAULT_ARGS_KEY] // ← SavedStateHandle の初期値はここから来る 8 9 val key = this[VIEW_MODEL_KEY] 10 ?: throw IllegalArgumentException(/* ... */) 11 return createSavedStateHandle( 12 savedStateRegistryOwner, viewModelStateRegistryOwner, key, defaultArgs, 13 14 ) } 16 / 41
  20. 問題が言い換えられた SavedStateHandle 中間整理 の初期値は CreationExtras の DEFAULT_ARGS_KEY から来る つまり 「SavedStateHandleに画面引数が入っているか」

    = 「誰かが DEFAULT_ARGS_KEY に詰めたか」 補足: Hilt を使わない場合も同じ。担当がデフォルトの SavedStateViewModelFactory に 変わるだけで、 CreationExtras から createSavedStateHandle() を呼ぶ点は変わらない 17 / 41
  21. 詰めていたのはNavBackStackEntry Navigation Component NavBackStackEntry.kt — navigation-common 2.9.8 の defaultViewModelCreationExtras の実体

    1 // NavBackStackEntry 2 internal val defaultViewModelCreationExtras: MutableCreationExtras 3 get() { 4 val extras = MutableCreationExtras() 5 extras[SAVED_STATE_REGISTRY_OWNER_KEY] = entry 6 extras[VIEW_MODEL_STORE_OWNER_KEY] = entry 7 arguments?.let { args -> extras[DEFAULT_ARGS_KEY] = args } // ← 8 return extras 9 ここ } https://github.com/androidx/androidx/blob/androidx-main/navigation/navigationcommon/src/commonMain/kotlin/androidx/navigation/NavBackStackEntry.kt 19 / 41
  22. 詰めていたのはNavBackStackEntry Navigation Component NavBackStackEntry.kt — navigation-common 2.9.8 の defaultViewModelCreationExtras の実体

    1 // NavBackStackEntry 2 internal val defaultViewModelCreationExtras: MutableCreationExtras 3 get() { 4 val extras = MutableCreationExtras() 5 extras[SAVED_STATE_REGISTRY_OWNER_KEY] = entry 6 extras[VIEW_MODEL_STORE_OWNER_KEY] = entry 7 arguments?.let { args -> extras[DEFAULT_ARGS_KEY] = args } // ← 8 return extras 9 ここ } https://github.com/androidx/androidx/blob/androidx-main/navigation/navigationcommon/src/commonMain/kotlin/androidx/navigation/NavBackStackEntry.kt 19 / 41
  23. はここで詰めていた Navigation 犯人確保……? が arguments を DEFAULT_ARGS_KEY に載せる → それが

    SavedStateHandle の初期値になる → だから savedStateHandle["userId"] で取れていた NavBackStackEntry 20 / 41
  24. はここで詰めていた Navigation 犯人確保……? が arguments を DEFAULT_ARGS_KEY に載せる → それが

    SavedStateHandle の初期値になる → だから savedStateHandle["userId"] で取れていた NavBackStackEntry ところで、これは Navigation 固有の仕組みなのだろうか? 20 / 41
  25. 同じ名前のプロパティが、あちこちにいる ComponentActivity 1.13.0 override val defaultViewModelCreationExtras Fragment 1.8.9 public CreationExtras

    get() { getDefaultViewModelCreationExtras() { val extras = MutableCreationExtras() MutableCreationExtras extras = ...; // ... // ... val intentExtras = intent?.extras if (getArguments() != null) { if (intentExtras != null) { extras.set( extras[DEFAULT_ARGS_KEY] = SavedStateHandleSupport.DEFAULT_ARGS_KEY, intentExtras } 実はNavigation固有ではない getArguments()); } } return extras return extras; } 21 / 41
  26. 詰めている行だけを抜き出すと 三者を並べる // ComponentActivity extras[DEFAULT_ARGS_KEY] = intent.extras // Fragment extras[DEFAULT_ARGS_KEY]

    = arguments // NavBackStackEntry extras[DEFAULT_ARGS_KEY] = arguments 間遷移も、Fragment 間遷移も、Navigation も 「画面引数が SavedStateHandle に入る」は全部同じ一つの仕組みだった Activity 22 / 41
  27. はkeyをそのまま渡すだけ NavEntry Navigation 3 NavEntry.kt — navigation3 1.1.4 1 public

    class NavEntry<T : Any>( 2 private val key: T, 3 public val contentKey: Any = defaultContentKey(key), 4 public val metadata: Map<String, Any> = emptyMap(), 5 private val content: @Composable (T) -> Unit, 6 ) { 7 @Composable 8 public fun Content() { 9 // entry<RouteB> { key -> ... } 10 this.content(key) 11 12 の key に、そのまま渡る } } 24 / 41
  28. はkeyをそのまま渡すだけ NavEntry Navigation 3 NavEntry.kt — navigation3 1.1.4 1 public

    class NavEntry<T : Any>( 2 private val key: T, 3 public val contentKey: Any = defaultContentKey(key), 4 public val metadata: Map<String, Any> = emptyMap(), 5 private val content: @Composable (T) -> Unit, 6 ) { 7 @Composable 8 public fun Content() { 9 // entry<RouteB> { key -> ... } 10 this.content(key) 11 12 の key に、そのまま渡る } } 24 / 41
  29. はkeyをそのまま渡すだけ NavEntry Navigation 3 NavEntry.kt — navigation3 1.1.4 1 public

    class NavEntry<T : Any>( 2 private val key: T, 3 public val contentKey: Any = defaultContentKey(key), 4 public val metadata: Map<String, Any> = emptyMap(), 5 private val content: @Composable (T) -> Unit, 6 ) { 7 @Composable 8 public fun Content() { 9 // entry<RouteB> { key -> ... } 10 this.content(key) 11 12 の key に、そのまま渡る } } 24 / 41
  30. もargumentsも登場しない Bundle Navigation 3 は Compose との統合だけを念頭に設計されている わざわざ Bundle に詰め直す理由がない

    結果として arguments という概念自体が存在しない Nav3 では、ViewModel との統合を担う ViewModelStoreNavEntryDecorator はどうか 25 / 41
  31. が実際にやっていること Decorator ViewModel 統合 ViewModelStoreNavEntryDecorator.kt — lifecycle-viewmodel-navigation3 2.11.0 1 public

    class ViewModelStoreNavEntryDecorator<T : Any>( 2 viewModelStoreProvider: ViewModelStoreProvider, 3 ) : NavEntryDecorator<T>( 4 onPop = { key -> viewModelStoreProvider.clearKey(key) }, 5 decorate = { entry -> 6 val owner = rememberViewModelStoreOwner( 7 entry.contentKey, viewModelStoreProvider, 8 savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, 9 ) 10 CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { 11 entry.Content() 12 } 13 14 }, ) 26 / 41
  32. が実際にやっていること Decorator ViewModel 統合 ViewModelStoreNavEntryDecorator.kt — lifecycle-viewmodel-navigation3 2.11.0 1 public

    class ViewModelStoreNavEntryDecorator<T : Any>( 2 viewModelStoreProvider: ViewModelStoreProvider, 3 ) : NavEntryDecorator<T>( 4 onPop = { key -> viewModelStoreProvider.clearKey(key) }, 5 decorate = { entry -> 6 val owner = rememberViewModelStoreOwner( 7 entry.contentKey, viewModelStoreProvider, 8 savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, 9 ) 10 CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { 11 entry.Content() 12 } 13 14 }, ) 26 / 41
  33. が実際にやっていること Decorator ViewModel 統合 ViewModelStoreNavEntryDecorator.kt — lifecycle-viewmodel-navigation3 2.11.0 1 public

    class ViewModelStoreNavEntryDecorator<T : Any>( 2 viewModelStoreProvider: ViewModelStoreProvider, 3 ) : NavEntryDecorator<T>( 4 onPop = { key -> viewModelStoreProvider.clearKey(key) }, 5 decorate = { entry -> 6 val owner = rememberViewModelStoreOwner( 7 entry.contentKey, viewModelStoreProvider, 8 savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, 9 ) 10 CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { 11 entry.Content() 12 } 13 14 }, ) 26 / 41
  34. の仕事は"置き場所"を用意すること Decorator 決定的な不在 ごとに ViewModelStoreOwner を作る それを LocalViewModelStoreOwner に流す pop

    時に ViewModelStore をクリアする NavEntry そして NavEntry の key を DEFAULT_ARGS_KEY に詰める処理は、存在しない 27 / 41
  35. 引数は生成時に注入する を経由しないので、 インスタンス生 成のタイミングで直接渡す Navigation 3 SavedStateHandle Assisted Inject +

    creationCallback 文字列キーと Bundle の往復が消える ルートオブジェクトが型のまま手元に来る 失われたものというより、明示化されたと捉えたい @HiltViewModel( assistedFactory = RouteBViewModel.Factory::class ) class RouteBViewModel @AssistedInject constructor( @Assisted val navKey: RouteB, ) : ViewModel() { @AssistedFactory interface Factory { fun create(navKey: RouteB): RouteBViewModel } } 30 / 41
  36. 見えていなかったのは仕組みだった 視点の転換 から画面引数を取る機能は、公式・文書化された正規手段だった ただし、それを成立させていた仕組み (Navigation が arguments を DEFAULT_ARGS_KEY 経由で初期値に流し込む統合)は、

    普段は見えない場所にあった Nav3 はこの統合を捨て、引数は引数として明示的に型付きで渡す設計に振った SavedStateHandle 書き方が変わったのは、仕組みが変わったから 31 / 41
  37. まとめ まとめ の初期値は SavedStateHandle 2 Activity 3 Nav3 4 だから画面引数は生成時に注入する。配管が変わったので書き方も変わる

    の CreationExtras の 1 ・Fragment・NavBackStackEntryは全部 NavEntry DEFAULT_ARGS_KEY から来る defaultViewModelCreationExtras で同じことをしていた にはargumentsがなく、keyを詰める人もいない 32 / 41
  38. 参考 公式レシピ集 androidx/androidx — 本日のコードの出典 References nav3-recipes — google/dagger —

    HiltViewModelFactory の出典 本日のコードは以下のバージョンのソースで確認しています ライブラリ バージョン androidx.navigation3 1.1.4 androidx.lifecycle 2.11.0 androidx.navigation 2.9.8 androidx.activity / androidx.fragment 1.13.0 / 1.8.9 com.google.dagger:hilt-android 2.57 33 / 41
  39. でプロセスキル復元はどうするの? Nav3 Q&A 自体はちゃんと機能する ViewModelStoreNavEntryDecorator が SavedStateRegistryOwner を配線している savedStateHandle["key"] =

    value で保存した値は復元される SavedStateHandle 機能しないのではなく、画面引数が初期値として入ってこないだけ 36 / 41
  40. で引数を渡せないの? NavEntry.metadata NavEntry Q&A には metadata: Map<String, Any> がある public

    val metadata: Map<String, Any> = emptyMap(), ただしこれは NavDisplay 向けの表示メタ情報(トランジション指定など)であり、 ViewModel の生成経路( CreationExtras )には接続 されていない → 画面引数の受け渡しには使えない 37 / 41
  41. を使いたくない場合は? Assisted Inject creationCallback Q&A を使わず、key を Composable 側で保持して ViewModel

    のメソッド引数として渡す方法もある entry<RouteB> { key -> val viewModel: RouteBViewModel = hiltViewModel() LaunchedEffect(key) { viewModel.load(key.id) } ScreenB(viewModel = viewModel) } ただし公式レシピが示しているのは Assisted Inject の方 (初期化タイミングが init に閉じるぶん、状態の組み立てが素直になる) もう一歩踏み込んで、従来の書き方そのものに戻す方法もある(次ページ) 38 / 41
  42. どうしても従来の書き方に戻したい場合は? Q&A 詰める人がいないなら、自分が詰める人になればいい の初期値が DEFAULT_ARGS_KEY から来ることは本編で見た通り ならば、extras に自分で DEFAULT_ARGS_KEY を詰めて渡せばいい

    hiltViewModel() には extras の口がないが、素の viewModel() にはある ViewModel 側は Nav2 時代のコードのまま一切変更不要 SavedStateHandle 次ページで具体的な書き方を示す( savedStateHandle["id"] が復活する) 39 / 41
  43. を自分で取得してextrasごと渡す Factory rememberHiltViewModelFactory — androidx.hilt 1.4.0-beta01 1 以降で public entry<RouteB>

    { key -> 2 val owner = checkNotNull(LocalViewModelStoreOwner.current) 3 val viewModel: RouteBViewModel = viewModel( 4 factory = rememberHiltViewModelFactory(), 5 extras = MutableCreationExtras(owner.defaultViewModelCreationExtras) 6 .apply { this[DEFAULT_ARGS_KEY] = encodeToSavedState(key) }, 7 ) 8 ScreenB(viewModel = viewModel) 9 配管を敷き直す } には extras の口がないので、素の viewModel() に降りるのがポイント owner のデフォルト extras をコピーするので、 createSavedStateHandle が要求する SAVED_STATE_REGISTRY_OWNER_KEY などは Decorator が設定済みのものがそのまま揃う hiltViewModel() 40 / 41
  44. を自分で取得してextrasごと渡す Factory rememberHiltViewModelFactory — androidx.hilt 1.4.0-beta01 1 以降で public entry<RouteB>

    { key -> 2 val owner = checkNotNull(LocalViewModelStoreOwner.current) 3 val viewModel: RouteBViewModel = viewModel( 4 factory = rememberHiltViewModelFactory(), 5 extras = MutableCreationExtras(owner.defaultViewModelCreationExtras) 6 .apply { this[DEFAULT_ARGS_KEY] = encodeToSavedState(key) }, 7 ) 8 ScreenB(viewModel = viewModel) 9 配管を敷き直す } には extras の口がないので、素の viewModel() に降りるのがポイント owner のデフォルト extras をコピーするので、 createSavedStateHandle が要求する SAVED_STATE_REGISTRY_OWNER_KEY などは Decorator が設定済みのものがそのまま揃う hiltViewModel() 40 / 41
  45. を自分で取得してextrasごと渡す Factory rememberHiltViewModelFactory — androidx.hilt 1.4.0-beta01 1 以降で public entry<RouteB>

    { key -> 2 val owner = checkNotNull(LocalViewModelStoreOwner.current) 3 val viewModel: RouteBViewModel = viewModel( 4 factory = rememberHiltViewModelFactory(), 5 extras = MutableCreationExtras(owner.defaultViewModelCreationExtras) 6 .apply { this[DEFAULT_ARGS_KEY] = encodeToSavedState(key) }, 7 ) 8 ScreenB(viewModel = viewModel) 9 配管を敷き直す } には extras の口がないので、素の viewModel() に降りるのがポイント owner のデフォルト extras をコピーするので、 createSavedStateHandle が要求する SAVED_STATE_REGISTRY_OWNER_KEY などは Decorator が設定済みのものがそのまま揃う hiltViewModel() 40 / 41
  46. 敷き直しの作法と代償 配管を敷き直す なぜ savedStateHandle["id"] で取れるのか encodeToSavedState は @Serializable な key

    の各プロパティを、 プロパティ名をキーにしてルートに平坦に書き込む 復元の意味論も Nav2 と同じ(プロセスキル復元値が defaultArgs を上書きするマージ) ただし代償もある 「文字列キーと SavedState の往復」という暗黙の配管を自分の責任で復活させることになる 公式レシピの推奨(引数は型のまま明示的に渡す)からは外れる 使いどころ: ViewModel とテストを変えずに Nav2 → Nav3 移行を進めたい過渡期 41 / 41