Using the navigation library, we stack the same ro...
# compose
h
Using the navigation library, we stack the same route several times (but with different arguments) and then want to jump back to the first via popUpTo. We could use currentBackStack to search for the first route including the "base route" (i.e. the route without arguments) but currentBackStack is restricted and should not be used. Is there a clean solution for this?
m
You can subscribe to
navController.currentBackStackEntryAsState()
State, and then search for the base route(the one without arguments) in the current destination's hierarchy, and then navigate to that base route. Example code snippet:
Copy code
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val baseDestination = currentDestination?.hierarchy?.first { it.arguments.isEmpty() }

// ...

navController.navigate(baseDestination?.route) {
    popUpTo(baseDestination?.id) {
        // ...
    }
}
h
I didn't know about the
hierarchy
property. Looks good, will try, thanks!
@Johnny fyi
s
hierarchy
may not be what you want here though right? The hierarchy will just give you the chain of destinations that are above the current destination. That will be the root destination of your NavHost Then if you are inside a sub nav graph, it will be the route of that graph. And then all other nav graph routes that are above you still, if that destination is quite nested. If you want to find the ID of the top-most destination of that specific route, I think you’d need to check the the stack of the entries instead, and not only the hierarchy of the current one. So like inside
navController.currentBackStack
, that returns a
List<NavBackStackEntry>
and look through all of those, check if their destination.route is what you need and then get that ID to know where to pop up to? Something like
Copy code
navController.currentBackStack.value.firstOrNull { navBackStackEntry ->  
  navBackStackEntry.destination.route == "yourRoute"
}?.id
public val NavDestination.hierarchy
as the docs describe: “”" Provides a sequence of the NavDestination’s hierarchy. The hierarchy starts with this destination itself and is then followed by this destination’s [NavDestination.parent], then that graph’s parent, and up the hierarchy until you’ve reached the root navigation graph. “”" So it doesn’t look at your current backstack, and I think this is what you’re after in this scenario.
h
You're right. I've tried out the proposed solution and just wanted to comment that it doesn't work for us, because of the same reason you've just wrote down. We are currently using
currentBackStack
but as I've mentioned, this api is restricted and who knows when it will be private (like backQueue).
s
That’s true, I now noticed this has the
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
annotation. The
getBackStackEntry
function is so close to what you need here https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigati[…]x/navigation/NavController.kt;l=2414-2423?q=getBackStackEntry But it by itself does a
lastOrNull
call on the private
backQueue
. If there was another API exposed which let you receive all those options and pick the one that you need then that would already be enough (in your case it’d be
firstOrNull
instead). I wonder why this is not exposed to the public. Maybe time for a feature request? I’d definitely +1 that to see what the library owners think about exposing such an alternative API there.
With all that said though, I am now also noticing that the
popUpTo
function here https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigati[…]4-104?q=popUpTo&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport takes in an ID but it’s a
@IdRes
so maybe this is only meant to work for the situation where your destinations are defined in XML, and not in pure compose? Maybe there isn’t a way to pop to a specific destination which you can identify through an ID instead of the route in the compose version of navigation? Could be totally wrong here, but this @IdRes annotation does not give me a lot of confidence 😄
h
I think it's obvious that one would like to query the actual navigation history. Really don't understand why there isn't a non-restricted, public api for this. @Ian Lake wrote a comment here: https://issuetracker.google.com/issues/217465473#comment6 (in another context) but I wonder how the logic we want to apply can be done otherwise. 🤔
i
Navigation 2.6 specifically supports popping up to a route with arguments, rather than only with the route pattern, so you can just use that, no new APIs needed
Multiple copies of the same destination with different arguments and being able to pop up to a particular one based on arguments is exactly the use case covered by those additions
h
But we don't know the arguments of the route we want to pop up to. We just want to pop up to the first route of that kind, regardless the arguments.
i
Then just popBackStack with the route pattern multiple times until it returns false
(which is how you know when every instance has been popped)
h
Calling popBackStack in a loop and then navigating forward seems much less elegant to me than navigating forward and giving NavOptions with a matching popUpTo statement. Without looking at the implementation, I would have assumed that the popBackStack loop + navigate is significantly less performant and could lead to buggy animations or something. I mean, what's the point of popUpTo if we can only use it when we know the exact route we want to pop up to and can't find it programmatically? popBackStack is final - we can't decide beforehand whether or not to do the navigation.
i
It is no less efficient, and doesn't affect animations at all
I think you should know what you want to pop up to and you've made some possibly questionable decisions much earlier in the process if you don't. But you haven't shared those specifics, so who can tell
It may certainly be that we're in an XY Problem situation: https://en.m.wikipedia.org/wiki/XY_problem
If you'd like to take a few steps back and talk about your setup in very concrete terms from a requirements point of view and only after that talking through the solution you've made to solve those requirements, that might help ground the conversation and ensure that we're talking about your actual problem X and not your attempted solution Y
Like maybe your multiple copies of the same route should actually be all within their own nested graph. Then popping them all involves just popUpTo the graph itself (which inherently pops every screen in that graph)
h
To stay with the Y for a moment: Navigation is not always as simple and stringent as one would like it to be from a developer's perspective. Navigation can be much more dynamic and not follow a foreseeable path. Especially if the content and structure of the application is partly derived from a headless CMS and deep links are involved. As a developer, I find it an unnecessary restriction not to have the possibility to read out the navigation history. You talk about "questionable decisions" - even if this is the case, I would rather call it workarounds for shortcomings in the navigation lib.
s
Well you're literally given the space to explain your use case so that so many random people who you don't work with can help you find your solution yet you're like "let's stay with Y for a moment". Why? 😅 I'm super interested to see what the solution would be to your issue, but you need to help us (or Ian specifically) help you.
h
I just didn't have time to describe the specific use case. We have been using the currentBackStack to mark routes that have outdated data or need to update their data. For example, the stack is A > B > C > D, and in D you now want to signal (due to some interaction that changes the data) that A, B and C need to update the data when navigating back. The order A > B > C > D is not necessarily predetermined by us, but can result from links in the content coming from a CMS. Using the back stack, we mark these routes as outdated. Of course, you could use the normal way of returning results here, but that involves a lot of boilerplate and the solution we have now works quite well for us. As a side note, our persistence layer consists mainly of an HTTP cache. I.e. it is not enough to just update the data - we need to set an appropriate cache control header. And that's what we do, among other things, when the route has been flagged as mentioned.
i
Ah, that is a completely different problem altogether, not at all related to popping multiple instances of a route off the back stack
In a reactive app, you'd generally have each screen get its data via a reactive stream, such as a Flow provided by a data layer underneath (which could certainly just be a layer responsible for your http cache). In that kind of case, your screen D would tell the data layer to invalidate the set of data that needs to be fetched again, which would automatically cause the Flow for the other screens to emit the updated data once you go back to those screens
You wouldn't and shouldn't be doing anything at the Navigation layer to get that working
h
Yes, these are the two places where we use
currentBackStack
. Our ViewModels use UseCases from the KMM part and with the proposed solution we would have to emit in a flow which UseCase has to load new data. The UseCases aggregate data from one or more repositories. The repositories do not know which UseCases are using them, so we cannot do the emission there. It is also not possible in the UseCases, because the repositories are used by many UseCases, and a UseCase does not know which other UseCases are using the same repository. This leaves the ViewModel, but the ViewModel does not know which UseCases have been used by the ViewModels in the back stack. Of course, a solution could be found, but simply "invalidating" the backstack seemed simple and effective.