Navigation popUpTo and PopUpToInclusive aren't clearing the backstack - android

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?

<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.

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
in popUpTo 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)

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.

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);

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

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 ).

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()
}
}
Back Stack

I faced a similar problem and my approach was simple. In the navigation graph, you have to designate the starting screen. Mine was:
app:startDestination="#id/webview"
It's called the start destination, it is the first screen users see when opening your app, and it's the last screen users see when exiting your app.
If you do not wish your login activity to be shown as you exit the app, just remove it as the start destination and use the fragment that you wish to show last in your case, It's the Email Login screen.
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav"
app:startDestination="#id/Email Login screen">
Also, make sure you override the onBackPressed() method from the host activity code as:
override fun onBackPressed() {
finish()
super.onBackPressed()
}
Now that you have removed the login fragment as the start destination, it's now not obvious what fragment will be shown first when the app opens.
Add a method to implement that in the host activity and call it from the oncreate(). In my case,i created initContent() to handle that logic. This was the code:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
if (savedInstanceState == null) {
initContent()
}
}
private fun initContent() {
if (isNetworkConnected()) {
navController.navigate(R.id.webView)
} else {
navController.navigate(R.id.noInternetFragment)
}
}
Hope this helps someone.

Related

How to pop through fragment stack in NavController without loosing "parent" state

I open some main fragment FragA, which then can open other fragments, that are added to the stack, but when I press back I show a DialogC, which should clear the stack and get me back to FragA, without loosing it's state, restore it from stack, rather then creating it - to recreate it I'll have to pass some arg through the whole stack.
I tried some configs with popUpTo in different places, and also used findNacController.popUpTo(with/out_aruments) or findNavController.navigate(R.id.action_dialog_c_to_frag_a) without destination defined in action, but pop can't find action in stack, navigate wants to recrete fragment when destination is defined, withou it cannot find pop action in stack (I/NavController: Ignoring popBackStack to destination frag_a as it was not found on the current back stack)
This is sample of my nav_graph:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<fragment
android:id="#+id/asdf"
android:name="SomeNaviFragment">
<action
android:id="#+id/action_asdf_to_frag_a"
app:destination="#id/frag_A" />
</fragment>
<fragment
android:id="#+id/frag_A"
android:name="FragA">
<argument
android:name="some_id"
app:argType="integer" />
<action
android:id="#+id/action_frag_a_to_frag_b"
app:destination="#id/frag_B" />
</fragment>
<fragment
android:id="#+id/frag_B"
android:name="FragB">
<action
android:id="#+id/action_frag_b_to_dialog_frag_c"
app:destination="#id/DialogFragC"/>
</fragment>
<dialog
android:id="#+id/DialogFragC"
android:name="DialogC">
<action
android:id="#+id/action_dialog_c_to_frag_a"
app:popUpTo="#id/frag_a"
app:popUpToInclusive="true"/>
</dialog>
</navigation>
In short - I wan't to go deeper from FragA through some fragments, but when the DialogC shows up in some point, I want to get back to FragA, to it's initial state. Is it possible to achive it without passing the creation arguments for FragA?
Some solution already tried, like: Navigate Back with Navigation Component with it's linked resources, but this didn't help at all.
I've used the article in your post to make this https://github.com/yoobi/backNavigation I hope it helps.
EDIT: you're looking for a combination of this and the article then.
The popUpTo attribute of an action "pops up" the back stack to a given destination before navigating. (Destinations are removed from the back stack.)
If the popUpToInclusive attribute is false or is not set, popUpTo removes destinations up to the specified destination, but leaves the specified destination in the back stack.
If popUpToInclusive is set to true, the popUpTo attribute removes all destinations up to and including the given destination from the back stack.
If popUpToInclusive is true and popUpTo is set to the app's starting location, the action removes all app destinations from the back stack. The Back button takes the user all the way out of the app.
You can also check the count of your backstack with : parentFragmentManager.backStackEntryCount

popUpTo seems not to work in Navigation Component

