Android Navigation - how to go two destinations deep?
Asked Answered
L

1

6

I have an app that manages Boxes. This app has a single activity and uses fragments as destinations. It has (among others) the following destinations/fragments:

  • Home (registered as topLevelDestination)
  • BoxesManagement (registered as topLevelDestination)
  • BoxEdit

From the home destination I want to offer a convenient link to create a new box. For that I want to link BoxEdit directly, but with BoxesManagement in the back stack.

Attempt 1 - Explicit Deep Linking

Since an intermediate destination will only land on the back stack when it is the start destination of a <navigation> element, I placed BoxesManagement (as start) and BoxEdit in a nested graph. The nested graph is in a separate file, but <include>d in the parent graph. Then I used:

findNavController()
    .createDeepLink()
    .setDestination(R.id.nav_edit_box)
    .createPendingIntent()
    .send()

This did work in the way that it got me to the desired destination with BoxesManagement in the back stack. However, it causes two issues:

  • The application is started again. At least that is what I assume happens, since the entire screen flashes very briefly. This behavior lets me assume that I am not supposed to be using deep links for in-app navigation. (This is not surprising as the documentation never hints at this use-case.)
  • When I go back to BoxesManagement, I see the hamburger menu icon (as expected). But when I use the drawer menu to navigate to Home, nothing happens.

Attempt 2 - Direct Navigation

findNavController()
    .navigate(R.id.nav_box_edit)

This does not work and simply fails with:

java.lang.IllegalArgumentException: Navigation action/destination <APP-PKG>:id/nav_box_edit cannot be found from the current destination Destination(<APP-PKG>:id/nav_home) label=Home class=<APP-PKG>.ui.HomeFragment 

I am not surprised, as Murat Yener from Google already pointed out that this is not supported.

Attempt 2b - Setting the Graph

From this question and answer I got the idea to set the graph on the nav controller before navigating.

findNavController().apply {
    setGraph(R.navigation.nav_graph_boxes)
    navigate(R.id.nav_box_edit)
}

This does work, but also causes two issues:

  • When following this navigation, the nav-controller's graph is now R.navigation.nav_graph_boxes instead of R.navigation.nav_graph. When I use a different path with single steps through BoxesManagement, I can also reach BoxEdit but with R.navigation.nav_graph as the controller's graph.
  • The same issue as above with deep linking, navigating to Home using the drawer menu does nothing. I believe this is actually a direct consequence of the first issue.

The proposed solution in the linked Q&A is to set the graph back to R.navigation.nav_graph when navigating back. I don't know where in the code to do that though, as I don't navigate back through any explicit action, but rather through the up-button and the drawer menu, which - as stated before - does not work anymore with this approach.

Attempt 3 - Implicit Deep Linking

As suggested by the official doc I decided to try implicit deep linking, even though that brings none of that sweet type-safety that I was promised when using the navigation component.

I added

<deepLink app:uri="android-app://my.app.url/boxes/{boxId}/edit"/>

to the nav_box_edit fragment definition and used

findNavController()
    .navigate(Uri.parse("android-app://my.app.url/boxes/0/edit"))

from the Home destination.

This got me to my destination, but again I have a problem with this:

  • It does not put BoxesManagement into the back stack (this is to be expected per the documentation).

Question

How do I properly navigate to a (nested) destination while putting another destination on the back stack before it?

Limnetic answered 8/9, 2022 at 17:16 Comment(2)
If you want to add two destinations to the back stack, why don't you call navigate() twice?Denazify
At first I thought this wouldn't work (see my answer below), but I figured it out. Maybe add a sentence or two about that to the documentation? In my mind navigate() meant: "Go here, now! Don't do anything else" I wrongly assumed that execution would just continue with the lifecycle of the target destination.Limnetic
L
4

Navigating two Destinations Deep

In order to create a back stack as desired, you just have to push all desired destinations on the stack:

findNavController().apply {
    navigate(R.id.nav_boxes_management)
    navigate(R.id.nav_box_edit)
}

This works only if all destinations are in the same nav graph. As specified in the question, this is not the case here, so a slightly different approach needs to be taken:

findNavController().apply {
    navigate(FragmentHomeDirections.actionBoxesManagement())
    navigate(FragmentBoxesManagementDirections.actionEditBox(0L))
}

Using the directions of the now-top-of-the-stack destination BoxesManagement, the navigation into the nested graph succeeds.

(Thanks to ianhanniballake for giving me the crucial hint!)

Navigating back to Home

This works, but I don't know why:

findNavController().apply {
    navigate(FragmentHomeDirections.actionBoxesManagement(), navOptions {
        popUpTo(R.id.nav_home) {
            inclusive = false
            saveState = true
        }
    })
    navigate(FragmentBoxesManagementDirections.actionEditBox(0L))
}

I figured it out by looking at the source code of NavigationUI, which effectively handles the drawer menu actions.

Limnetic answered 8/9, 2022 at 19:36 Comment(3)
When you say same nav_graph does it imply same navigation path, or same graph file? because I have some nested graphs but they are all part of the same path through directions.Boarding
By "same nav_graph" I mean the same graph file. If that is not the case, the first code snippet won't work, but the second will.Limnetic
Well it actually worked with nested graphs.Boarding

© 2022 - 2024 — McMap. All rights reserved.