Scenario what i'm trying to achieve:
Loading activity with two frame containers (for list of items and for details).
At the app launch time add listFragment in listFrame and some initial infoFragment in detailsFrame containers.
Navigating through list items without adding each detail transaction to back stack (want to keep only infoFragment in stack).
As soon as user hit back button (navigate back) he falls back to intial infoFragment what was added in launch time.
If sequential back navigation fallows then apps exit.
My code:
protected override void OnCreate(Bundle savedInstanceState)
{
...
var listFrag = new ListFragment();
var infoFrag = new InfoFragment();
var trans = FragmentManager.BeginTransaction();
trans.Add(Resource.Id.listFrame, listFrag);
trans.Add(Resource.Id.detailsFrame, infoFrag);
trans.Commit();
...
}
public void OnItemSelected(int id)
{
var detailsFrag = DetailFragment.NewInstance(id);
var trans = FragmentManager.BeginTransaction();
trans.Replace(Resource.Id.detailsFrame, detailsFrag);
if (FragmentManager.BackStackEntryCount == 0)
{
trans.AddToBackStack(null);
}
trans.Commit();
}
My problem:
After back button has been hit, infoFrag is overlapped with previous detailFrag! Why?
You can do this:
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
getSupportFragmentManager().popBackStack(getSupportFragmentManager().getBackStackEntryAt(0).getId(), getSupportFragmentManager().POP_BACK_STACK_INCLUSIVE);
} else {
super.onBackPressed();}
In your activity, so you to keep first fragment.
You shouldn't have, in your first fragment, the addToBackStack. But, in the rest, yes.
Very nice explanation by Budius. I read his advice and implemented similar navigation, which I would like to share with others.
Instead of replacing fragments like this:
Transaction.remove(detail1).add(detail2)
Transaction.remove(detail2).add(detail3)
Transaction.remove(detail3).add(detail4)
I added a fragment container layout in the activity layout file. It can be either LinearLayout, RelativeLayot or FrameLayout etc.. So in the activity on create I had this:
transaction.replace(R.id.HomeInputFragment, mainHomeFragment).commit();
mainHomeFragment is the fragment I want to get back to when pressing the back button, like infoFrag. Then, before EVERY NEXT transaction I put:
fragmentManager.popBackStackImmediate();
transaction.replace(R.id.HomeInputFragment, frag2).addToBackStack(null).commit();
or
fragmentManager.popBackStackImmediate();
transaction.replace(R.id.HomeInputFragment, frag3).addToBackStack(null).commit();
That way you don't have to keep track of which fragment is currenty showing.
The problem is that the transaction that you're backing from have two steps:
remove infoFrag
add detailsFrag (that is the first1 detail container that was added)
(we know that because the documentation This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId and then add(int, Fragment, String) with the same arguments given here. )
So whenever the system is reverting that one transaction is reverting exactly those 2 steps, and it say nothing about the last detailFrag that was added to it, so it doesn't do anything with it.
There're two possible work arounds I can think on your case:
Keep a reference on your activity to the last detailsFrag used and use the BackStackChange listener to whenever the value change from 1 to 0 (you'll have to keep track of previous values) you also remove that one remaining fragment
on every click listener you'll have to popBackStackImmediatly() (to remove the previous transaction) and addToBackStack() on all transactions. On this workaround you can also use some setCustomAnimation magic to make sure it all looks nice on the screen (e.g. use a alpha animation from 0 to 0 duration 1 to avoid previous fragment appearing and disappearing again.
ps. I agree that the fragment manager/transaction should be a bit more clever to the way it handles back stack on .replace() actions, but that's the way it does it.
edit:
what is happening is like this (I'm adding numbers to the details to make it more clear).
Remember that .replace() = .remove().add()
Transaction.remove(info).add(detail1).addToBackStack(null) // 1st time
Transaction.remove(detail1).add(detail2) // 2nd time
Transaction.remove(detail2).add(detail3) // 3rd time
Transaction.remove(detail3).add(detail4) // 4th time
so now we have detail4 on the layout:
< Press back button >
System pops the back stack and find the following back entry to be reversed
remove(info).add(detail1);
so the system makes that transaction backward.
tries to remove detail1 (is not there, so it ignores)
re-add(info) // OVERLAP !!!
so the problem is that the system doesn't realise that there's a detail4 and that the transaction was .replace() that it was supposed to replace whatever is in there.
You could just override onBackPressed and commit a transaction to the initial fragment.
I'm guessing but:
You've added the transaction to replace infoFrag with 1st detailsFrag into the backstack.
But then you replace 1st detailsFrag with 2nd detailsFrag.
At this point when you click back, the fragment manager cannot cleanly replace 1st detailsFrag with infoFrag as 1st detailsFrag has already been removed and replaced.
Whether the overlapping behaviour is expected or not I don't know.
I would suggest debugging the Android core code to see what it is doing.
I'm not sure whether you can achieve without say overriding Activity::onBackPressed() and doing the pops yourself having added all transactions to the backstack.
Related
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!
I am hitting a very strange problem in Android and I can't figure out why it's happening or how to code around it. I truly believe this to be an Android bug.
I have a MainActivity which contains a FrameLayout named main_container (its height and width are both match_parent as each fragment should be the only fragment "showing" to the user). From MainActivity, I add Fragment A like so:
mFragmentManager.beginTransaction()
.replace(R.id.main_container, frag, fragTag)
.commit();
From there, Fragment A, upon a user's click of a view, will add Fragment B like so ("frag" and "fragTag" are different values than the above code snippet):
mFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right)
.add(R.id.main_container, frag, fragTag)
.addToBackStack(null)
.commit();
And from here, Fragment B will add Fragment C like so (again, "frag" and "fragTag" are different values than the previous two snippets):
mFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right)
.add(R.id.main_container, frag, fragTag)
.addToBackStack(null)
.commit();
So at this point, in the backstack, we should have Fragment A -> Fragment B -> Fragment C.
Fragment C invokes the MediaPicker upon the user's click of a view. Doing so calls all fragments' onPause methods and the app is put in the background. Now when the user selects an image, the application is resumed but here's where the bug happens... it resumes in this order, as proven with breakpoints in each fragments' onResume method:
Fragment A -> Fragment C -> Fragment B
This causes all sorts of issues because each of my fragments registers itself as a listener in the MainActivity to handle back button clicks. This logic relies on that ordering being correct. For some reason, it's still showing Fragment C on top, but onResume was definitely called out of order.
Perhaps even worse though... rather than clicking for MediaPicker, you can simply rotate the phone to cause a config change. This exhibits the same behavior of reordering to A -> C -> B but in this case it DOES actually show the wrong fragment on top. It SHOWS Fragment B on top.
Is it a design point that you can't rely on Android to resume fragments in the same order you added them to the backstack and I'm supposed to code around it? Or am I doing something wrong? Or is this really an Android bug? I am by far not a newbie to Android development, but this one has me stumped.
EDIT:
I've pinpointed what is going on and apparently it's by design. It seems pretty crazy to me and I disagree with the logic behind it. I may be able to fix this with reflection, but I don't like doing that. Anyways, on to the problem.
The problem is with the way FragmentManagerImpl keeps track of active fragments. It has an ArrayList to keep track of active fragments and when everything is paused (such as in my case where I'm starting an intent to get a photo from media gallery, thus it's leaving my app), upon resuming back into my app, it moves the fragments back to active in the same order they're in that ArrayList. Sounds great, eh?
Well here's my problem. When things are taken OUT of that ArrayList, they don't remove() the item, they just set it to null and then have logic to reuse that empty "slot" (line 1168 in the github link) when the next fragment comes along. In my case, the transient fragment that leaves a hole in the ArrayList is a DialogFragment. Putting it back into terms of my original report, Fragment A shows a DialogFragment... clicking a certain button in that DialogFragment brings up Fragment B. Clicking another view in Fragment B brings up Fragment C. But here's what happens to the ArrayList FragmentManagerImpl keeps track of after clicking the button in the DialogFragment:
{ FragA, null (used to be DialogFragment), FragB }
So apparently DialogFragment was moved out of active state after FragB was moved to active, thus leaving a hole. So now we click the view in FragB to bring up FragC and the ArrayList looks like so:
{ FragA, FragC (reused DialogFragment's slot), FragB }
We go off to the media picker, come back, and voila the fragments are resumed out of order with respect to how I instantiated them in the first place. This makes no sense to me and if you don't step into OS code with breakpoints, you never figure out why Android is not behaving the way you told it to. Seems like it would have been easier to just do an ArrayList.remove() of the fragment you removed, thus leaving no holes.
Like I said, I can probably get around this with reflection... but I'm leery of that because there is also this mIndex variable in all Fragments that corresponds to the index of it's slot in that ArrayList (mActive). So I'd have to be sure to keep those in sync... and now I have a dependency on knowing how the OS code works. :(
This is a known issue. Google "android fragment reordering" and you will get a whole page of links on the subject including some solutions.
I'm implementing menu navigation using Fragments. So I begin with Home, and then users can navigate to diferent sections and details of each section.
When a user changes section, then I call pop on the fragmentmanager backstack until I reach Home, and then load the new section.
This is all working as expected. But I'm getting this problem:
load a section that calls setHasOptionsMenu(true) on onResume()
loads another section (old section it's suposed to get out of the stack). I see it OK. No menu is shown
leave the application (for example, go to Android Laucher activity) and then when I return, I see the correct section, but it's showing the Menu of the old Fragment.
I've iterated the backstack and printed each fragment, and there it's not the fragment with the menu.
I put a debug mark on the onResume() method (where the setHasOptionsMenu(true) is flagged) and it indeed enters here, so the Fragment it's still somewhere.
I want to know if I'm doing something wrong and how could I solve it, thx
Update:
I'm using this code to load new fragments
fm.beginTransaction()
.add(container, sectionFragment.getFragment())
.addToBackStack(sectionFragment.getFragmentName())
.commit();
And for remove:
private void clearStack(){
int count = fm.getBackStackEntryCount();
while(count > 1){
fm.popBackStack();
count--;
}
}
NOTE 1: I'm using add instead replace because I don't want to loose the state of my fragment when I navigate back from detail section. When I load another different section, then I call clearStack to pop the stack up to 1, and then loads new fragment. At the end, I'm calling executePendingTransactions() to finish to remove the fragments from the transaction.
NOTE 2: I'm seeing that it is entering on my fragment onDestroy() method, so it is suposed to be destroyed. But I don't know why it is getting called again when the Main activity resumes.
I found that the problem was not in the logic of adding and removing fragment of the stack.
The problem was that some of the fragment loaded another fragments inside of it (it had ViewPager component). Then I thought that when the fragment was removed then these fragments were removed too.
This is true ONLY if you use getChildFragmentManager() method. This method MUST be used when loading fragments inside other fragmets. If not, then the fragments are asociated with the fragments activity.
popBackStack will just revert your last FragmentTransaction.
If you use FragmentTransaction.add, popBackStack will just call FragmentTransacetion.remove.
But if you call FragmentTransaction.replace, popBackStack will call FragmentTransaction.remove and FragmentTransaction.add
For your "NOTE 1" :
FragmentTransaction.replace will not change your fragment state.
I found this question, because after calling
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
this code fragmentManager.getFragments().size() returns me the maximum number of fragments, which were in the stack. I checked every fragment on null. And I found that some fragment is null in my case. Maybe it will help someone)
If you are really looking to remove fragments at once then follow:
How to replace Fragments of different types?
Otherwise use replace transaction for fragments to smooth transitiona and hassel free approach, see https://stackoverflow.com/a/23013075/3176433
Also understand Fragment lifecycle,
http://developer.android.com/guide/components/fragments.html
I had a similar problem where the popBackStack() didn't remove my fragment.
However, I noticed that I called the wrong FragmentManager, where I had to call getSupportFragmentMananger() instead of getFragmentManager().
Maybe there is a <fragment> or <androidx.fragment.app.FragmentContainerView> in an activity with android:name="androidx.navigation.fragment.NavHostFragment", app:defaultNavHost="true" and app:navGraph="#navigation/nav_graph".
In this case navigation is held by nav_graph. If you don't want to use NavController and NavHostFragment, maybe you should remove navigation and clean <fragment> tag.
When I start my app it runs an AsyncTask to load up and then in onPostExecute, I then setContentView to the new layout then add a fragment with two buttons offering two modes by an add FragmentTransaction. After one of the two modes is clicked, it then replaces the fragment with yet another FragmentTransaction using the replace method.
If the app crashes it returns to the first screen, loading up the two buttons offering the two modes. In this case if either mode is selected, the second fragment is loaded but is now the background is suddenly transparent showing the two buttons below and they remain clickable. If they are clicked again they properly replace the fragment so that it isn't visible below. This is just weird, I can't understand what could cause this.
I've researched and seen these two similar questions, one and two, which suggested that it might be because the ID is wrong or I have defined the fragment in XML. Neither of these two factors are the case.
My code is shown below:
Below I replace the loading screen.
#Override
protected void onPostExecute(Void result) {
setContentView(R.layout.activity_main_screen);
FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction();
transaction.add(R.id.fragment_container, new ModeFragment())
.commit();
}
After which, when a button is clicked I pass the fragment I wish to replace the current with into this method below:
private void replaceCurrentFragment(Fragment fragment) {
FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction();
transaction.replace(R.id.fragment_container, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.addToBackStack(null).commit();
}
This works the first time, however if a crash occurs then the app returns to the first fragment and the second time this method is passed, the new replacing fragment is semi-invisible. Clicking the button on the first fragment again calls this method again and it is now fine.
Obviously I don't want the app to crash so this shouldn't occur, but I get this feeling that there's something wrong with how I'm writing my code.
I've had the same problem happen to me, and it was because I loaded a fragment in the OnCreate of my Activity, without checking if there was a savedInstanceState, so android first reopen all old fragments, then do the OnCreate, which added the fragment over the old ones without replacing them so when you navigate to another fragment, it only replaces the top one, but not the bottom one, so you will see the fragments under it.
Might not be exactly the same thing for you, but it might help you figure it out.
Hi I am not sure I am doing the right thing. I have several fragments in one activity (not shown at the same time). When I add the fragment do I have to check if a previous instance exists? I am using the compatibility package and my fragment CameraFragment is a separate class (in its own file):
private void addNewFragment(Fragment fragment, String tag) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.frag1, fragment, tag);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.addToBackStack(null);
ft.commit();
}
and then :
public void startPicTaking() {
addNewFragment(CameraFragment.newInstance(), TAG_PIC_TAKING);
}
So each time a user clicks a button to take a picture I use this methods BUT shall I verify if the fragment already exists and remove it first or does the static method newInstance make sure the fragment is not duplicated?
I have read the doc several times but I don't understand why the line:
ft.addToBackStack(null);
what is it for? I know you can pop the back stack and it keeps the transaction but how can it be used and for what? Is it necessary or if I don't use it I can skip it?
thanks
I have several fragments in one activity (not shown at the same time). When I add the fragment do I have to check if a previous instance exists?
No, it will just create a new instance of that Fragment when it adds the next instance of it. It will not affect the previous instance of it.
So each time a user clicks a button to take a picture I use this methods BUT shall I verify if the fragment already exists and remove it first or does the static method newInstance make sure the fragment is not duplicated?
You could do that if you wanted to, to ensure that no Fragment appears twice in the stack. (So when you hit back, you don't get the same activity again.) Depending on exactly what appears in your back stack, you may not want to remove stuff lower down. (Consider that a user expects previous fragments to appear when he hits the back button.)
I have read the doc several times but I don't understand why the line: ft.addToBackStack(null); what is it for?
When Fragment objects are added to the back stack, then each time the user hits back, they will go to the previous item on the stack. If you do not add an item to the back stack, the user will not encounter it when they hit the back button.