so I'm using the android navigation component and I have a problem (2.2.0-rc04 version).
I have a welcomeFragment(wF). From wF I want to navigate to loginSellerFragment(lSF) which is in a different navigation graph. I also don't want to remove wF from backstack ( popUpTo, popUpToInclusive) when navigating to lSF because a user might wanna go back to it.
<fragment
android:id="#+id/welcomeFragment">
<action
android:id="#+id/action_welcomeFragment_to_nav_onboarding_seller"
app:launchSingleTop="true"
app:destination="#id/nav_onboarding_seller" />
</fragment>
After navigating to lSF the backstack looks like this : wF lSF
We're on lSF now, after login we want to go to feedFragment(fF) which again is in a separate graph, but this time we want to clear all the backstack, because if a user is logged in and presses back he wants the app to exit, not to take him back to wF or lSF, so I used popUpTo="#id/loginSellerFragment popUpToInclusive='true" in the action from lSF to fF.
<fragment
android:id="#+id/loginSellerFragment">
<action
android:id="#+id/action_login_to_seller"
app:destination="#+id/seller" . //this is the graph that has as firstDestination, feedFragment
app:launchSingleTop="true"
app:popUpTo="#id/loginSellerFragment"
app:popUpToInclusive="true" />
</fragment>
So in the backstack in this moment should be only fF because we removed everything up to lSF(lSF included)
The problem
When I'm on fF and press back, the app doesn't close, instead it takes me to wF ( wF should have been popped off the backstack already)
What I've tried
I've tried instead of popUpTo="#id/loginSellerFragment popUpToInclusive='true" to use popUpTo="#id/welcomeFragment popUpToInclusive='true" and it worked fine, but I'm pretty sure that this is not how it should be done. What am I missing here guys? Am I building the backstack wrong?
Also I've tried adding popUpTo="#id/welcomeFragment popUpToInclusive='true" after navigating from wF to lSF , but this will break my user experience, because I don't want the app to exit when I'm still in the login process.
Please note that all of this fragments are in separate graphs.
To navigate I use FragmentDirections e.g : findNavController.navigate(WelcomeFramgentDirections.actionXtoY())
It's not easy to grasp how Navigation Component manipulates backstack when you are using popUpTo option.
The solution you mentioned in your question is correct:
You indeed should use popUpTo="#id/welcomeFragment" popUpToInclusive="true" instead of popUpTo="#id/loginSellerFragment" popUpToInclusive="true".
I will try to explain why.
When you launch your application, your backstack will be empty and welcomeFragment will be displayed.
When you navigate from welcomeFragment to loginSellerFragment, you will have welcomeFragment in your backstack.
Than if you login, you will navigate from loginSellerFragment to feedFragment, and in backstack you will have loginSellerFragment and welcomeFragment.
Since you used popUpTo="#id/welcomeFragment", aplication will start to pop (remove) fragments from your backstack until it reaches welcomeFragment. The welcomeFragment will be also removed since we used popUpToInclusive="true".
Backstack should behave like FILO (First In Last Out) stack, so it will remove fragments in this manner:
First the top fragment will be removed and that is loginSellerFragment.
Next, welcomeFragment will be the top fragment. Since we need to pop up fragments until we reach welcomeFragment this is where we stop, but welcomeFragment will be also removed because of popUpToInclusive="true" and your backstack will be empty.
If you try to navigate back from welcomeFragment, you will exit app because your backstack is empty.
I hope that this helps. You could also read more about stack data structure.
For me setting XML code didn't really solve the issue, had to use an extra line of code
findNavController()
.navigate(R.id.navigationFragment,
null,
NavOptions.Builder()
.setPopUpTo(R.id.splashFragment,
true).build()
)

Android Jetpack navigation A ->B->C->A

I couldn't find anything online so here I am. I'm using the jetpack navigation component and I want to navigate from fragmentA to fragmentB, and then fragmentB will navigate to fragmentC, but when pressing hw back button I want to go back straight to fragmentA. is this possible with the current release?
This can be done with adding popUpTo to your action where you move from B -> C.
<fragment
android:id="#+id/fragmentB"
android:name="com.ballboycorp.anappaday.navigationtest.FragmentB"
android:label="fragment_b"
tools:layout="#layout/fragment_b">
<action
android:id="#+id/action_fragmentB_to_fragmentC"
app:destination="#+id/fragmentC"
app:popUpTo="#+id/fragmentA" />
</fragment>
What that means is
From B move to C and when user clicks back button, move back to A.
You should navigation to C using that action instead of giving destination id.
button.setOnClickListener {
findNavController().navigate(R.id.action_fragmentB_to_fragmentC)
}
Visually this is how it looks
With the Navigation Component you can handle onBackPressed on fragments. In your onViewCreated of fragment C just add this line of code:
requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
view.findNavController().popBackStack(R.id.fragmentA, false)
}
})

Android NavController move to another host destination

