如何在 NavGraph 组件之间(仅限)共享 ViewModel?

7
我希望能够在多个组件之间共享一个视图模型,就像我们在一个 Activity 中的 Fragment 之间共享视图模型一样。
但是当我尝试这样做时,
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") { ... }
        }
    }
}

我遇到了一个错误

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

需要

  • ViewModel应该仅在NavGraph范围内活动。
  • 当我去不同的路线并回来时,我应该初始化一个新的ViewModel(这就是为什么我在NavGraph中调用它的原因)

几乎相似的解决方案

  1. Philip Dukhov为问题如何在Compose NavGraph中的两个或多个Jetpack组合中共享ViewModel?提供的答案

    但是,在这种方法中,ViewModel保留在启动它的Activity的范围内,因此永远不会被垃圾回收。


请查看此答案 - Phil Dukhov
Compose Navigation 基于 Jetpack Navigation,因此本文档中的所有内容都适用于 Compose。该答案使用的是 Compose 代码。 - Phil Dukhov
Navigation Compose的另一种解决方案是Jetmagic。它对共享viewmodels有很好的支持:https://github.com/JohannBlake/Jetmagic - Johann
@AndroidDev 哦,不是不尊重。只是如果项目停止更新,那么维护起来会很困难。我会去看看的 :) - clamentjohn
@clmno 你是指像谷歌的YouTube视频播放器一样吗?他们已经6年没有更新了,而且它不能用于为Compose编写的Android应用程序 - 更不用说谷歌甚至都不费心发布播放器的源代码。最终,Android开发人员现在无法在Compose中播放YouTube视频。因此,关于API未得到维护的论点似乎有些苍白,因为谷歌自己都无法维护自己的API。 - Johann
显示剩余2条评论
2个回答

9

解决方案1

(来源于文档

导航后退堆栈存储了一个 NavBackStackEntry 不仅为每个单独的目标,还包括每个包含单独目标的父级导航图。这使您能够检索到作用域限定于导航图的 NavBackStackEntry ,从而创建一个关联到导航图的 ViewModel,从而实现在导航图的目标之间共享UI相关数据。在此方式下创建的任何 ViewModel 对象生存期持续到关联的 NavHost 和其 ViewModelStore 被清除,或者导航图从后退堆栈弹出。

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

在每个组合项中添加以下内容,以获取 loginBackStackEntry ,然后将其用作 ViewModelStoreOwner 以获取视图模型。

val loginBackStackEntry = remember { navController.getBackStackEntry("login") }
val loginViewModel: LoginViewModel = viewModel(loginBackStackEntry)

因此,最终的代码更改为:
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)
                ... 
            }
        }
    }
}

解决方案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()
}
  1. 接下来只需在你的导航图中使用此扩展程序
navigate(secondNestedRoute, startDestination = nestedStartRoute) {
  composable(route) {
    val loginViewModel: LoginViewModel = it.parentViewModel(navController)
  }
}

为什么我们不能使用 viewModel(parentBackStackEntry) - Afzal N
2
如果您正在使用 Hilt 进行依赖注入,只需在上面的代码中使用 hiltViewModel(...) 而不是 viewModel(...) 即可。 - lenooh
@lenooh,您的意思是应该使用remember + getBackStackEntry + hiltViewModel吗?还是其他方法? - BArtWell
BArtWell: 是的,那是正确的。 - lenooh
如果使用嵌套的导航图,这个扩展函数可能会导致应用程序崩溃,如果你导航到这个导航图,然后通过后退导航弹出它的父级BackStackEntry。这个YouTube视频展示了一个非常类似的扩展函数,但是防止尝试读取不存在的后退堆栈条目。https://www.youtube.com/watch?v=FIEnIBq7Ups - undefined

0
首先,创建一个函数来获取导航的 ViewModelStoreOwner。
@Composable
fun rememberParentViewModelStoreOwner(
    navController: NavHostController,
    parentRoute: String,
): ViewModelStoreOwner {
    return remember(navController.currentBackStackEntry) {
        object : ViewModelStoreOwner {
            override val viewModelStore =
                navController.getBackStackEntry(parentRoute).viewModelStore
        }
    }
}

然后,检索它并在您所需的可组合构建器中使用它来构建ViewModel
composable("username") {
    val loginViewModelStoreOwner = rememberParentViewModelStoreOwner(navController, "login")
    val loginViewModel: LoginViewModel = viewModel(loginViewModelStoreOwner)

    // ...
}

composable("password") {
    val loginViewModelStoreOwner = rememberParentViewModelStoreOwner(navController, "login")
    val loginViewModel: LoginViewModel = viewModel(loginViewModelStoreOwner)

    // ...
}

composable("registration") {
    val loginViewModelStoreOwner = rememberParentViewModelStoreOwner(navController, "login")
    val loginViewModel: LoginViewModel = viewModel(loginViewModelStoreOwner)

    // ...
}

解释:

通常情况下,一个ViewModel是通过局部作用域的ViewModelStoreOwnerLocalViewModelStoreOwner.current)从可组合构建器的NavBackStackEntry中构建的。如果已经使用相同的ViewModelStoreOwner构建了一个ViewModel,它将重用先前的实例而不是再次创建。在这种情况下,我们将使用登录导航ViewModelStoreOwner来构建ViewModel


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接