androidandroid-architecture-navigation

Navigate with PopUpTo but not knowing what you root destination is - Navigation Architecture Component


Using Navigation Architecture Component we also have dynamic navigation from code like this in some cases:

navController.navigate(R.id.cartFragment)

Then when we need to navigate to a new root destination(back navigation will close the app) from that destination, we have no way of knowing what our current root destination is, so we don't know what to set the popUpTo destination to.

val navigationOptions = NavOptions.Builder().setPopUpTo(R.id.???, true).build()
navController.navigate(R.id.loginFragment, null, navigationOptions)

If we set it to a destination not in the backstack, then we get the warning log (from popBackStackInternal in NavController):

"Ignoring popBackStack to destination *:id/splashFragment as it was not found on the current back stack"

The case happens because we also have multiple cases before in the flow, where we set PopUpTo so dependant on the flow we have different root destinations.

I have looked at all avaliable methods on NavController and tried some reflection on mBackStack, but I was not able to figure out a way to clear the backstack.

How to clear the backstack when not knowing the current root destination?


Edit: Navigation graph example added

<fragment
    android:id="@+id/navigation_cart"
    android:name="ProjectsFragment_"
    android:label="Cart"
    tools:layout="@layout/fragment_cart" />

<fragment
    android:id="@+id/loginFragment"
    android:name="LoginFragment_"
    android:label="Login"
    tools:layout="@layout/fragment_login">
    <--! Dynamic navigation to either Consent or Cart based on state -->
    <action
        android:id="@+id/action_login_to_home"
        app:destination="@id/navigation_cart"
        app:popUpTo="@id/loginFragment"
        app:popUpToInclusive="true" />
    <action
        android:id="@+id/action_loginFragment_to_consentFragment"
        app:destination="@id/consentFragment" />
    <action
        android:id="@+id/action_loginFragment_to_contactFragment"
        app:destination="@id/contactFragment" />
</fragment>

<fragment
    android:id="@+id/splashFragment"
    android:name="SplashFragment_"
    android:label="SplashFragment"
    tools:layout="@layout/fragment_splash">
    <--! Dynamic navigation to either Onboarding, Login or Cart based on state -->
</fragment>
<dialog
    android:id="@+id/consentFragment"
    android:name="ConsentFragment"
    android:label="ConsentFragment"
    tools:layout="@layout/fragment_consent" />
<fragment
    android:id="@+id/onboardingIntroFragment"
    android:name="OnboardingIntroFragment_"
    android:label="OnboardingIntroFragment"
    tools:layout="@layout/fragment_onboarding">
    <action
        android:id="@+id/action_onboardingIntroFragment_to_loginFragment"
        app:destination="@id/loginFragment"
        app:popUpTo="@id/onboardingIntroFragment"
        app:popUpToInclusive="true" />
</fragment>
<dialog
    android:id="@+id/contactFragment"
    android:name="ContactFragment"
    android:label="ContactFragment"
    tools:layout="@layout/fragment_contact" />

Regarding the concrete cases above from the SplashFragment we will dynamic navigate to either Onboarding, Login or Cart based on state. If that we navigate to Login and after success login navigate dynamic to Cart.

I both cases of the general dynamic navigation we do not know what our root destination is, and though can't set popUpTo correctly. When called from Splash it is SplashFragment, when called from Login it is LoginFragment, or another case in checkout it could be CartFragment or another fragment that is root destination.

How to figure out either dynamically what the root destination is or just to clear the backstack as a part of navigation?

Though this is a bit simplified as there is more cases of the same pattern in our app.

Edit 2: Root destination definition I define root destination as the destination to popUpTo with inclusive to clear the full backstack, så the backaction will close the app. For example Splash -> Login will clear splash with popUpTo, then Login to Cart should clear the rest of the backstack but now the root destination is not Splash but Login as Splash was popped when going to Login.


Solution

  • We ended up solving it in two ways.

    1) When we could figure out from our dynamic navigation location what the current root destination was we parsed that argument in to our dynamic Navigator.

    object Navigator {
        fun showStartFragment(navController: NavController, @IdRes currentDestination: Int) {
            val navigationOptions = NavOptions.Builder().setPopUpTo(currentDestination, true).build()
    
            if (!Settings.Intro.onboardingCompleted) {
                navController.navigate(R.id.onboardingIntroFragment, null, navigationOptions)
            } else {
                val isLoggedIn = Settings.User.loggedIn
                if (isLoggedIn) {
                    MainActivity.shared?.mainStateHandler?.showNextFragment(navController, currentDestination)
                } else {
                    navController.navigate(R.id.loginFragment, null, navigationOptions)
                }
            }
        }
    }
    


    2) But we also had the case where an asyn endpoint respone could result in, that the user must be logged out, do to expired token or the user has been blocked. To solve that we hacked the NavController to get the root destination with reflection.

    fun NavController.getRootDestination(): NavDestination? {
        val mBackStack = NavController::class.java.getDeclaredField("mBackStack").let {
            it.isAccessible = true
            return@let it.get(this) as Deque<*>
        }
        for (entry in mBackStack) {
            val destination = Reflector.getMethodResult(entry, "getDestination") as NavDestination
            if (destination !is NavGraph) {
                return destination
            }
        }
        return null
    }
    
    object Reflector {
        fun getMethodResult(`object`: Any, methodName: String): Any? {
            return `object`.javaClass.getMethod(methodName).let {
                it.isAccessible = true
                return@let it.invoke(`object`)
            }
        }
    }