Is there a way to reuse a viewmodel between severa...
# compose-android
j
Is there a way to reuse a viewmodel between several routes in a nested graph? If I clear my nav. back stack when navigation to another screen in the nested graph, a new instance of my viewmodel is created.
s
how are you creating the viewmodel and managing its lifecycle? this can typically be solved by using a dependency injection library to manage the viewmodels
j
I’m using Koin to DI the viewmodel.
I’m using the following code to reuse the VM
Copy code
@Composable
inline fun <reified T: ViewModel> NavBackStackEntry.sharedViewModel(
    navController: NavController,
): T {
    val parentGraph = destination.parent ?: return koinNavViewModel()
    val parentViewModelStoreOwner = remember(this) {
        navController.getViewModelStoreOwner(parentGraph.id)
    }
    return koinNavViewModel(viewModelStoreOwner = parentViewModelStoreOwner)
}
This works if I keep the previous routes in the backstack. If I clear backstack on navigation it fails.
i
What does "If I clear backstack on navigation" mean?
j
Copy code
navController.navigate(
    route = screen,
    navOptions = if (clearBackStack) {
        navOptions {
            // Pop up to the start destination of the graph to
            // avoid building up a large stack of destinations
            // on the back stack as users select items
            popUpTo(navController.graph.findStartDestination().id) {
                saveState = saveBackStackState
            }
            // Avoid multiple copies of the same destination when
            // reselecting the same item
            launchSingleTop = true
            // Restore state when reselecting a previously selected item
            restoreState = saveBackStackState
        }
    } else null)
I pass in a navOption that clears the back stack.
i
You wouldn't use that kind of code at all for navigating between sibling screens in the same graph, you'd only use that kind of code when changing between totally independent graphs (e.g., for separate bottom nav destinations)
j
Would you recommend I only call
navController.navigate(route = screen)
i
If you just want to add a screen to the current back stack, that's what you call, yep
j
What do I do, if I don’t want a particular screen in the stack to be navigable anymore? Essentially, I have a nested graph dedicated to Onboarding. Once the account is setup, there is a final “Success” screen and the users shouldn’t be able to navigate backwards anymore.
i
that's
popUpTo
without any of the rest of that
j
Something like this?
Copy code
fun navigateTo(
    screen: AWScreen,
) = navController.navigate(
    route = screen,
    navOptions = navOptions {
        // Pop up to the start destination of the graph to
        // avoid building up a large stack of destinations
        // on the back stack as users select items
        popUpTo(navController.graph.findStartDestination().id)
    })
Also, how should I hold onto my ViewModel so that it doesn’t get recreated?
i
I find that the best way to think about things is to write a before back stack and an after back stack, then it becomes really clear what you need to do to go from one to the other. Let's try an example where your graph looks like this (with a note that you shouldn't be using onboarding or login as the start destination of your graph in any case - search for 'login' in this room for more details and read the Conditional Navigation docs):
Copy code
NavHost(route = Root, startDestination = OnboardingGraph) {
  navigation<OnBoardingGraph>(startDestination = Login) {
    composable<Login>()
    composable<Register>()
    Composable<Success>()
  }
  navigation<HomeGraph>() {
    composable<Home>()
  }
}
So when you navigate to a destination, the navigation graphs are on the back stack too, so you start out at:
Copy code
Root -> OnboardingGraph -> Login
If you
navigate(Register)
, your back stack becomes
Copy code
Root -> OnboardingGraph -> Login -> Register
Similarly when you `navigate(Success)`:
Copy code
Root -> OnBoardingGraph -> Login -> Register -> Success
You'll note that here the
Login
and
Register
screens are still on the back stack, despite you being on
Success
. You could have avoided that by using
Copy code
navigate(Success) {
  popUpTo<Login> { inclusive = true }
}
Which means you want to pop up to
Login
and use
inclusive = true
to also pop
Login
, which would instead mean your back stack looks like
Copy code
Root -> OnboardingGraph -> Success
And then if you wanted to move to
Home
and pop the entire
OnboardingGraph
off at once, you could use
Copy code
navigate(Home) {
  popUpTo<OnboardingGraph> { inclusive = true }
}
which would put you at:
Copy code
Root -> HomeGraph -> Home
When you call
navController.getBackStackEntry
(which is what you really should be using instead of
getViewModelStoreOwner
- keep in mind that a
NavBackStackEntry
implements
ViewModelStoreowner
, so both APIs give you the same object back, but only
getBackStackEntry
supports the type safe APIs), what you are doing is finding the entry on the back stack
👍🏽 1
So you really shouldn't be using a generic version like what you are doing, but being explicit - use
navController.getBackStackEntry<OnboardingGraph>()
if you want to store your ViewModel in the
OnboardingGraph
Then, with the help of those before/after back stacks, it becomes pretty easy to tell when the ViewModel's state is going away - as soon as you pop the graph itself. The important thing to know is that popping the last destination in the graph will automatically pop the graph itself (we don't just let empty graphs hang around)
So if you are all within the
OnboardingGraph
, navigating from one to another sibling will keep the entire graph around. If you pop the entire graph / every destination in that graph off the back stack, then yep, you will lose the graph too
🙏🏿 1
j
I was able to figure out my navigation problems by correcting my call to
navController.navigate(…)
and slightly changing dependency on my “shared” VM. The final destination that required the previous sibling screens to no longer be navigable, didn’t actually need use the “shared” VM, I’ve given it its own VM and it works correctly. @Ian Lake Thank you for your explanation. This will help me in the future.
🙌 1