I have a question regarding to NavController:
There is the scheme (sorry for my painting, hope it helps :D)
I have MainActivity and BottomNavigationView with 3 tabs: A, B, C.
When I tap to A it opens Fragment A1 and there is next button which opens Fragment A2.
In Fragment A2 there are buttons back, next, no problems with navigation here.
The problem is when I need to navigate from Fragment A2 to section B the same like a click on B in BottomNavigationView.
The problem is that it's different graph, how to switch them?
My ideas:
I found work-around: requireActivity().bottomBar.selectedItemId = R.id.graph_b but it's not good idea.
I would like to achieve it using navigation component. I was trying to do findNavController().navigate(R.id.graph_b), but it leads to crash:
java.lang.IllegalArgumentException: navigation destination
com.my.app.staging:id/graph_b is unknown to this NavController
How to make it using NavController ?
There is Google Example project with all architecture:
https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
And to simplify my question I've added a button in this project, where on click should opens different screen:
You graphs are defined in your Parent Activity and thats where you will be able to control them.
Your first way is actually the solution. Your graphs are defined within your activity. When you are inside any destinations inside your graph A (say A1, A2 etc.) they have no knowledge of the your other graphs B & C. The only way to get to the graph is through parent activity, and hence
requireActivity().bottomBar.selectedItemId = R.id.graph_b
The second way that you have tried will definitely not work because findNavController().navigate(R.id.graph_b) is used when you have nested navigation. In other words graph_b should be inside graph_a which is not your case.
That being said what you can do is just write
requireActivity().bottomBar.selectedItemId = R.id.graph_b
fancier. Instead of running inside your fragments, its better to run inside your activity.
// In your fragment
requireActivity?.moveToGraphB()
// and in your activity
fun moveToGraphB() {
bottomBar.selectedItemId = R.id.graph_b
}
Or more more fancier would be using SharedViewModel which I don't think is necessary.
In graph B you can add the line
where nav_graphA is the name of the graph you want to navigate to when the button is clicked. Then you can add
` <fragment
android:id="#+id/aboutFragment"
android:name="com.mycompany.aboutFragment"
tools:layout="#layout/fragment_about"
android:label="About" >
<action
android:id="#+id/action_aboutFragment_to_nav_graphA"
app:destination="#id/nav_graphA" />
</fragment>
`
to create the action to navigate when the button is clicked.

Navigation Component .popBackStack() with arguments

I have Two fragment. SecondFragment and ThirdFragment. Actually I use the Navigation Component for passing value between fragments. Like this:
SecondFragment:
val action = SecondFragmentDirections.action_secondFragment_to_thirdFragment().setValue(1)
Navigation.findNavController(it).navigate(action)
Here is how I read the value from the ThirdFragment:
arguments?.let {
val args = ThirdFragmentArgs.fromBundle(it)
thirdTextView.text = args.value.toString()
}
It's work fine. Now my stack is look like this:
ThirdFragment
SecondFragment
There is any option for pass value from the opened ThirdFragment to the previous SecondFragment with the new Navigation Component? (When ThirdFragment is finishing)
I know about onActivityResult, but If Nav.Component serve better solution than I want use that.
Thank you!
It's a bit late for this answer but someone may find it useful. In the updated versions of the navigation component library it is now possible to pass data while navigating back.
Suppose the stack is like this
FragmentA --> FragmentB.
We are currently now in FragmentB and we want to pass data when we go back to FragmentA.
Inside FragmentAwe can create an observer with a key:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
// Instead of String any types of data can be used
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")
?.observe(viewLifecycleOwner) {
}
}
Then inside FragmentB if we change its value by accessing previous back stack entry it will be propagated to FragmentA and observer will be notified.
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("key", "value that needs to be passed")
navController.popBackStack()
Just came across setFragmentResult(), pretty easy to use. The docs on this are here.
If you are navigating: Fragment A -> Fragment B -> Fragment A
Add this to fragment A:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener("requestKey") { requestKey, bundle ->
shouldUpdate = bundle.getBoolean("bundleKey")
}
}
Then in fragment B add this line of code:
setFragmentResult("requestKey", bundleOf("bundleKey" to "value to pass back"))
// navigate back toFragment A
When you navigate back to fragment A the listener will trigger and you'll be able to get the data in the bundle out.
What you are asking for is an anti-pattern. You should either
navigate to the second fragment again with the new values you would like to set
use the third fragment ins a separate activity and start it with startActivityForResult()
use a ViewModel or some kind of singleton pattern to hold on to your data (make sure you clear the data after you no longer need it)
these are some of the patterns that came to my mind. Hope it helps.
As described here:
When navigating using an action, you can optionally pop additional destinations off of the back stack. For example, if your app has an initial login flow, once a user has logged in, you should pop all of the login-related destinations off of the back stack so that the Back button doesn't take users back into the login flow.
To pop destinations when navigating from one destination to another, add an app:popUpTo attribute to the associated element. app:popUpTo tells the Navigation library to pop some destinations off of the back stack as part of the call to navigate(). The attribute value is the ID of the most recent destination that should remain on the stack.
<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"/>
</fragment>

Categories

Resources