Navigation popUpTo and PopUpToInclusive aren't clearing the backstack
AndroidKotlinNavigationAndroid JetpackFragment BackstackAndroid Problem Overview
I'm new to the Android Jetpack Navigation architecture. I'm trying it out on a new app. There's one activity and a few fragments, two of them are login screen and email login screen. I defined those fragments in my navigations XML. The flow of the app is as follows:
Login screen
→ Email Login screen
What I want is, after navigating to the email login screen, when I press back, the app exits. Meaning the back-stack for login screen is removed. I know login screens aren't supposed to work that way, but I'm still just figuring things out.
I followed the documentation from Google's Get started with the Navigation component. It said, using app:popUpTo
and app:popUpToInclusive="true"
is supposed to clear the backstack, yet when I press back on email login screen, it still goes back to login instead of exiting.
So, here's what I've tried.
nav_main.xml
<fragment android:id="@+id/loginFragment"
android:name="com.example.myapp.ui.main.LoginFragment"
android:label="@string/login"
tools:layout="@layout/fragment_login" >
<action
android:id="@+id/action_login_to_emailLoginFragment"
app:destination="@id/emailLoginFragment"
app:popEnterAnim="@anim/slide_in_right"
app:popExitAnim="@anim/slide_out_right"
app:popUpTo="@+id/emailLoginFragment"
app:popUpToInclusive="true"/>
</fragment>
<fragment android:id="@+id/emailLoginFragment"
android:name="com.example.myapp.ui.main.EmailLoginFragment"
android:label="EmailLoginFragment"
tools:layout="@layout/fragment_login_email" />
LoginFragment.kt
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding.emailLoginButton.setOnClickListener {
findNavController().navigate(R.id.action_login_to_emailLoginFragment)
}
return binding.root
}
I gave a click event to a button. In it, I used the Navigation Controller to navigate to the email login screen by giving it the action's ID. In the <action>
, there are app:popUpTo
and app:popUpToInclusive="true"
.
After reading the documentation over and over, as well as reading plenty of StackOverflow questions, I found those properties are supposed to remove my login screen off the back-stack. But they don't. The button does navigate to the email login screen, but when I press back, it still goes back to login screen instead of exiting the app. What am I missing?
Android Solutions
Solution 1 - Android
<action
android:id="@+id/action_login_to_emailLoginFragment"
app:destination="@id/emailLoginFragment"
app:popEnterAnim="@anim/slide_in_right"
app:popExitAnim="@anim/slide_out_right"
app:popUpTo="@+id/loginFragment"
app:popUpToInclusive="true"/>
Your popUpTo is going back to the email login, and then popping it because of the inclusive. If you will change the popUpTo to your login fragment, it will be navigated back to, and popped as well because of the inclusive flag, which will result in your desired behaviour.
Solution 2 - Android
These 2 lines make the trick works:
If you want to go from A to B and expect to finish A:
You need to call B with this action:
<fragment
android:id="@+id/fragmentA"
tools:layout="@layout/fragment_a">
<action
android:id="@+id/action_call_B"
app:destination="@+id/fragmentB"
app:popUpTo="@id/fragmentA"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/fragmentB"
tools:layout="@layout/fragment_b">
</fragment>
If you put log to your fragments you can see that fragmentA is destroyed after calling fragmentB with this action.
Solution 3 - Android
You can do it in XML just like this answer does, or you can also do it programmatically:
NavOptions navOptions = new NavOptions.Builder().setPopUpTo(R.id.loginRegister, true).build();
Navigation.findNavController(mBinding.titleLogin).navigate(R.id.login_to_main, null, navOptions);
Solution 4 - Android
> I write this answer for people who have not completely understood the
> way popUpTo
works and I hope its example helps someone because most
> examples for navigation are repetitive in most sites and do not show
> the whole picture.
In any <action>
if we write a value for app:popUpTo
, it means we want to delete some of the fragments from the back stack just after completing the action, but which fragments are going to be removed from the back stack when action is completed?
Its order is Last In First Out so:
- All fragments between the last fragment and the fragment defined in
popUpTo
will be removed. - And if we add
app:popUpToInclusive="true"
, then the fragment defined inpopUpTo
will also be removed.
Example: Consider fragments from A to G in a navigation graph like this:
A->B->C->D->E->F->G
We can go from A to B and then from B to C and so on. Consider the following two actions:
- An action E->F we write:
<action
...
app:destination="@+id/F"
app:popUpTo="@+id/C"
app:popUpToInclusive="false"/>
- And for F->G we write:
<action
...
app:destination="@+id/G"
app:popUpTo="@+id/B"
app:popUpToInclusive="true"/>
Then after going from E to F using the action E->F, the fragments between the last fragment (F) and C (which is defined in popUpTo
of E->F
) will be removed. The fragment C will not be removed this time because of app:popUpToInclusive="false"
so our back stack becomes:
A->B->C->F (F is currently on Top)
Now if we go to fragment G using action F->G :
all fragments between the last fragment(G) and B (which is defined in popUpTo
of F->G
) will be removed but this time the fragment B will also be removed because in F->G action we wrote app:popUpToInclusive="true"
. so back stack becomes:
A->G (G is on top now)
Solution 5 - Android
Let's say that your app has three destinations—A, B, and C—along with actions that lead from A to B, B to C, and C back to A. The corresponding navigation graph is shown in figure
With each navigation action, a destination is added to the back stack. If you were to navigate repeatedly through this flow, your back stack would then contain multiple sets of each destination (A, B, C, A, B, C, A, and so on). To avoid this repetition, you can specify app:popUpTo and app:popUpToInclusive in the action that takes you from destination C to destination A, as shown in the following example:
<fragment
android:id="@+id/c"
android:name="com.example.myapplication.C"
android:label="fragment_c"
tools:layout="@layout/fragment_c">
<action
android:id="@+id/action_c_to_a"
app:destination="@id/a"
app:popUpTo="@+id/a"
app:popUpToInclusive="true"/>
After reaching destination C, the back stack contains one instance of each destination (A, B, C). When navigating back to destination A, we also popUpTo A, which means that we remove B and C from the stack while navigating. With app:popUpToInclusive="true", we also pop that first A off of the stack, effectively clearing it. Notice here that if you don't use app:popUpToInclusive, your back stack would contain two instances of destination A
Solution 6 - Android
popUpTo
its to define the place that you want to go when you press back. If you set popUpInclusive = true
, the navigation skipe that place too ( in popUpTo
).
Solution 7 - Android
Sample: A -> B -> A
FragmentB.kt
Attempts to pop the controller's back stack
private fun popBackStackToA() {
if (!findNavController().popBackStack()) {
// Call finish on your Activity
requireActivity().finish()
}
}