7

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.

Stefano Sansone
  • 2,377
  • 7
  • 20
  • 39
clamentjohn
  • 3,417
  • 2
  • 18
  • 42
  • See [this answer](https://stackoverflow.com/a/64961032/3585796) – Phil Dukhov Oct 20 '21 at 08:56
  • Compose Navigation is based on Jetpack Navigation, so everything in this doc should be valid for Compose. And that answer is using Compose code – Phil Dukhov Oct 20 '21 at 09:14
  • An alternative solution to Navigation Compose is Jetmagic. It has great support for sharing viewmodels: https://github.com/JohannBlake/Jetmagic – Johann Oct 20 '21 at 11:12
  • @AndroidDev is this multiplatform friendly? Else why use a non-jetpack library for android development? We also use Hilt which is build with android in mind and has interop and docs for compose – clamentjohn Oct 20 '21 at 11:18
  • @clmno It's open source and written entirely in Kotlin, so it should be multi-platform friendly. Sure beats the hell out of Navigation Compose, as it even lets you create responsive layouts in a way similar to how xml layouts are done under the older view system.. – Johann Oct 20 '21 at 11:21
  • @AndroidDev Ah, no disrespect. It's just that it would be hard to maintain if the project ceases / stops updating. I'll check it out :) – clamentjohn Oct 20 '21 at 11:25
  • @clmno You mean like Google's YouTube video player for Android? They haven't updated that in 6 years and it cannot be used in an Android app written for Compose - Not to mention that Google doesn't even bother to release the source code for the player. In the end, Android devs are screwed right now to play YouTube videos in Compose. So the argument about not being maintained seems kind of lame given that Google themselves fail to maintain their own APIs. – Johann Oct 20 '21 at 11:28

1 Answers1

9

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)
  }
}
clamentjohn
  • 3,417
  • 2
  • 18
  • 42