Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Navigation3でやりとりした画面引数をSavedStateHandleから引き出せない理由
Search
mikan
July 22, 2026
Technology
30
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
89
Navigation3でViewModelにデータを渡す方法
mikanichinose
0
730
「脳に収まるコードの書き方」を読んで学んだこと
mikanichinose
1
240
RepositoryのSSoT化
mikanichinose
0
100
Kotlin Multiplatform 始めました
mikanichinose
1
170
Web APIをなぜつくるのか
mikanichinose
0
4.3k
イベントをどう管理するか
mikanichinose
3
420
ライブラリでしかお目にかかれない珍しい実装
mikanichinose
2
520
Strong Skipping Mode によってrecompositionはどう変わったのか
mikanichinose
0
420
Other Decks in Technology
See All in Technology
Snowflakeで実現する全社横断の顧客の声(VOC)分析・活用基盤@Snowflake World Tour Tokyo 2026
yuto16
0
190
DEFCON_CHV_CTF_Write-up.pdf
bata_24
0
140
omasushiというライブラリを作った
polidog
PRO
0
200
Slack上でインフラをトラブルシュートする! Agentic Platform Engineeringの第一歩
teru0x1
4
1.3k
消えない 動かない 効かない
yama3133
1
120
WAF 運用改善の承認サイクル/SRE_BizReach_MIXI_1
visional_engineering_and_design
2
560
AI時代だからこそ、スケールしないことをやろう
yutashigemura
1
150
Reactの設計論
uhyo
15
8.4k
リージョンの壁を越える、 ちょっと変わったAWSサービスの話
falken
PRO
1
310
全員がプロダクトへ向き合う組織を持続成長させるために——組織づくりのフライホイールと4象限 / The Flywheel Model and Four Quadrants for Organizational Design
hiro_torii
4
980
AIネイティブプロダクトで顧客価値を最大化するプロダクトエンジニアとFDEの協働
righttouch
PRO
0
240
AIで実装は速くなった。なのにプロダクトは速くならない。職能の壁を越えて価値のフローを設計する
nwiizo
8
7.9k
Featured
See All Featured
Statistics for Hackers
jakevdp
799
230k
Highjacked: Video Game Concept Design
rkendrick25
PRO
1
450
WENDY [Excerpt]
tessaabrams
13
39k
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
1
380
VelocityConf: Rendering Performance Case Studies
addyosmani
331
25k
What's in a price? How to price your products and services
michaelherold
247
13k
Producing Creativity
orderedlist
PRO
348
41k
Docker and Python
trallard
47
4.2k
The Language of Interfaces
destraynor
162
27k
Jamie Indigo - Trashchat’s Guide to Black Boxes: Technical SEO Tactics for LLMs
techseoconnect
PRO
0
660
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
3
1.2k
Art, The Web, and Tiny UX
lynnandtonic
304
22k
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