Fragments behaviour: FragmentTransaction::replace() and reverse backStack operation - android

I call this method to go forward from AFrag to BFrag:
showFragment()
{
FragmentTransaction fragmentTransaction = mFragmentMgr.beginTransaction();
// Add fragment to the container ContentView
fragmentTransaction.replace(R.id.operation_fragments_frame, mBFrag, mBFrag.getTag());
// Add FADE effect
fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
// Keep the transaction in the back stack so it will be reversed when backbutton is pressed
fragmentTransaction.addToBackStack(null);
// Commit transaction
fragmentTransaction.commit();
}
It shows a new fragment (BFrag), replace the previous one (AFrag) and keep info about the transaction, so it can be reversed/undone automatically on back button pressed.
When back button is pressed everything looks fine, the previous fragment is shown (AFrag). But when I go forward again (AFrag -> BFrag) I got a "Fragment already added exception".
Didn't the reverse/undone operation remove the new fragment (BFrag)? Is this the expected behaviour?
That's weird because after this, I decided to set a check:
if(mBFrag.isAdded())
{
fragmentTransaction.show(mBFrag);
}
else
{
fragmentTransaction.replace(R.id.operation_fragments_frame, mBFrag, mBFrag.getTag());
}
and stills, it gets into the else statement... and I get the exception.
Any insight on what am I doing wrong, please?
Thx.

have you tried to use an other method, like remove(), then do an add(). or anything similar? I saw on some other post that the replace() method does not always behave correctly.

Related

Android BackStack NavigationDrawer

