如何在Compose NavGraph中的两个或多个Jetpack组件之间共享一个ViewModel?

19
考虑这个例子。
对于身份验证,我们将使用2个屏幕 - 一个屏幕用于输入电话号码,另一个屏幕用于输入OTP。
这两个屏幕都是使用Jetpack Compose制作的,并且对于NavGraph,我们正在使用Compose Navigation。
另外,我必须提到DI是由Koin处理的。
val navController = rememberNavController()

NavHost(navController) {
    navigation(
        startDestination = "phone_number_screen",
        route = "auth"
    ) {
        composable(route = "phone_number_screen") {
            // Get's a new instance of AuthViewModel
            PhoneNumberScreen(viewModel = getViewModel<AuthViewModel>())
        }

        composable(route = "otp_screen") {
            // Get's a new instance of AuthViewModel
            OTPScreen(viewModel = getViewModel<AuthViewModel>())
        }
    }
}

那么,在Jetpack Compose NavGraph中,我们如何在两个或多个组合中共享相同的ViewModel呢?
4个回答

25

您可以将顶层viewModelStoreOwner传递给每个目标

  1. 直接在.viewModel()调用中传递,在我的示例中为composable("first")
  2. 覆盖整个内容的LocalViewModelStoreOwner,因此CompositionLocalProvider内的每个合成都可以访问相同的视图模型,在我的示例中为composable("second")
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
    "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
    composable("first") {
        val model = viewModel<SharedModel>(viewModelStoreOwner = viewModelStoreOwner)
    }
    composable("second") {
        CompositionLocalProvider(
            LocalViewModelStoreOwner provides viewModelStoreOwner
        ) {
            SecondScreen()
        }
    }
}
在第二种情况下,您可以在组合树的任何级别内部通过CompositionLocalProvider获取模型。
@Composable
fun SecondScreen() {
    val model = viewModel<SharedModel>()
    SomeView()
}

@Composable
fun SomeView() {
    val model = viewModel<SharedModel>()
}

SaveStateHandle是否能够完成同样的工作?这样我们就可以在另一个屏幕中使用上一次保存的状态下的ViewModel了吗? - EliodeBeirut
@EliodeBeirut,你是不是指的是SavedStateHandle?它只包含导航项捆绑包,不能控制当前屏幕。我不确定它如何完成相同的工作。 - Phil Dukhov

12
使用 Hilt,你可以像下面这样做。但是由于你正在使用 Koin,我还不知道 Koin 的方式。
@Composable
fun MyApp() {
    NavHost(navController, startDestination = startRoute) {
        navigation(startDestination = innerStartRoute, route = "Parent") {
            // ...
            composable("exampleWithRoute") { backStackEntry ->
                val parentEntry = remember {
                  navController.getBackStackEntry("Parent")
                }
                val parentViewModel = hiltViewModel<ParentViewModel>(
                  parentEntry
                )
                ExampleWithRouteScreen(parentViewModel)
            }
        }
    }
}

官方文档:https://developer.android.com/jetpack/compose/libraries#hilt

2
这是 Google 推荐的方法,它运行得非常完美。只需考虑到“以这种方式创建的任何 ViewModel 对象都会一直存在,直到关联的 NavHost 及其 ViewModelStore 被清除,或者导航图从后堆栈中弹出。” - Nasser Ghodsian
@Rafiul,我该如何申请这个 https://stackoverflow.com/q/71942247/13614484? - user13614484

0

这里有另一种使用Koin的方法。

它严格执行与验证答案相同的操作,但编写起来更简单。它将具有完全相同的viewModelStoreOwner,而无需显式编写它。如果我错了,请告诉我。

val navController = rememberNavController()

val sharedViewModel = getViewModel()

NavHost(navController = navController, startDestination = "first") {
    composable("first") {
        // You can use sharedViewModel
    }
    composable("second") {
        // You can use sharedViewModel
    }
}

1
我认为与接受的答案不同的是,在Phil Dukhov的答案中,您可以直接在每个组合中获取相同的viewmodel实例。而使用您的方法,则需要将viewmodel实例作为参数传递给组合。 后者并不建议根据官方文档: “您永远不应将ViewModel实例传递给其他组合,只传递它们所需的数据和执行所需逻辑的函数作为参数。” - BenjyTec

0
这是处理这个问题最简单和高效的方法。
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(
    navController: NavController
): T {
    val navGraphRoute = destination.parent?.route ?: return ViewModel()
    val parentEntry = remember(this) {
        navController.getBackStackEntry(navGraphRoute)
    }
    return ViewModel(parentEntry)
}

然后从composefun中调用它。
composable("route") {
            it.sharedViewModel<ThoughtViewModel>(navController = navController).apply {
               RequestsScreen(this@apply)
            }
           
        }

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