Open6

ComposeNavigationでSavedStateHandleを使ってViewModelに画面引数を渡す仕組み

ゆつぼゆつぼ

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
}

参考:
https://developer.android.com/guide/navigation/design/type-safety?source=post_page-----282667643ea3--------------------------------#arguments-wrapper
https://github.com/android/architecture-samples/blob/views/app/src/main/java/com/example/android/architecture/blueprints/todoapp/ViewModelFactory.kt

ゆつぼゆつぼ

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を返していた。
状態によっては異なるものになるかもしれない。