Scoping States in Jetpack Compose

This is precisely what navigation graph scoped view models are used for.

This involves two steps:

  1. Finding the NavBackStackEntry associated with the graph you want to scope the ViewModel to

  2. Pass that to viewModel().

For part 1), you have two options. If you know the route of the navigation graph (which, in general, you should), you can use getBackStackEntry directly:

// 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)

However, if you want something more generic, you can retrieve the back stack entry by using the information in the destination itself – its 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)
  }
}

Which allows you to write something like:

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

While the parent destination will be equal to the root graph for a simple navigation graph, when you use nested navigation, the parent is one of the intermediate layers of your graph:

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())
  }
  ...
}

Note that you are not limited to only the direct parent: every parent navigation graph can be used to provide larger scopes.

Leave a Comment