Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Navigation3でやりとりした画面引数をSavedStateHandleから引き出せない理由
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
mikan
July 22, 2026
Technology
13
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Navigation3でやりとりした画面引数をSavedStateHandleから引き出せない理由
mikan
July 22, 2026
More Decks by mikan
See All by mikan
Lazy APIを使ってGradleビルド速度を改善する
mikanichinose
1
76
Navigation3でViewModelにデータを渡す方法
mikanichinose
0
710
「脳に収まるコードの書き方」を読んで学んだこと
mikanichinose
1
230
RepositoryのSSoT化
mikanichinose
0
95
Kotlin Multiplatform 始めました
mikanichinose
1
160
Web APIをなぜつくるのか
mikanichinose
0
4k
イベントをどう管理するか
mikanichinose
3
410
ライブラリでしかお目にかかれない珍しい実装
mikanichinose
2
510
Strong Skipping Mode によってrecompositionはどう変わったのか
mikanichinose
0
410
Other Decks in Technology
See All in Technology
変更し続けられるシステムをどう保つか — AI時代のSSoTという設計原則
kawauso
1
1k
Aurora MySQL 8.4リリース! Rubyistが備えること / what-rubyist-should-prepare-for-aurora-mysql-8-4
fkmy
0
600
クラウドを使う側から、作る側へ / 大吉祥寺.pm 2026前夜祭
fujiwara3
4
1.1k
キャリアLT会#3
beli68
2
230
Vポイント分析基盤におけるデータモデリング20年史
taromatsui_cccmkhd
4
710
壊して学ぶAWS CDK: そのcdk deployで消えるもの、残るもの
k_adachi_01
1
480
Type-safe IaC for Dart
coborinai
0
180
AI Native なプロダクト組織の立ち上げ方 : 生産性 100 倍への挑戦
mikesorae
0
1.2k
Alphaモジュール使っていいのかい!?いけないのかい!?どっちなんだいっ!?
watany
1
320
OPENLOGI Company Profile for engineer
hr01
1
74k
Webアプリ認証の全体像 / The Big Picture of Web App Authentication
kitano_yuichi
1
410
Jitera Company Deck
jitera
0
270
Featured
See All Featured
Marketing to machines
jonoalderson
1
5.6k
How To Speak Unicorn (iThemes Webinar)
marktimemedia
1
510
Deep Space Network (abreviated)
tonyrice
0
230
WCS-LA-2024
lcolladotor
0
750
Designing Powerful Visuals for Engaging Learning
tmiket
1
460
The Curse of the Amulet
leimatthew05
2
13k
Building Flexible Design Systems
yeseniaperezcruz
330
40k
Product Roadmaps are Hard
iamctodd
55
12k
Avoiding the “Bad Training, Faster” Trap in the Age of AI
tmiket
0
190
Design of three-dimensional binary manipulators for pick-and-place task avoiding obstacles (IECON2024)
konakalab
0
490
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
0
350
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
380
Transcript
[Android] 技術共有会 / 2026.07.23 でやりとりした 画面引数を から 引き出せない理由 Navigation3 SavedStateHandle
m mikan( 一瀬喜弘) 10 20 30 40 60 80 95
目次 Agenda に移行したら SavedStateHandle から画面引数が取れなくなった 1 Nav3 2 そもそも なぜ今までは取れていたのか
を追う 3 犯人は 4 Nav3 defaultViewModelCreationExtras だった はその配管を捨てた。だから書き方が変わる 02 / 41
01 あれ、取れない
これまでの"いつもの"書き方 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
でも同じだった 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
ところが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
公式レシピはこう書いている 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
公式レシピはこう書いている 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
画面引数はコンストラクタで受け取る 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
画面引数はコンストラクタで受け取る 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
の本来の仕事 SavedStateHandle 前提の確認 システムによるプロセス終了(プロセスキル)から状態を復元するための API Jetpack SavedState ライブラリを ViewModel から扱いやすくしたもの
「画面が破棄されても消えてほしくない状態」を預ける場所 ここで一度立ち止まりたい 復元用の保存領域に、なぜ画面遷移の引数が入っていたのだろう? 09 / 41
先に結論 結論 には、画面引数を SavedStateHandle の初期値に流し込む統合があった Nav3 にはそれがない Nav2 機能そのものは公式・文書化されていた ただし、それを成立させていた配管は見えない場所にあった
では、その配管は誰が・どうやって敷いていたのか — ここからが本編 10 / 41
02 SavedStateHandle はどこから来るのか
の中身を開く 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
の中身を開く 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
の中身を開く 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
人の役者 登場人物 3 役者 役割 ViewModelProvider 既存インスタンスを返すか、新規生成するかを判断する ViewModelProvider.Factory 生成方法の具体を知っている CreationExtras
生成に必要な追加情報の入れ物 今日の話で押さえるのは1点だけ Factory#create(modelClass, extras) に CreationExtras が渡される 13 / 41
生成する人と、材料を運ぶ人 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
は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
は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
は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
初期値は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
初期値は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
問題が言い換えられた SavedStateHandle 中間整理 の初期値は CreationExtras の DEFAULT_ARGS_KEY から来る つまり 「SavedStateHandleに画面引数が入っているか」
= 「誰かが DEFAULT_ARGS_KEY に詰めたか」 補足: Hilt を使わない場合も同じ。担当がデフォルトの SavedStateViewModelFactory に 変わるだけで、 CreationExtras から createSavedStateHandle() を呼ぶ点は変わらない 17 / 41
03 手口はdefaultViewModelCreationExtras
詰めていたのは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
詰めていたのは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
はここで詰めていた Navigation 犯人確保……? が arguments を DEFAULT_ARGS_KEY に載せる → それが
SavedStateHandle の初期値になる → だから savedStateHandle["userId"] で取れていた NavBackStackEntry 20 / 41
はここで詰めていた Navigation 犯人確保……? が arguments を DEFAULT_ARGS_KEY に載せる → それが
SavedStateHandle の初期値になる → だから savedStateHandle["userId"] で取れていた NavBackStackEntry ところで、これは Navigation 固有の仕組みなのだろうか? 20 / 41
同じ名前のプロパティが、あちこちにいる 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
詰めている行だけを抜き出すと 三者を並べる // 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
では、Nav3はどうか 23 / 41
は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
は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
は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
もargumentsも登場しない Bundle Navigation 3 は Compose との統合だけを念頭に設計されている わざわざ Bundle に詰め直す理由がない
結果として arguments という概念自体が存在しない Nav3 では、ViewModel との統合を担う ViewModelStoreNavEntryDecorator はどうか 25 / 41
が実際にやっていること 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
が実際にやっていること 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
が実際にやっていること 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
の仕事は"置き場所"を用意すること Decorator 決定的な不在 ごとに ViewModelStoreOwner を作る それを LocalViewModelStoreOwner に流す pop
時に ViewModelStore をクリアする NavEntry そして NavEntry の key を DEFAULT_ARGS_KEY に詰める処理は、存在しない 27 / 41
画面遷移の引数を入れる人がいない だから、入っていない 28 / 41
04 どう受け止め、どう書くか
引数は生成時に注入する を経由しないので、 インスタンス生 成のタイミングで直接渡す 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
見えていなかったのは仕組みだった 視点の転換 から画面引数を取る機能は、公式・文書化された正規手段だった ただし、それを成立させていた仕組み (Navigation が arguments を DEFAULT_ARGS_KEY 経由で初期値に流し込む統合)は、
普段は見えない場所にあった Nav3 はこの統合を捨て、引数は引数として明示的に型付きで渡す設計に振った SavedStateHandle 書き方が変わったのは、仕組みが変わったから 31 / 41
まとめ まとめ の初期値は SavedStateHandle 2 Activity 3 Nav3 4 だから画面引数は生成時に注入する。配管が変わったので書き方も変わる
の CreationExtras の 1 ・Fragment・NavBackStackEntryは全部 NavEntry DEFAULT_ARGS_KEY から来る defaultViewModelCreationExtras で同じことをしていた にはargumentsがなく、keyを詰める人もいない 32 / 41
参考 公式レシピ集 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
ご清聴ありがとうございました
+ Appendix
でプロセスキル復元はどうするの? Nav3 Q&A 自体はちゃんと機能する ViewModelStoreNavEntryDecorator が SavedStateRegistryOwner を配線している savedStateHandle["key"] =
value で保存した値は復元される SavedStateHandle 機能しないのではなく、画面引数が初期値として入ってこないだけ 36 / 41
で引数を渡せないの? NavEntry.metadata NavEntry Q&A には metadata: Map<String, Any> がある public
val metadata: Map<String, Any> = emptyMap(), ただしこれは NavDisplay 向けの表示メタ情報(トランジション指定など)であり、 ViewModel の生成経路( CreationExtras )には接続 されていない → 画面引数の受け渡しには使えない 37 / 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
どうしても従来の書き方に戻したい場合は? Q&A 詰める人がいないなら、自分が詰める人になればいい の初期値が DEFAULT_ARGS_KEY から来ることは本編で見た通り ならば、extras に自分で DEFAULT_ARGS_KEY を詰めて渡せばいい
hiltViewModel() には extras の口がないが、素の viewModel() にはある ViewModel 側は Nav2 時代のコードのまま一切変更不要 SavedStateHandle 次ページで具体的な書き方を示す( savedStateHandle["id"] が復活する) 39 / 41
を自分で取得して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
を自分で取得して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
を自分で取得して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
敷き直しの作法と代償 配管を敷き直す なぜ savedStateHandle["id"] で取れるのか encodeToSavedState は @Serializable な key
の各プロパティを、 プロパティ名をキーにしてルートに平坦に書き込む 復元の意味論も Nav2 と同じ(プロセスキル復元値が defaultArgs を上書きするマージ) ただし代償もある 「文字列キーと SavedState の往復」という暗黙の配管を自分の責任で復活させることになる 公式レシピの推奨(引数は型のまま明示的に渡す)からは外れる 使いどころ: ViewModel とテストを変えずに Nav2 → Nav3 移行を進めたい過渡期 41 / 41