I have a problem that I've been dealing with for the last couple o days and don't seem to find an answer to it.
Description : I have a main activity which contains a navigation drawer. Each item of the navigation drawer (when clicked) creates a fragment. In that fragment, there is a listView of objects, which creates other fragments when clicked. In those fragments i have another listView of objects which opens other fragments. In other words, there series of fragment that open other fragment. Something like this:
http://s22.postimg.org/pddo5gsv5/backstack.png
In order to be able to get back to each fragment, I've implemented the addToBackstack("string") method.
My question is, how can I implement correct backstack for my application so that when i click a navigation Drawer item, all the fragments that have been added to backstack are cleared, without the one that the navigation Drawer item opens.
Any help would be appreciated. Thank you !
EDIT
Ok, it seems I managed to figure it out. Considering what advices i received from the replies, here's the solution I came up with:
#Override
public void onBackPressed() {
int count = getFragmentManager().getBackStackEntryCount();
if (count != 0) {
FragmentManager.BackStackEntry backEntry = getFragmentManager()
.getBackStackEntryAt(
getFragmentManager().getBackStackEntryCount() - 1);
if (backEntry.getName() == NAVIGATION) {
finish();
} else
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
To put it in words: First, i added a backstack entry even for the top level fragments, given them a specific tag. The I have overridden the Activity's back button function so that when the last backstack entry is a top-level fragment to finish the activity (so that it not simply detach the fragment from activity, living it empty). Otherwise, if the last entry isn't an top-level fragment, execute a popBackStack.
PS: All non-top-level fragments are added to the backstack with a different tag then the top-level one. Also, i had to do a POP_BACK_STACK_INCLUSIVE in the navigation Drawer's click listener.
getFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
Thank you all for the advices and hopefully this EDIT help other users.
You can use the following code to solve your problem:
getFragmentManager().beginTransaction()
.replace(R.id.fragment_container, fragment)
.addToBackStack(fragment_tag)
.commit();
In order to make the code above work, you have to create the fragments dynamically. As hardcoded fragments cannot be replaced. To do that, you can create a container (FrameLayout etc.) which in our example has the id fragment_container. Then, the code above will add the fragment in the container dynamically. Finally, you have to pass as parameter in the addToBackStack method the fragment_tag. That means, that this transaction will be added in the back stack. And finally, in order to get it from the backstack you have to use the code below:
getFragmentManager().popBackStack(fragment_tag, FragmentManager.POP_BACK_STACK_INCLUSIVE));
The POP_BACK_STACK_INCLUSIVE flag, insures that "all matching entries will be consumed until one that doesn't match is found or the bottom of the stack is reached. Otherwise, all entries up to but not including that entry will be removed."
You can clear the fragment backstack by using something like:
fragmentManager.popBackStack("string", FragmentManager.POP_BACK_STACK_INCLUSIVE);
and then you can addToBackstack and commit as usual. More info.
A code snippet that shows the way I normally use it in navigation drawers:
FragmentManager fragmentManager = getSupportFragmentManager();
if(clearBackStack) {
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(R.id.content_frame, fragment);
if(!clearBackStack) {
ft.addToBackStack(null);
}
ft.commit();

Recreate Fragments on BackKey Pressed

Im trying to Solve a problem where when a user press back key, the fragment should be recreated rather than loading from back stack. I have a single Main activity with a frame layout and i replace the fragments within the single frame dynamically. the code below works when the user go from fragment within fragment. but when the user select from navigation drawer, the replaced fragment is going on to the top of backstack which is causing problems.
Right now the code i wrote in BackKey Pressed Event
public override void OnBackPressed()
{
Android.Support.V4.App.FragmentManager.IBackStackEntry entry =
SupportFragmentManager.GetBackStackEntryAt(SupportFragmentManager.BackStackEntryCount - 1);
string str = entry.Name;
if (SupportFragmentManager.BackStackEntryCount == 0)
{
this.Finish();
}
else
{
Fragment fr = (Fragment)MagicallyCreateInstance(str);
SupportFragmentManager.BeginTransaction().Replace(Resource.Id.content_frame, fr).Commit();
SupportFragmentManager.PopBackStack();
}
base.OnBackPressed();
}
i also have a replace fragment method which i use to replace fragments. but in this process the back key is getting disabled by default somehow (Not sure how) but whenever there is existing fragment in the backstack, the old UI is getting loaded. Can i refresh the layout here?
public void ReplaceFragment(Fragment fragment, FragmentManager fragmentManager)
{
string backStateName = fragment.Class.SimpleName;
bool fragmentPopped = fragmentManager.PopBackStackImmediate(backStateName, 0);
if (!fragmentPopped && fragmentManager.FindFragmentByTag(backStateName) == null)
{
fragmentManager.BeginTransaction()
.Replace(Resource.Id.content_frame, fragment).SetTransitionStyle(FragmentTransaction.TransitFragmentFade)
.AddToBackStack(backStateName)
.Commit();
}
}
Can anyone please help me solve this any one of the above?
I hope I'm understanding your problem correctly. But it sounds like you want to make sure that when the user selects a destination from your main navigation you want to make sure the back stack is cleared or reset.
Ex:
Stack looks like this:
A->B->C
User selects D from main navigation, stack should look like:
D
NOT
A->B->C->D
If this is the case, you should clear the back stack before navigating to any top-level destinations. This can be done like so:
FragmentManager.PopBackStack(null, FragmentManager.PopBackStackInclusive);
The documentation for this method is not great, but it will pop everything off the back stack. A discussion can be found here: https://groups.google.com/d/msg/android-developers/0qXCA9rW7EI/M9riRM0kl9QJ

Replaced fragment still visible

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.

Replacing a Fragment with itself does not show anything

I'm trying to decide and show a fragment in activity's onResume method, but in case a previously added fragment is chosen again, then the activity goes blank.
Sample code (with one fragment):
#Override
protected void onResume(){
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.myLayout, fragA);
trans.commit();
getSupportFragmentManager().executePendingTransactions();
}
With above code, when the activity is created for the first time, it shows fragA correctly, but in case I press Home Key and then switch back to my activity (in order to provoke onResume again), it all goes blank (seems like fragA is removed).
Is replacing a previously added fragment removes itself? or how not to loose a fragment if it is replaced by itself?
You can't replace a fragment with itself. The first half of a replace is a removal of the previous fragment at that id. Once a fragment is removed it can no longer be added or used by the fragment manager (so the add portion of the replace will not work properly).
Depending on your use case, you have two options:
Create a new fragment instead of reusing the existing instance
Use some other method to see if its necessary to replace your fragment
Finally, you probably don't need to call executePendingTransactions.
You can try:
if( !(getSupportFragmentManager().findFragmentById(R.id.myLayout) instanceof FragmentA) ) {
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.myLayout, fragA);
trans.commit();
}
And I assume that fragA is FragmentA class object.
Finally, I had to put a check before replacing fragments. In case, an (already added) fragment is requested for replace again, I had to check if its already added then ignore the replacement, else proceed. For example:
#Override
protected void onResume() {
if (!fragA.isAdded()) {
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.myLayout, fragA);
trans.commit();
//getSupportFragmentManager().executePendingTransactions(); //unnecessary
}
}
When referencing back to a created Fragment please do make sure to try adding the
fragmentTransaction.addToBackStack(null);
method right before committing so that your Fragment is resumed instead of destroyed as mentioned in the developer guides.
If you don't call addToBackStack() when you perform a transaction that removes a fragment, then that fragment is destroyed when the transaction is committed and the user cannot navigate back to it. Whereas, if you do call addToBackStack() when removing a fragment, then the fragment is stopped and is later resumed if the user navigates back.
You can find this at the end of this page.

How to keep only first added Fragment in back stack (fragment overlapping)?

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.

Categories

Resources