如何在 NavGraph 组件之间共享视图模型(仅限)

clm*_*mno 7 android viewmodel android-jetpack-compose dagger-hilt

我想在许多可组合项之间共享视图模型。就像我们如何在活动中的片段之间共享视图模型一样。

但是当我尝试这个时

setContent {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        navigation(startDestination = "username", route = "login") {
            // FIXME: I get an error here
            val viewModel: LoginViewModel = viewModel()
            composable("username") { ... }
            composable("password") { ... }
            composable("registration") { ... }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我收到一个错误

@Composable 调用只能在 @Composable 函数的上下文中发生

需要

  • 视图模型应该仅在 NavGraph 范围中处于活动状态。
  • 当我转到不同的路线并返回时,我应该初始化一个新的视图模型(这就是我在导航图中调用它的原因)

几乎类似的解决方案

  1. Philip Dukhov对问题的回答:如何在 Compose NavGraph 内的两个或多个 Jetpack 可组合项之间共享视图模型?

    但在这种方法中,视图模型保留在启动它的活动的范围内,因此永远不会被垃圾收集。

clm*_*mno 9

解决方案1

(从文档复制)

导航返回堆栈不仅为每个单独的目的地存储NavBackStackEntry ,还为包含单独目的地的每个父导航图存储 NavBackStackEntry。这允许您检索范围NavBackStackEntry为导航图的 。导航图范围NavBackStackEntry提供了一种创建ViewModel导航图范围的方法,使您能够在图的目标之间共享 UI 相关数据。ViewModel以这种方式创建的任何对象都会一直存在,直到关联对象NavHost及其ViewModelStore被清除或从返回堆栈中弹出导航图为止。

这意味着我们可以使用 NavBackStackEntry 来获取我们所在的导航图的范围,并使用它来ViewModelStoreOwner获取该范围的视图模型。

将其添加到每个可组合项中以获取BackStackEntryfor login,然后使用它作为ViewModelStoreOwner来获取视图模型。

val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
Run Code Online (Sandbox Code Playgroud)

所以最终代码改为

setContent {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        navigation(startDestination = "username", route = "login") {
            composable("username") { 
                val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
                val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
                ... 
            }
            composable("password") { 
                val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
                val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
                ... 
            }
            composable("registration") { 
                val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
                val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)
                ... 
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

解决方案2

复制自ianhanniballake答案

这也可以使用扩展来实现

  1. 获取当前范围并获取或创建该范围的视图模型
@Composable
fun <reified VM : ViewModel> NavBackStackEntry.parentViewModel(
    navController: NavController
): VM {
    // First, get the parent of the current destination
    // This always exists since every destination in your graph has a parent
    val parentId = destination.parent!!.id

    // Now get the NavBackStackEntry associated with the parent
    val parentBackStackEntry = navController.getBackStackEntry(parentId)

    // And since we can't use viewModel(), we use ViewModelProvider directly
    // to get the ViewModel instance, using the lifecycle-viewmodel-ktx extension
    return ViewModelProvider(parentBackStackEntry).get()
}
Run Code Online (Sandbox Code Playgroud)
  1. 然后只需在导航图中使用此扩展即可
navigate(secondNestedRoute, startDestination = nestedStartRoute) {
  composable(route) {
    val loginViewModel: LoginViewModel = it.parentViewModel(navController)
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果您使用 hilt 进行 DI,只需在上面的代码中使用 `hiltViewModel(...)` 而不是 `viewModel(...)`。 (2认同)