androidviewmodelandroid-jetpack-composedagger-hilt

How to share a viewmodel between NavGraph components (only)


I would like to share a viewmodel between many composables. Just like how we share a viewmodel between fragments within an Activity.

But when I try this

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") { ... }
        }
    }
}

I get an error

@Composable invocations can only happen from the context of a @Composable function

Need

  • The viewmodel should only be active in the NavGraph Scope.
  • When I go to a different route and come back I should initialize a new viewmodel (this is why I'm calling it in the NavGraph)

Almost similar solution

  1. Answer by Philip Dukhov for the question How to share a viewmodel between two or more Jetpack composables inside a Compose NavGraph?

    But in this approach the viewmodel stays in the scope of the activity that launched it and so is never garbage collected.


Solution

  • Solution 1

    (copied from the docs)

    The Navigation back stack stores a NavBackStackEntry not only for each individual destination, but also for each parent navigation graph that contains the individual destination. This allows you to retrieve a NavBackStackEntry that is scoped to a navigation graph. A navigation graph-scoped NavBackStackEntry provides a way to create a ViewModel that's scoped to a navigation graph, enabling you to share UI-related data between the graph's destinations. Any ViewModel objects created in this way live until the associated NavHost and its ViewModelStore are cleared or until the navigation graph is popped from the back stack.

    This means we can use the NavBackStackEntry to get the scope of the navigation graph we are in and use that as the ViewModelStoreOwner to get the viewmodel for that scope.

    Add this in every composable to get the BackStackEntry for login and then use that as the ViewModelStoreOwner to get the viewmodel.

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

    So the final code changes to

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

    Solution 2

    Copied from ianhanniballake answer

    This can also be achieved using an extension

    1. Get the current scope and get or create the viewmodel for that scope
    @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. Then simply use this extension inside your navigation graph
    navigate(secondNestedRoute, startDestination = nestedStartRoute) {
      composable(route) {
        val loginViewModel: LoginViewModel = it.parentViewModel(navController)
      }
    }