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

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

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.
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