Struggling with multiple backstacks with compose r...
# compose
a
Struggling with multiple backstacks with compose routes... What would be the correct way to navigate to a destination that is in another tab? Let’s say A, J, X is the bottom bar top-level destinations, and this is the graph:
Copy code
A -> B -> C
J -> K -> L
X -> Y -> Z
If app is in J, then it wants to go navigate to Y, how do I navigate to Y correctly? I just call
navController.navigate("Y")
, but then the bottom bar now behaves weirdly, tapping the J bottom bar does nothing. The bottom bar is showing because it’s logic for visibility is
currentDestination.hierarchy.any { it.route in bottomDestinations }
based on official docs and samples. Is there a better way?
i
You are in full control over the source of truth for what tab is selected if you don't want to use the navigation graph you are on as the source of truth (which is what the docs use)
a
Thank you for the quick response 🙏 I see. I do want to use the navigation graph as source of truth. If I want to respect the graph definition, the valid navigation paths for J would be: A, X (via bottom bar), and K. Is that correct? Trying to add more context, my UseCase is like a notification list with notifications containing deeplinks which can basically navigate anywhere in the app which supports deeplinks. Would explicit deeplinks via PendingIntent be the right solution here? Assuming below would be a lot more complicated as it’s going against the docs.
You are in full control over the source of truth for what tab is selected
i
So are you...trying to add Y to the back stack associated with A (where you are at right now)? Or are you trying to swap to the back stack of X, then show Y? Those are very different things
a
I imagine both could be valid use case within my context, but here let’s say I want to do the latter. Apologies if I have poor understanding how the back stack swapping works, I did try what you are describing I think, but it behaves weirdly, would you mind taking a little look at what may be wrong at my attempt at this?
Copy code
navController.navigate("Y_navgraph") {
    popUpTo("J_navgraph") { // specifically J, as A seems to work (probably because it's startDestination)
        saveState = true
    }
    launchSingleTop = true
    restoreState = true
}
i
You most certainly want to do the exact same thing that you do for your bottom nav, including popping up to the same place
a
I see. I hope you bear with me for a bit more as I think I’m getting really close to what I need. All my bottom bar basically have this logic:
Copy code
navController.navigate("Y_navgraph") {
    popUpTo(navController.graph.findStartDestination().id) {  // basically A
        saveState = true
    }
    launchSingleTop = true
    restoreState = true
}
Doing so I’m not sure the Up & Back button works correctly. I kind of expect it to go to
X
which is the parent of
Y
in the graph, or going back to
J
seems right as well. But it seems to go back to A (the start destination). Is this as expected or am I still missing something? It’s a bit weird to me that
Y
gets navigated to as the startDestination of the
X
tab, I kind of expected the startDestination to be there…
i
I think you need to be more specific about what your actual back stacks are when you start and where you want them to be when you finish. If you've never navigated to 'X' before, then the
restoreState = true
isn't going to do anything - there's nothing to restore. You've navigated to 'Y', so you get 'Y' as the only destination on that stack, exactly what you told NavController to do
What happens if you have navigated to 'X' before? Do you want to destroy that stack (and its state) before you navigate to a fresh copy of it, starting with 'X', then 'Y'? You'd need to explicitly tell the NavController that's what you want to do:
Copy code
// clear any previously saved stack on X
navController.clearBackStack("X")
// navigate to "X", swapping tabs
navController.navigate("X") {
  popUpTo(navController.graph.findStartDestination().id) {
    // Make sure to save the state of "J"
    saveState = true
  }
}
// Now add "Y" on top of X
navController.navigate("Y")
a
I see! TIL about
clearBackStack(route)
which helped me understand
saveSate
better. So
saveState
saved a back stack tagged to the current route (
J
) before doing the
navigate(route) { popUpTo(…) }
operation. Then
restoreState
must be trying to restore the back stack of
navigate(X)
IIUC. And in above snippet we don’t want to restore the backstack hence we don’t set
restoreState=true
. I’m curious if
clearBackstack
here is necessary given we are not restoring `X`’s back stack anyway.
i
you probably still want to clear it out so that any existing
ViewModel
or saved state associated with those previous destinations is properly cleaned up
👍 1
a
I see, do you mean that back stack remains available indefinitely and can still be accessed later even if I already navigated to
X
without restoring the state? assuming I don’t
saveState
from X again.
i
Yep, we talk a bit about that mental model of holding onto that stack separately in the deep dive blog post (in the context of fragments, but it works the same in Navigation): https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134
👀 1
🙏 1
So if you don't override the saved back stack (by saving over it), restoring it, or clearing it, it'll just keep being saved
👍 1
a
In my case, I do actually want to save the backstack (don’t want to reinit the ViewModel in X)
Copy code
navController.navigate("X") {
    popUpTo(navController.graph.findStartDestination().id) {
        saveState = true
    }
    launchSingleTop = true
    restoreState = true
}
navController.navigate("Y")
And this one works nicely for me. Pressing Back from Y goes back to X, then X back to A.
Thank you so much for helping clarify this! I’ve been eyeing migrating to multiple backstack for some time (ever since that issue ticket was created) and only got the chance to attempt this now. We have been doing single backstack for a long time so I am used to thinking with that model. I know your time is very valuable. I will share this to my team and try to experiment a bit more with different use cases.