Fragment Transaction is added to backstack even if disallowAddToBackStack is called - android

Here is a sample code to repo this bug:
If I replace 3 fragments in a row, and disable the second one to be added into the backstack.
fragmentManager = supportFragmentManager
val fargmentA = FragmentA()
fragmentManager.beginTransaction().replace(R.id.container, fargmentA).addToBackStack("a").commitAllowingStateLoss();
val fargmentB = FragmentB()
fragmentManager.beginTransaction().replace(R.id.container, fargmentB).disallowAddToBackStack().commitAllowingStateLoss();
val fargmentC = FargmentC()
fragmentManager.beginTransaction().replace(R.id.container, fargmentC).addToBackStack("c").commitAllowingStateLoss();
After the above transaction, I called popBackStack():
val count = fragmentManager.backStackEntryCount;
for(i in 0 until count ){
fragmentManager.popBackStackImmediate()
}
I can still see the onCreateView() method is called from FragmentB which I disallow add to backstack.
Is this a known bug or just how fragment manager behaves?
Thanks!

There's two main things to keep in mind:
replace() is the equivalent to calling remove() on every fragment currently added to that container, then calling add() with your new fragment, so that transaction with fragmentC is going to remove fragmentB as part of its operation
popBackStack() puts you exactly in the state you were in before the transaction. This means that fragmentC gets removed (the opposite of add()) and fragmentB gets added (the opposite of remove())
This means that it is expected that popping your fragmentC transaction will bring fragmentB back - your disallowAddToBackStack() just means that your fragmentB transaction can never be reversed.
This actually has some serious implications when it comes to mixing addToBackStack() and non-addToBackStack() fragment transactions on the same container: after you do your fragmentB transaction, there's no way to get back to fragmentA.
Generally, this means that if that was actually want you wanted, you replace your non-back stack fragmentB transaction with
fragmentManager.popBackStack()
val fargmentB = FragmentB()
fragmentManager.beginTransaction().replace(R.id.container, fargmentB).addToBackStack("b").commitAllowingStateLoss();
This would ensure that all transactions use addToBackStack() and the reversal works as expected at each step.
Note that every one of your transactions should use setReorderingAllowed(true) as per the Fragment transaction guide. This will prevent cases where fragments are moved up to higher lifecycle levels then moved immediately back down and ensures that combined operations like a popBackStack() and commit() together run as a single atomic transition.

Related

Android onBackPressed adds Fragment instead of replacing

In my app, I have a TopActionBar fragment that is loaded on the MainActivity that loads a MaterialToolbar, along with my navigation drawer. I have a FrameLayout in this fragment that I replace with fragments to navigate between pages. When I replace a fragment (using a function I have defined in a utils.kt file), I am tracking the fragments that are loaded for the first time and adding them to the BackStack so that I can pop them and prevent duplicates of that fragment from being added to the BackStack. Here is the relevant logic for how that is being managed in my Utils.kt file:
fun replaceFragment(destinationFragment : Fragment,
currentFragment: String,
title : String,
initialLaunch: Boolean = false
) {
val destinationFragmentName = destinationFragment.javaClass.simpleName
val fragmentTag : Fragment? = fragmentManager.findFragmentByTag(destinationFragmentName)
if(destinationFragmentName !== currentFragment || initialLaunch) {
val fragmentTransaction = fragmentManager.beginTransaction()
// some logic to determine animations depending on the fragment being replaced
if (fragmentTag == null) {
fragmentTransaction.replace(R.id.frame_layout, destinationFragment, destinationFragmentName)
fragmentTransaction.addToBackStack(destinationFragmentName)
} else { // re-use the old fragment
fragmentTransaction.replace(R.id.frame_layout, destinationFragment, destinationFragmentName)
}
fragmentTransaction.commit()
}
}
And then this is how I have overwritten the onBackPressed function in my MainActivity:
override fun onBackPressed() {
if (fragmentManager.backStackEntryCount > 0) {
fragmentManager.popBackStackImmediate()
} else {
super.onBackPressed()
}
}
A couple of things aren't working properly. Take this example flow of fragments below:
A -> B -> C -> B -> C
When I press back I get this flow:
BC -> AC -> App Close
Here multiple Fragments are being displayed at the same time.
What I expect to happen is:
C -> B -> A -> App Close
Can someone maybe offer some insights into why this is occurring and what I can do to fix this? If I don't conditionally addToBackStack, and just addToBackStack for every single Fragment I replace, it works fine, but I don't want the multiple copies in the BackStack. I need to keep the most recent instance of each Fragment in the BackStack. So in my example:
A -> B -> C -> B -> C
The BackStack would no longer have the first C, just the most recent one.
As per the FragmentManager guide:
When you call addToBackStack() on a transaction, note that the transaction can include any number of operations, such as adding multiple fragments, replacing fragments in multiple containers, and so on. When the back stack is popped, all of these operations are reversed as a single atomic action. If you've committed additional transactions prior to the popBackStack() call, and if you did not use addToBackStack() for the transaction, these operations are not reversed. Therefore, within a single FragmentManager, avoid interleaving transactions that affect the back stack with those that do not.
So what you are experiencing is the operation you did with addToBackStack being reversed (causing your original copy of B to reappear) while not touching the new C you did not use addToBackStack.
The FragmentManager's back stack is just that: a stack. That means you can't remove B from the stack unless it is at the top of the back stack. That means there's no way to remove B from the middle of the stack without using something like the support for multiple back stacks to completely swap between independent back stacks.
If you just want to make sure there is only one copy of B on the top of the stack, you'll want to popBackStack() to remove the topmost if the names are the same before unconditionally using addToBackStack() on your new instance.

