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
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.
(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-scopedNavBackStackEntry
provides a way to create aViewModel
that's scoped to a navigation graph, enabling you to share UI-related data between the graph's destinations. AnyViewModel
objects created in this way live until the associatedNavHost
and itsViewModelStore
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)
...
}
}
}
}
Copied from ianhanniballake answer
This can also be achieved using an extension
@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()
}
navigate(secondNestedRoute, startDestination = nestedStartRoute) {
composable(route) {
val loginViewModel: LoginViewModel = it.parentViewModel(navController)
}
}