https://kotlinlang.org logo
Title
j

Joseph Hawkes-Cates

05/02/2023, 4:19 PM
I’m seeing something weird with regards to my bottom nav bar and saved state. We have several screens which are hooked into our bottom nav bar and we save/restore state for these screens when navigating to them. We’re using
hiltViewModel()
and compose navigation to inject the VMs for these screens within the scope of what’s on the backstack. I’m seeing that these destinations whose state is saved do not call onCleared() on the VM when they are popped off the backstack. Has anyone else seen this and is it expected? 🧵
The state is saved as expected for these screens and the VM which works as expected, but because the VM isn’t cleared when it’s popped, I’m seeing flow collection that the VM is doing stick around after the fact.
So our VM collects a flow from a repository and every time I navigate to a different bottom bar destination and then back a new instance of the VM is created with the state restored from the previous instance, but the new instance has a new VM scope and starts a new collection of the repository flow.
this ends up leaking the collection jobs for this flow repeatedly and I’m not sure if I should be doing something else to avoid/detect this
i

Ian Lake

05/02/2023, 6:28 PM
a new instance of the VM is created with the state restored from the previous instance
These seem like opposites. Either the same VM instance is being reused (the expected behavior) or you get a brand new VM with no old state.
j

Joseph Hawkes-Cates

05/02/2023, 6:29 PM
yeah, sorry. I can see that’s confusing.
We are getting a new instance of the VM when we come back to the destination after it has been popped off the backstack. But I can see in my logs that the old instances are still collecting the flow from the repo
So anytime the repo flow emits, the log indicating the flow is collected happens once for everytime the VM has been instantiated
I also added a log in the onCleared() override to confirm it is never called
i

Ian Lake

05/02/2023, 6:33 PM
When you save a back stack (e.g., pass
saveState
to your
popUpTo
call), those VMs are going to still exist in memory (that's intentional, it is part of the state of those destinations). That's why all of our guides avoid collecting in the VM itself, but expose a Flow to the UI that stops collecting when the UI goes away (e.g., using
WhileSubscribed
on a
stateIn
call). The
restoreState
call as part of
navigate
is what brings those VMs back into the back stack. If you never
restoreState
, then yeah, those VMs are going to be there until you do
You can check the
navBackStackEntry.id
to get the unique ID associated with each entry - that's the unique ID the VMs are associated with. If you aren't getting the same ID back, then these are new instances of the same destination, not a restored one
j

Joseph Hawkes-Cates

05/02/2023, 6:36 PM
ok, thanks. I’ll check the ID out. We are setting restoreState to true whenever we navigate to this destination. Is the expectation that we would get the same instance of the VM every time we come back to it (within the same app process)?
i

Ian Lake

05/02/2023, 6:43 PM
Yes
j

Joseph Hawkes-Cates

05/02/2023, 6:48 PM
ok, confirmed that the backstack entry IDs are changing.
I’ll have to figure out why that is happening
My backstack is essentially going from
RootNavGraph > NavGraphA > Start-A
to
RootNavGraph > NavGraphB > Start-B
when switching between destinations in the bottom bar. So A and B are different items in the bottom bar. That navigation between items always looks like this:
navController.navigate(routeA) {
    launchSingleTop = true
    restoreState = true
    popUpTo(RootNavGraphName) {
         saveState = true
    }
}
The destination I’m looking at doesn’t have any nav arguments that would make the route different.
i

Ian Lake

05/02/2023, 7:26 PM
Does it work if you follow the documentation and use
popUpTo(navController.graph.findStartDestination().id)
?
j

Joseph Hawkes-Cates

05/02/2023, 7:44 PM
Good question, I’ll try it out
Behavior is the same when I do it that way except it pops up to the start destination of the root instead of the root itself which is fine, but it still is getting a new backstackEntry ID every time we come back to a bottom nav item.
So I debugged through what the nav controller is doing here and I think this might be an issue with how I’m using nested nav graphs. First I traced it to these lines in one of the navigate functions where it determines if it has state to restore and does the restore. My destination’s ID is never in that
backStackMap
// Starts at line 1695 in navigation 2.5.3
if (navOptions?.shouldRestoreState() == true && backStackMap.containsKey(node.id)) {
            navigated = restoreStateInternal(node.id, finalArgs, navOptions, navigatorExtras)
} else {
   // ...
}
Then I looked at where the state is saved in that map:
// Starts at line 585 in navigation 2.5.3, inside popBackStackInternal
if (savedState.isNotEmpty()) {
                val firstState = savedState.first()
                // Whether is is inclusive or not, we need to map the
                // saved state to the destination that was popped
                // as well as its parents (if it is the start destination)
                val firstStateDestination = findDestination(firstState.destinationId)
                generateSequence(firstStateDestination) { destination ->
                    if (destination.parent?.startDestinationId == destination.id) {
                        destination.parent
                    } else {
                        null
                    }
                }.takeWhile { destination ->
                    // Only add the state if it doesn't already exist
                    !backStackMap.containsKey(destination.id)
                }.forEach { destination ->
                    backStackMap[destination.id] = firstState.id
                }
                // And finally, store the actual state itself
                backStackStates[firstState.id] = savedState
            }
This section is called, but
savedState.first()
is always the intermediary nav graph for my destination rather than the screen destination itself. This is being set in
backStateMap
, but the other state in savedState which is for my destination that I try to navigate to does not appear to be used.
i

Ian Lake

05/02/2023, 9:18 PM
You should be navigating to the route's of the graph itself, yes
j

Joseph Hawkes-Cates

05/02/2023, 9:18 PM
I switched our navigation call for navigating between the bottom bar items to use the NavGraph as the route instead of the route for the start destination of that nav graph and then it worked as I expected and the backstack entry Ids are re-used and my “leaking” VM instances problem is resolved!
yeah, so we originally weren’t using nested nav graphs like this so these items were navigating to the destinations directly and we didn’t update that behavior when we added the nested nav graphs.
Thanks for the help!
i

Ian Lake

05/02/2023, 9:22 PM
Glad you were able to get it fixed!