Why after transaction to second fragment landscape mode showing me first transaction again?

I have 2 fragments. My first fragment have button which leads me to second fragment. It has this code:
binding.btnGet5Days.setOnClickListener {
val forecastFragment = ForecastFrag()
val transaction: FragmentTransaction =
parentFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, forecastFragment)
transaction.addToBackStack(null)
transaction.commit()
}
In my MainActivity i have this code:
val cityFragment = CityFrag()
val fm: FragmentManager = supportFragmentManager
fm.beginTransaction()
.add(R.id.fragment_container, cityFragment)
.commit()
Fragments are in FragmentContainer,
The problem is when i'm joining second fragment through this button and turn my phone into landscape mode, my first fragment layering to my second fragment. How can i solve it? :)
It's hard to know without seeing your full code, but it's possible the code you posted from MainActivity is adding a fragment on top of the existing stack. When you rotate the device, the Activity is destroyed and recreated, but the FragmentManager maintains its state so you don't lose everything. If your recreated activity code always adds a new fragment instance, you'll end up with what was already there, plus another CityFrag on top
The official recommendation is to use the Jetpack Navigation library, which will handle all that for you. If you don't want to go that far right now, you'll have to do your own checking and creation logic.
One thing you can do is to check if the savedInstanceState Bundle passed into your activity's onCreate is null - if it is, then this is a fresh start, and you can initialise with your first fragment. If it's not null, then your app is being recreated from some saved state, so you should probably let the FragmentManager take care of restoring itself and its back stack.
Otherwise take a look at FragmentManager - there's a bunch of useful methods like getBackStackEntryCount, findFragmentByTag etc. that you can use to work out what state your fragments are in, and if you need to add one or not. Depends on your code!

onPause() and onResume() order when replacing Fragments

I have 2 Fragments that I transition using this code:
private void switchTo(Fragment frag) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
transaction.replace(R.id.contentPanel, frag);
transaction.commit();
}
When I transition between the 2 Fragments, I would need to do some cleaning up in between. I put this cleanup code in the onPause() method of the fragment. The problem now is that onResume() of the 2nd Fragment takes place before onPause() of the 1st Fragment
I put a print statement in each fragment's onPause(), onResume(), and onStop() of both Fragments and this is the order it spits it out.
FRAGMENT 2 RESUMED
FRAGMENT 1 PAUSED
FRAGMENT 1 STOPPED
Is there a way to coordinate these 2 fragments such that I can clean up in between each transition?
Perform transaction.setAllowOptimization(false) before commiting it.
Google introduced optimizations, which are by default turned on in latest versions of support libs.
From docs:
The side effect of optimization is that fragments may have state changes out of the expected order. For example, one transaction adds fragment A, a second adds fragment B, then a third removes fragment A. Without optimization, fragment B could expect that while it is being created, fragment A will also exist because fragment A will be removed after fragment B was added. With optimization, fragment B cannot expect fragment A to exist when it has been created because fragment A's add/remove will be optimized out.
Those optimizations sometimes break logics that we as users of that API are dependent of.

android: return to specific Fragment (and pop off everything after it)

