Android remove transaction from back stack - android

I have 3 fragments, and I'm navigating using bottom menu (3 items), lets say I navigate this way :
A -> B -> C -> B -> C
when I press the back button, that's what will happen
A <- B <- C <- B <- C
and what I want is this
A <- B <- C
that's mean if add fragment that's already added the old one must be deleted, more precisely remove the transaction from the back stack
this code will not work because we are adding new transaction here :
FragmentTransaction transaction = mContext.beginTransaction();
Fragment lastFragment = mContext.findFragmentByTag(mFragmentTag);
if (lastFragment != null) {
transaction.remove(lastFragment);
transaction.commit();
}
btw, may some developers make a mistake, but the back stack stores transactions and NOT fragments.

To get this behaviour, you can follow something like this:
I am assuming you have a onTabSelected(int position) which gets called every time you tap on the bottom menu.
public void onTabSelected(int position, boolean wasSelected) {
FragmentManager fragmentManager = getSupportFragmentManager();
// Pop off everything up to and including the current tab
fragmentManager.popBackStack(SELECTED_FRAG_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
// Add again the new tab fragment
fragmentManager.beginTransaction()
.replace(R.id.container, TabFragment.newInstance(),
String.valueOf(position)).addToBackStack(SELECTED_FRAG_TAG)).commit();
}
Firstly, you need to have tags for all your fragment. The basic idea is popBackStack upto that fragment tag that is selected.
And from the documentation of popBackStack(String name, int flags)
Pop the last fragment transition from the manager's fragment
back stack. If there is nothing to pop, false is returned.
This function is asynchronous -- it enqueues the
request to pop, but the action will not be performed until the application
returns to its event loop.
#param name If non-null, this is the name of a previous back state
to look for; if found, all states up to that state will be popped.
The {#link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
the named state itself is popped. If null, only the top state is popped.
#param flags Either 0 or {#link #POP_BACK_STACK_INCLUSIVE}.

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.

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

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

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.

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