Hello I have a question related to deep links handling with compose navigation, where I couldn't get...
t
Hello I have a question related to deep links handling with compose navigation, where I couldn't get navigation to work as expected after deep linking to a destination that isn't a start destination. I have been stuck on this for hours and I would really appreciate any help!
The app has a single NavHost, which has 3 destinations with route named "a", "b" and "c"; "a" is the start destination. The standard way to navigate between the 3 desitinations within the app is through function with
route
parameter:
Copy code
//snippet
navigate(
    route = route, //route argument is "a" or "b" or "c"
    navOptions {
        popUpTo(graph.findStartDestination().id) {
            saveState = true
        }
        restoreState = true
        launchSingleTop = true
    }
)
If we navigate to "a" through a deep link, the app behaves perfectly as expected. The issue is, if we navigate to "b" or "c" (which are not the starting destinations) through deep link, then attempt to navigate to "a" through the "standard navigation function" above, we cannot navigate to "a" at all. * To debug this issue I tried changing the
navOptions
configuration I noticed that: 1. setting
launchSingleTop
to
false
does not fix the issue 2. setting
restoreState
to
false
fixes the issue, but that completely breaks the intended design. 3. setting
saveState
to
false
fixes the issue, but again that completely breaks the intended design. 4. setting both
saveState
and
restoreState
to
false
also fixes the issue, but again that completely breaks the intended design. I have no clue what went conceptually wrong here, any clues would help. Thanks again for your time!
For your convenience I have created a test project to simulate the above issue
The issue is also suspiciously similar to another one <http://(https://stackoverflow.com/questions/68456471/jetpack-compose-bottom-bar-navigation-not-responding-after-deep-linking|on stackoverflow>, which was a bug 2 years ago 🤔
i
Each bottom nav item should be its own nested
navigation
element, even if that nested graph only has a single destination in it - that's what tells the NavController to save a back stack for A before going to B, thus ensuring that you aren't just stacking everything on A's back stack (which is the behavior you're seeing now and why clicking on A isn't doing anything - you're already on the A back stack)
👀 1
t
Thanks for your help again @Ian Lake, but I genuinely don't understand what you mean; I think my understanding on navigation is completely wrong. 😟 I thought each
NavHost
has a single
NavController
(regardless of whether or not the underlying
NavGraph
involve nested graphs), and a
NavController
holds a single backstack. I don't understand what you mean by "tells the
NavController
to save a back stack for A before going to B". I suppose my previous understanding is wrong and a
NavController
can hold on to multiple back stacks? like one extra back stack for every single nested graph?
i
That
saveState
and
restoreState
are exactly the APIs that are saving a back stack and then later restoring that back stack, yes
t
sorry so a
NavController
can hold on to multiple back stacks?
t
thanks I'll definitely read through it
Copy code
"which is the behavior you're seeing now and why clicking on A isn't doing anything - you're already on the A back stack"
I don't understand why this caused the issue, with the simple
NavGraph
structure I suppose there is just a single stack? What has the deep link navigation changed?
i
Think about if you had a more complicated app with an A1, A2, A3, same with B1, B2, etc. When you navigate from A1 to A2, you'd not use the save and restore flags - that's just adding A2 to the current back stack. But when you use the bottom nav to go to B1, the popUpTo+save is what saves A2 into the A stack That's why when you reselect the A tab, B1 again gets saved and the restore flag puts you back on A2. When you deep link to A2 for instance, all NavController is doing is navigating to A1, then A2. There's no saveState being used here during that But when you deep link to B1, you don't want the same behavior as with A2 - what you actually want is A1 to be on its stack, then essentially do what your bottom nav does - use saveState while navigating to B1 so that the A stack is set up correctly if you want to switch back to it. How does NavController know when to just regular navigate to A2 vs saveState when you go to B1? It uses the fact that you've switched nested graphs. That's why A1, A2, etc. all need to be in one nested graph and B1, etc. need to be in a B nested graph, etc. Even if there's only one destination in each nested graph right now, you still need the nested graphs to get the right deep link behavior
t
Thanks Ian I will be sure to take your advice on having nested graphs for the destinations. You are also clearly very knowledgeable and experienced as you see through what I was trying to do in my test project (bottomnav) even though I didn't structure the test project that way 🙂
On the test project,
Copy code
NavHost(
    modifier = Modifier.weight(1f),
    navController = navController,
    startDestination = "a",
) {
    deepLinkTestingComposable("a")
    deepLinkTestingComposable("b")
    deepLinkTestingComposable("c")
}
As we deep link toward
b
,
NavController
navigates to
a
first as it is the starting destination, then navigates to
b
adding
b
to the current back stack. The back stack at this point is
b
stacked on top of
a
, and
a
on top of
null
. Next we call:
Copy code
navigate(
    route = a,
    navOptions {
        popUpTo(graph.findStartDestination().id) {
            saveState = true
        }
        restoreState = true
        launchSingleTop = true
    }
)
I now understand that this actually don't make sense architecturally as you have explained in your reply, yet I don't understand why we cannot navigate to
a
using this call, at all!
graph.findStartDestination().id
should resolve to the ID of
a
, and with `popUpTo`'s
inclusive
being false by default, I thought
b
would be popped. Yet
b
is not popped and the call does nothing as we remain on
b
... why is this the case?
i
You're restoring the back stack that was on top of A when you first called saveState - that stack is B. So you're saving B, then restoring B
t
Copy code
/**
     * Whether this navigation action should restore any state previously saved
     * by [PopUpToBuilder.saveState] or the `popUpToSaveState` attribute. If no state was
     * previously saved with the destination ID being navigated to, this has no effect.
     */
    @get:Suppress("GetterOnBuilder", "GetterSetterNames")
    @set:Suppress("SetterReturnsThis", "GetterSetterNames")
    public var restoreState: Boolean = false
but I thought
restoreState
controls whether the state of the destinations should be restored if possible, and not the stack itself? 🤔
i
It restores what was saved with saveState. And what is saveState attached to? The pop of one or more destinations
t
Thanks, I traced
restoreStateInternal
and I think I understand now 👌
318 Views