Let's say in my app there are a few possible navigation flows (all are Fragments)
A -> B -> C -> D -> E
A -> F -> B -> C
I'd like to be able to return to fragment B regardless of the transaction backstack depth (ie. I don't want to keep track if I'm currently showing E or C). I noticed it's possible to tag the fragments, but the following code doesn't seem to work:
In fragment A create fragment B aka SocialViewFragment:
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
// Method 1
transaction.add(R.id.fragment_container, frag, SocialViewFragment.FRAG_TAG).commit();
// Method 2
//transaction.replace(R.id.fragment_container, frag);
//transaction.addToBackStack(SocialViewFragment.FRAG_TAG).commit();
Then in Fragment E, popBackStack returns false (and does nothing), cause it can't find the tag?!
FragmentManager mgr = PlaybackFrag.this.getActivity().getSupportFragmentManager();
if (mgr.getBackStackEntryCount() > 0) {
// Want to go back to SocialViewFragment !!!
mgr.popBackStack(SocialViewFragment.FRAG_TAG, 0); // returns False - can't find the tag!
}
It looks as though you are confusing two different types of tags.
The optional String parameter you can pass to add() is a tag for the Fragment that allows you to later find the same Fragment by calling findFragmentByTag().
The optional String parameter passed to addToBackStack() and popBackStack() is referred to as a "name" and is used to identify a particular transaction in the FragmentManager's back stack. It is not a Fragment tag because a back stack entry represents a particular transaction that could have multiple Fragment additions or removals.
To utilize the back stack names correctly, make sure you call addToBackStack() with a non-null String, then later you can call popBackStack() with the same String to pop to that particular transaction.
Also note that in your add() call you aren't calling addToBackStack() at all. Because of this, mgr.getBackStackEntryCount() will be 0 and your popBackStack() call will never happen at all (unless you have added other Fragments to the back stack).
In order to popBackStack you should addToBackStack first.
Use the method that is commented in your code:
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, frag);
transaction.addToBackStack(SocialViewFragment.FRAG_TAG).commit();

Fragment which is not top most in backstack is resumed

Given the application flow show in the graphic and textually described in the following.
Fragment 1 is the lowest fragment but not in the backstack by setting disallowAddToBackStack.
Fragment 2 is pushed onto the stack, using fragmentTransaction.addToBackStack().
A new instance of fragment 1 is pushed onto the stack.
The top most fragment (fragment 1) is popped from the stack.
Activity 2 becomes foreground.
Activity 1 becomes foreground.
Here is the generalized method I use to handle fragments:
private void changeContainerViewTo(int containerViewId, Fragment fragment,
Activity activity, String backStackTag) {
if (fragmentIsAlreadyPresent(containerViewId, fragment, activity)) { return; }
final FragmentTransaction fragmentTransaction =
activity.getFragmentManager().beginTransaction();
fragmentTransaction.replace(containerViewId, fragment);
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
if (backStackTag == null) {
fragmentTransaction.disallowAddToBackStack();
} else {
fragmentTransaction.addToBackStack(backStackTag);
}
fragmentTransaction.commit();
}
Problem
When activity 1 resumes in the last step the lowest instance of fragment 1 also resumes. At this point in time fragment 1 returns null on getActivity().
Question
Why is a fragment which is not the top most on the stack resumed?
If resuming the fragment is correct - how should I handle a detached fragment?
When an Activity is not showing UI and then come to show UI, the FragmentManager associated is dying with all of your fragments and you need to restore its state.
As the documentation says:
There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state.
In your Activity onSaveInstanceState and onRestoreInstanceState, try saving you Fragment references and then restore them with something like this:
public void onSaveInstanceState(Bundle outState){
getFragmentManager().putFragment(outState,"myfragment", myfragment);
}
public void onRetoreInstanceState(Bundle inState){
myFragment = getFragmentManager().getFragment(inState, "myfragment");
}
Try this out and have luck! :-)
I don't see how this would happen, unless (based on how you described the steps) you've misunderstood how fragmentTransaction.addToBackStack() works: it manages which transactions are placed in backstack, not fragments.
From the android docs:
By calling addToBackStack(), the replace transaction is saved to the
back stack so the user can reverse the transaction and bring back the
previous fragment by pressing the Back button.
So if your step 2 looked something like this in code:
fragmentTransaction.replace(containerViewId, fragment2);
fragmentTransaction.addToBackStack();
fragmentTransaction.commit();
and your step 3:
fragmentTransaction.disallowAddToBackStack()//or just no call to addToBackStack - you do not say
fragmentTransaction.replace(containerViewId, newfragment1);
fragmentTransaction.commit();
At this point, Fragment2 will be removed from the backstack, and your backstack consists of the two Fragment1 instances. in Step 4 you pop the top one, which means you should have the bottommost Fragment1 now at the top.
This explains why it is the resumed fragment if you return to the activity. But not, i'm afraid, why it is apparently detached from its activity.
Android OS can and will create and destroy fragments when it sees fit. This is likely happening when you launch Activity 2 and return to Activity 1. I'd verify for sure that it isn't the actively displayed fragment. What is probably happening is that you are seeing it do some of the creation steps for fragment 1 before it does the creation steps for fragment 2.
As for handling the detached fragments you should take a look at this page. The gist of it is that you should only be using the getActivity in certain fragment functions(Based on the fragment life cycle). This might mean that you have to move some of your logic to other functions.

Categories

Resources