FirstOne
FirstOne

Reputation: 6225

Navigation Component: jump to dynamic root destination and clear backstack

When using Jetpack Navigation, we can use popUpTo and popInclusive to clear the stack. But how do I clear the stack when I don't know what destination to popUpto?

Example

Say I have 3 main destinations that should have a clear stack when we arrive at them. And each main destination has its own flow of screens (which can be accessed directly with a deeplink).

Assuming the navigation flows downwards here:

Start app:
 - Main Dest 1 (nav to dest 2)
     - Dest 2 (nav to dest 3)
         - Dest 3 (nav to main dest 2)
            --clear--
 - (with clear stack)
   Main Dest 2 (nav to dest 4)
     - Dest 4 (nav to main dest 3)
         --clear-->
 - (with clear stack)
   Main Dest 3
 - Back button should close the app here

Since the navigation is very dynamic, I cannot guarantee the root destination. Even though I could, for example, pop up to "Main Dest 2", I cannot know that it's on the stack since I might have gone directly to "dest 4" from a URL.

I there a way of knowing which destination is the lowest so I can pop up to it and clear the stack?

Upvotes: 1

Views: 9064

Answers (4)

ianhanniballake
ianhanniballake

Reputation: 200130

The root of your graph is always on the back stack, so you can always popUpTo that destination.

So just make sure your root <navigation> element has an android:id associated with it:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph"
    app:startDestination="@+id/main_dest_1">
    <!-- The rest of your graph -->
</navigation>

Then whenever you want to pop the entire graph, just popUpTo="@id/nav_graph".

Of course, as per the Principles of Navigation, you should not be popping the whole stack off when you go to Main Dest 2, Main Dest 3, etc. as it is extremely important from a UX perspective that users know when the back button will exit the app - that start destination is exactly that sign post for users to know when that'll happen and prevents accidental closure of the app when users expect to return to the first screen before exiting your app.

Upvotes: 0

Bhanu Prakash Pasupula
Bhanu Prakash Pasupula

Reputation: 1002

You can add a destination change listener in root or base fragment and listen for navigation changes.

NavController.OnDestinationChangedListener { controller, destination, arguments ->
    // react on change or 
    //you can check destination.id or destination.label and act based on tha
)}

Upvotes: 0

azizbekian
azizbekian

Reputation: 62209

I couldn't find any official solution to the problem, but here's an approach that works for me:

private val mBackStackField by lazy {
  val field = NavController::class.java.getDeclaredField("mBackStack")
  field.isAccessible = true
  field
}

fun popToRoot(navController: NavController) {
  val arrayDeque = mBackStackField.get(navController) as java.util.ArrayDeque<NavBackStackEntry>
  val graph = arrayDeque.first.destination as NavGraph
  val rootDestinationId = graph.startDestination

  val navOptions = NavOptions.Builder()
    .setPopUpTo(rootDestinationId, false)
    .build()

  navController.navigate(rootDestinationId, null, navOptions)
}

Upvotes: 1

ich5003
ich5003

Reputation: 838

As far as I know there is no easy and beautiful way to get what you're trying to achieve. There are a few possible solutions though.


Use some sort of a base fragment

You could have a base-destination which is always at the bottom of your stack, let's call it baseDest. In your actions which need to clear the stack you could then always use

<action
    android:id="..."
    app:destination="@id/mainDestX"
    app:popUpTo="@+id/baseDest"
    app:popUpToInclusive="false" />

This will always keep your baseDest at the bottom of your stack allowing you to always popUpTo it. To achieve your behaviour on a backpress in one of your main destinations you could either:

  1. Override the onBackPressed-method in all your main dests to close the app. (Could be easily done if all your main destionations extend from the same class)
  2. When your baseDest fragment is loaded because of an backpress close the app programmatically.

Rethink your app navigation

Maybe you should rethink how the navigation in your app works. Rather than having some sort of possibly-cyclic graph structure it's easier to deal with tree-like navigation structures (which are also also more intuitively understood by users). Of course this is not always an option, but you should keep this in mind and consider restructuring your navigation.


Find a solution programmatically

Manually implement all your actions and navigations and use the methods provided in NavController to manually remove all entries in your stack before navigating to one of your main destinations. One possible approch here to do this is to retrieve your nav controller by using the findNavController method in your fragment. I have not tested any of the following methods, but you could try to:

  1. Execute yourNavController.getGraph().clear() followed by a yourNavController.navigate(yourDest)
  2. At the beginning of your application before any fragment is loaded use saved = yourNavController.saveState() and on navigation to one of your mainDestinations use yourNavController.restoreState(saved), again followed by a yourNavController.navigate(yourDest)
  3. Create a new NavGraph and use yourNavController.setGraph(createdGraph)

Upvotes: 2

Related Questions