popUpTo seems not to work in Navigation Component - android

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

Related

Android navigation component with popupTo and popUpToInclusive onResume not working

I'm trying to figure out how the navigation component works. Here's my fragments, SignIn Fragment and HomeFragment. In my navigation graph I have the following
<fragment
android:id="#+id/signInFragment"
android:name="com.nitsua.chatapp.screen.authentication.SignInFragment"
android:label="SignInFragment"
tools:layout="#layout/fragment_signin">
<action
android:id="#+id/navigate_to_home"
app:destination="#id/homeFragment"
app:popUpTo="#id/signInFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/homeFragment"
android:name="com.nitsua.chatapp.screen.home.HomeFragment"
android:label="HomeFragment"
tools:layout="#layout/fragment_home"/>
If I understand is correct, when I use the action: navigate_to_home, it will clear the fragment backstack until signInFragment including it because of popUpToInclusive set to true. If it is not set to true. SignInFragment will not be removed in backstack.
I tried signInFragment -> homeFragment, then homeFragment and back button. It works fine. It doesn't go to sign in and the app goes out of foreground. Then I try to open again the app from background. I expect the app to be in HomeFragment but it is in SignInFragment. The fragment backstack should store the HomeFragment transaction, is that right? then when it resume, it should resume the HomeFragment. but why it resumed the SignInFragment? Am I missing something? Can someone explain to me and recommend a way to resume to HomeFragment instead on SignInFragment?
Thank you.
You could put the signInFragment as an Activity. Check in your MainActivity if you have to show the signInActivity. If yes then start the activity.
Or you could start your homeFragment and right after that start the signInFragment if log in is required.
Then use this in your signInFragment.
navController.popBackStack(R.id.homeFragment, false)
Will pop your backstack till it reaches the first instance of homeFragment.
I ran into problem when start using navigation component but unfortunately Navigation component not provide any solution yet to call onResume when backStack or popup to another fragment .It reCreate the backfragment again when press back button.
My solution:
My data is in repository (Either getting from local Db or remote ).
Then create a LiveData in ViewModel and change his value false to true when first time load data from Api or Database .After then when user press back button check from liveData if value is true then get data from viewmodel and not calling the api.
private val localOrRemoteData=MutableLiveData()
fun setLocalOrRemoteData(status:Boolean){
localOrRemoteData.value=status
}
fun getLocalOrRemoteData():MutableLiveData<Boolean>{
return localOrRemoteData
}
getLocalOrRemoteData().observeOnce(lifecycleOwner) {
temp = it
}

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 popUpTo and PopUpToInclusive aren't clearing the backstack

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.

No view found for id when popping backstack without adding to backstack

I am working on an Android app and have encountered an error involving Fragments and FragmentTransactions. I have created an example app to demonstrate the problem. Here is what I am doing:
add Fragment1 to fragmentSpace, without adding to backstack.
replace fragmentSpace with Fragment2, adding to backstack as "beginning".
add Fragment3 to fragmentSpace2 (which is inside of Fragment2), without adding to backstack.
replace fragmentSpace2 with Fragment4, adding to backstack as null.
Call getFragmentManager().popBackStack("beginning", FragmentManager.POP_BACK_STACK_INCLUSIVE); in an attempt to undo all the Transactions, which should bring the user back to seeing only Fragment1.
However, the app crashes on step 5 with this error:
java.lang.IllegalArgumentException: No view found for id 0x7f090004 (me.MitchT.testfragmenttransactions:id/fragmentSpace2) for fragment Fragment3{7f35cb6 #2 id=0x7f090004}
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:886)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1067)
at android.app.BackStackRecord.popFromBackStack(BackStackRecord.java:1645)
...
I have found that if I add step 3 to the backstack, step 5 no longer crashes the app and there is no more error. Why?
The problem I have with adding step 3 to the backstack is that I want the user to be able to press the back button after Fragment3 is added and go back to Fragment1. If I add it to the backstack, then when the user presses the back button it removes Fragment3, but Fragment2 is still visible, so they must press the back button again. I am trying to avoid that double-back-button behavior.
I did not feel it was necessary to post the code for every single file in the example app on this question, so instead I created a gist.
So I guess my questions are:
Why do I get this error?
Why don't I get the error after adding step 3 to the backstack?
Is there a better way of "going back to the beginning" or going back two fragments at a time?
Thanks!
From what i understand. You have a fragment layout which is the container in activity
Activity hosts Fragment1. Then you replace Fragment 1 by Fragment 2 ( added to back stack.
At this stage clicking back button you will have fragment2 popped and you see the activity which hosts fragment1.
Fragment2 has a framelayout which holds fragment3. Then you have Fragment4 replacing fragment3 which is added to back stack.
Now clicking the button you have getFragmentManager().popBackStack("beginning", FragmentManager.POP_BACK_STACK_INCLUSIVE);. But there is no view with that id.
Just use popBackStack(). Fragment4 --> Fragment2 ( hosting fragment3) --> Fragment1 ( hosted by the activity).
If you want Fragment4 to Activity hosting Fragment1 don't add Fragment4 to the back stack.
Note : In case you want nested fragments you need to use getChildFragmentManager(). https://developer.android.com/about/versions/android-4.2.html
Also note the discussion in the comments sections and the link posted by op Is this the right way to clean-up Fragment back stack when leaving a deeply nested stack?

How to pop back stack for Activity with multiple Fragments?

Assume I have an Activity which contains two FrameLayouts (let's call them FrameA and FrameB) which in turn each contain a Fragment (let's call them FragmentA1 and FragmentB1 respectively). Now, I commit a series of individual fragment transactions using code similar to the following...
getFragmentManager()
.beginTransaction()
.replace(frameId, fragment)
.addToBackStack(null)
.commit();
... such that I replace FragmentA1 in FrameA with FragmentA2, then I replace FragmentB1 in FrameB with FragmentB2, then I replace FragmentA2 in FrameA with FragmentA3, then I replace FragmentB2 in Frame2 with FragmentB3, and the final state looks like the picture above (where only FragmentA3 and FragmentB3 are visible).
If I understood correctly how the back stack works, pressing 'back' will interleave popping of the Fragments between FrameA and FrameB (reflecting how I added them).
Does anyone know if it is possible to pop the last transaction on FrameA or FrameB selectively? (i.e. if I pressed 'Pop FrameA' then FrameA would be transitioned back from FragmentA3 to FragmentA2 and, instead, if I pressed 'Pop FrameB' then FrameB would be transitioned back from FragmentB3 to FragmentB2)
Supplement: I know I can get the Fragment last added to a given FrameLayout using the FragmentManager.findFragmentById(int framelayoutId) method, but calling FragmentTransaction.remove(fragment).commit() only removes the Fragment from the View and does not transition the View back to the Fragment it previously displayed.
Basically, no, there is only one back stack for an activity.
You will just need to implement your own separate back stacks.
As of Android 4.0 (and the associated support library) there are APIs that should make this relatively easy -- FragmentTransaction.detach(Fragment) lets you put a fragment into the same state it is when in the back stack, and FragmentManager.saveFragmentInstanceState(Fragment) lets you go further and completely throw away the Fragment object. Not coincidentally, these are used to implement ViewPager's FragmentPagerAdapter and FragmentStatePagerAdapter, respectively, so you could look at the code for these as an example of how to use them.
FragmentManager.popBackStack(String name, FragmentManager.POP_BACK_STACK_INCLUSIVE)
Here is the simplest answer, and the explanation is very clear: Well there are a few ways to go about this depending on the intended behavior, but this link should give you all the best solutions and not surprisingly is from Dianne Hackborn...

Categories

Resources