ComposeNavigationでSavedStateHandleを使ってViewModelに画面引数を渡す仕組み
前提
- Dagger2(No Hilt)
- Compose
- Navigation Compose
ViewModelのコンストラクタに、SavedStateHandleを定義すると、画面引数をコンストラクタで取れる。
ProfileViewModel(
savedStateHandle: SavedStateHandle,
profileRepository: ProfileRepository,
){
private val userId: String? = savedStateHandle.get<String>("userId")
}
NavigationをNavArgsを使って実装する。
引数は、backStackEntry.arguments?.getString("id")
という形で受け取れる。
backStackEntry
は、Navigationのcomposableブロックのitとして手に入る。
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
viewModel()の第一引数がViewModelStoreOwner
で、NavBackStackEntry
はViewModelStoreOwnerを実装しているため、このようにしてViewModelを生成できる。
composable("profile/{userId}") { backStackEntry ->
Profile(viewModel = viewModel(backStackEntry, factory = viewModelFactory))
}
が、デフォルト値がLocalViewModelStoreOwner.currentになっているため、省略できる。
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
composable("profile/{userId}") { backStackEntry ->
Profile(viewModel = viewModel(factory = viewModelFactory))
}
ViewModelFactoryは、createメソッドのCreationExtras
を使うことでSavedStateHandleを入手できる。
public fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T =
create(modelClass)
extrasにはcreateSavedStateHandle
が生えており、SavedStateHandleを生成できる。
val TodoViewModelFactory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T =
with(modelClass) {
val application = checkNotNull(extras[APPLICATION_KEY]) as TodoApplication
val tasksRepository = application.taskRepository
when {
isAssignableFrom(AddEditTaskViewModel::class.java) ->
AddEditTaskViewModel(tasksRepository)
isAssignableFrom(TasksViewModel::class.java) -> {
val handle = extras.createSavedStateHandle() // ★
TasksViewModel(tasksRepository, handle)
}
else ->
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
} as T
}
参考:
AbstractSavedStateViewModelFactory
というクラスもあるが、これはviewModelのライブラリが2.5.0より前の場合に使う必要があるものである。
composableに来るBackStackEntryと、viewModel()のデフォルト値のLocalViewModelStoreOwner.currentが同じか?
実行してみたところ、参照は同じ。
どちらもViewModelのSavedStateHandleで引数を受け取れた。
コードを追ってみた。
composable()のbackStackEntryは、NavHost.ktのcurrentEntryから来ており、それは
val currentEntry = visibleEntries.lastOrNull { entry -> it == entry }
から来て、
val visibleEntries by remember {
derivedStateOf {
allVisibleEntries.filter { entry ->
entry.destination.navigatorName == ComposeNavigator.NAME
}
}
}
として定義されていた。
更にたどると、NavControllerの変数として定義されていた。
public val visibleEntries: StateFlow<List<NavBackStackEntry>> =
_visibleEntries.asStateFlow()
navControllerには、currentBackStackEntryもあり、こちらは、backQueue.lastOrNull
を返していた。
状態によっては異なるものになるかもしれない。