Jetpack Compose中的作用域状态

44

在所有应用程序中,状态始终存在这三个范围: States

使用Compose,可以通过以下方式实现“每屏幕状态”:

NavHost(navController, startDestination = startRoute) {
    ...
    composable(route) {
       ...
       val perScreenViewModel = viewModel()  // This will be different from
    }
    composable(route) {
       ...
       val perScreenViewModel = viewModel()  // this instance
    }
    ...
}

通过以下方式可以实现“应用状态”:

val appStateViewModel = viewModel()
NavHost(navController, startDestination = startRoute) {
    ...
}

但是对于"Scoped State"呢?我们如何在Compose中实现它?


如果你需要一个可行的解决方案;我目前使用的是 compose router Github - 2jan222
2个回答

49
这正是导航图作用域的视图模型所用的目的。
它包括两个步骤:
  1. 查找与要为其设置ViewModel的图形关联的NavBackStackEntry

  2. 将其传递给viewModel()

对于第一步,您有两个选项。如果您知道导航图的路线(通常应该知道),则可以直接使用getBackStackEntry:
// Note that you must always use remember with getBackStackEntry
// as this ensures that the graph is always available, even while
// your destination is animated out after a popBackStack()
val navigationGraphEntry = remember {
  navController.getBackStackEntry("graph_route")
}
val navigationGraphScopedViewModel = viewModel(navigationGraphEntry)

然而,如果你需要更加通用的方法,你可以通过目标页面本身的信息 - 其parent来获取返回栈条目:

fun NavBackStackEntry.rememberParentEntry(): NavBackStackEntry {
  // First, get the parent of the current destination
  // This always exists since every destination in your graph has a parent
  val parentId = navBackStackEntry.destination.parent!!.id

  // Now get the NavBackStackEntry associated with the parent
  // making sure to remember it
  return remember {
    navController.getBackStackEntry(parentId)
  }
}

这使您可以编写类似以下内容的代码:

val parentEntry = it.rememberParentEntry()
val navigationGraphScopedViewModel = viewModel(parentEntry)

当您使用嵌套导航时,parent目标将等于图形的中间层之一,而对于简单导航图,它将等于根图。

NavHost(navController, startDestination = startRoute) {
    ...
  navigation(startDestination = nestedStartRoute, route = nestedRoute) {
    composable(route) {
      // This instance will be the same
      val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
    }
    composable(route) {
      // As this instance
      val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
    }
  }
  navigation(startDestination = nestedStartRoute, route = secondNestedRoute) {
    composable(route) {
        // But this instance is different
      val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
    }
  }
  composable(route) {
     // This is also different (the parent is the root graph)
     // but the root graph has the same scope as the whole NavHost
     // so this isn't particularly helpful
     val parentViewModel: YourViewModel = viewModel(it.rememberParentEntry())
  }
  ...
}

请注意,您不仅限于直接父级:每个父级导航图都可以用于提供更大的范围。

1
当涉及到 NavHost 内的行为时,作用域 ViewModel 的 onCleared() 方法仅在该目标(或一组目标,如果您正在使用导航图作用域 ViewModel)从后退堆栈中弹出并永久销毁时才会被调用。 - ianhanniballake
这些实现有机会被纳入官方API吗? - Jim Ovejera
4
请注意,remember {navController.getBackStackEntry(parentId)} 可能会导致崩溃,并且现在会触发 Lint 警告(更多信息请参见此处)。解决方案是使用 backStackEntry 作为 remember 的键,例如:remember(navBackStackEntry) {navController.getBackStackEntry(parentId)} - Guerneen4
1
@Guerneen4,你从哪里获取正在与remember一起使用的navBackStackEntry - levi
1
@levi 它是从可组合的 DSL 内部传递的,即 NavHost(...){navigation(....){composable(...){navBackStackEntry -> ...}}} - Marco Antonio
显示剩余4条评论

7

来自Compose和其他库-Hilt文档

要检索与导航路由范围限定的ViewModel实例,请将目标根作为参数传递:

val loginBackStackEntry = remember { navController.getBackStackEntry("Parent") }
val loginViewModel: LoginViewModel = hiltViewModel(loginBackStackEntry)

不使用 Hilt 也可以完成相同的操作

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

这个代码片段实现了与 @ianhanniballake 相同的功能,但代码更简洁。

注意:导航图有自己的路由 =“Parent”

完整代码示例

使用 Jetpack Compose 和 Navigation 的 Scoped State 示例

// import androidx.hilt.navigation.compose.hiltViewModel
// import androidx.navigation.compose.getBackStackEntry

@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)
            }
        }
    }
}

5
请注意,remember {navController.getBackStackEntry(parentId)} 可能会导致崩溃,并且现在触发了 Lint 警告(更多信息请参见此处)。解决方案是使用 backStackEntry 作为 remember 的键,例如:remember(backStackEntry) {navController.getBackStackEntry(parentId)} - Guerneen4

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