Assume I have 4 fragments A B C and D.
A and B are major fragments, C and D are minor.
I use navigation drawer to switch fragments.
A is the default starting fragment.
I want to achieve following features but cannot figure out how to play with the fragment manager and transactions.
A -> B or B -> A, replace current fragment, do not push backstack, but I want to keep the current fragment status (e.g. list view position) after navigate back
A/B -> C/D, add C/D on top of A/B, using back button to navigate back to A/B.
C -> D or D -> C, replace current fagment
C/D -> A/B, remove current fragment C/D and show A/B
Is the only way to implement this function that I should write some complicated function for switching the fragments (and also need to keep what is current fragment and what is the wanted target fragment)?
Is there better way out?
According to #DeeV 's answer, I came out with something like following.
LocalBrowse and WebsiteExplore are main fragments while Settings and About are sub fragments.
It seems to work fine but still a little bit ugly, any better idea?
private void switchToFragment(Class<?> targetFragmentClz) {
if(mCurrentFagment!=null && mCurrentFagment.getClass().equals(targetFragmentClz)) {
return;
}
BaseFragment targetFragment = null;
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if(targetFragmentClz.equals(LocalBrowseFragment.class)
|| targetFragmentClz.equals(WebsiteExploreFragment.class)) {
if(mCurrentFagment instanceof SettingsFragment //mCurrentFragment will not be null this time
|| mCurrentFagment instanceof AboutFragment) {
transaction.remove(mCurrentFagment);
}
if(mCurrentMainFagment==null || !mCurrentMainFagment.getClass().equals(targetFragmentClz)) {
targetFragment = (BaseFragment) Fragment.instantiate(this, targetFragmentClz.getName());
targetFragment.setHasOptionsMenu(true);
transaction.replace(R.id.ac_content_frame_main, targetFragment);
mCurrentMainFagment = targetFragment;
}
} else {
targetFragment = (BaseFragment) Fragment.instantiate(this, targetFragmentClz.getName());
targetFragment.setHasOptionsMenu(true);
getSupportFragmentManager().popBackStackImmediate();
transaction.replace(R.id.ac_content_frame_sub, targetFragment)
.addToBackStack(null);
}
transaction.commit();
mCurrentFagment = targetFragment;
}
One method that I can think of is to stack the two types of fragments on each other. So a system like this:
<FrameLayout>
<FrameLayout id="main_container">
<FrameLayout id="sub_container">
<FrameLayout>
Would mean that you have two containers holding fragments. The top one completely covers the other. Thus, you could have two method likes this:
public void swapMainContainer(FragmentManager fm, Fragment frag)
{
fm.beginTransaction().
.replace(R.id.main_container, frag, "TAG")
.commit();
}
public void swapSubContainer(FragmentManager fm, Fragment frag)
{
fm.popBackstackImmediate();
fm.beginTransaction()
.replace(R.id.sub_container, frag, "SUBTAG")
.addToBackStack(null)
.commit();
}
So if you use swapMainContainer() only with Fragment A and Fragment B, they will constantly replace each other but the commits won't be added to the backstack.
If you use swapSubContainer() only with Fragment C and Fragment D, they will likewise replace each other, but "Back" will close them. You are also popping the backstack every time you commit a sub Fragment thus removing the previous commit. Though, if there's nothing in the backstack, it won't do anything.
To remove C/D, simply call popBackStack() and it will remove them from the stack.
The flaw in this approach however is if you have more than these two Fragments that are added to the backstack. It may get corrupted.
EDIT:
Regarding saving view state, the fragment itself will have to handle that via this method.
Related
I have one activity containing one container that will receive 2 fragments.
Once the activity initialises i start the first fragment with:
showFragment(new FragmentA());
private void showFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, fragment, fragment.getTag())
.addToBackStack(fragment.getTag())
.commit();
}
Then when the user clicks on FragmentA, I receive this click on Activity level and I call:
showFragment(new FragmentB());
Now when I press back button it returns to fragment A (thats correct) and when i press again back button it show empty screen (but stays in the same activity). I would like it to just close the app (since the activity has no parent).
There are a lot of posts related with Fragments and backstack, but i can't find a simple solution for this. I would like to avoid the case where I have to check if im doing back press on Fragment A or Fragment B, since i might extend the number of Fragments and I will need to maintain that method.
You are getting blank screen because when you add first fragment you are calling addToBackStack() due to which you are adding a blank screen unknowingly!
now what you can do is call the following method in onBackPressed() and your problem will be solved
public void moveBack()
{
//FM=fragment manager
if (FM != null && FM.getBackStackEntryCount() > 1)
{
FM.popBackStack();
}else {
finish();
}
}
DO NOT CALL SUPER IN ONBACKPRESSED();
just call the above method!
addToBackStack(fragment.getTag())
use it for fragment B only, not for fragment A
I think your fragment A is not popped out correctly, try to use add fragment rather replace to have proper back navigation, however You can check count of back stack using:
FragmentManager fragmentManager = getSupportFragmentManager();
int count = fragmentManager.getBackStackEntryCount();
and you can also directly call finish() in activity onBackPressed() when you want your activity to close on certain fragment count.
I have 5 fragments(say A, B, C, D, E) and one activity with a container.
Whenever i want to add a fragment to a container I'll be using the following code.
getSupportFragmentManager().beginTransaction().replace(R.id.mainContainerRL, fragment, tag).addToBackStack(tag).commit();
Let's say i added Fragment A.
Upon some action in A, I added fragment B.
Upon some action in B, I added fragment C.
Upon some action in C, I added fragment D.
Upon some action in D, I added fragment E.
Now my stack should be as follows.
A -> B -> C -> D -> E
Now upon some action in Fragment E, I need to remove fragments D, C, B so that when user click back, he should directly see Fragment A.
I tried using following code.
public void removeScreen(#NonNull String tag) {
FragmentManager manager = getSupportFragmentManager();
Fragment fragment = manager.findFragmentByTag(tag);
if (fragment != null) {
FragmentTransaction trans = manager.beginTransaction();
trans.remove(fragment);
trans.commitAllowingStateLoss();
}
}
Upon some action in Fragment E, I called the above function with Tags of Fragment D, C, B(Tags are same as the one that i used for fragment transaction).
Now when i click back button fragment D is becoming visible but i was expecting fragment A.
It would be very helpful if somebody points out where am i going wrong.
If you want to reach exactly the same behavior that you've described, you can do it by this way:
FragmentManager manager = getSupportFragmentManager();
manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
This will clear all backstack until the bottom of stack will be reached.
I prefer using DialogFragment for this reason and pop them using interface callbacks and dismiss() function inside them. This is the easiest and quick way to implement what you are trying to do.
I think your problem occur here :
replace :
getSupportFragmentManager().beginTransaction().replace(R.id.mainContainerRL, fragment, tag).addToBackStack(tag).commit();
to:
getSupportFragmentManager().beginTransaction().add(R.id.mainContainerRL, fragment, tag).addToBackStack(tag).commit();
when you add your fragment to container,tou just add it ,if use"replace" method,you romove it from activirty's fagment manager,it case your "removeScreen" method did not work
Say I've pushed three fragments onto the stack: Fragment A, B, and C.
Goal: When I press back on Fragment C I want to have some hook into determining if Fragment B is showing and call frag.onShow().
Thought process: I added an onBackStackChangedListener in order to determine when fragments were added or removed from the stack. Here I check whether the current fragment is equal to the new one (pushed) or different (popped).
Problem: Both frag and getCurrentFragment() are returning the current fragment AFTER Fragment C has been dismissed. So here the new fragment is Fragment B but getCurrentFragment() is also returning Fragment B so it doesn't think it's a pop.
getSupportFragmentManager().addOnBackStackChangedListener(new OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
FragmentManager fm = getSupportFragmentManager();
int count = fm.getBackStackEntryCount();
if (count > 0) {
String name = fm.getBackStackEntryAt(count - 1).getName();
MyFragment frag = (MyFragment) fm.findFragmentByTag(name);
MyFragment currFrag = getCurrentFragment();
if (frag != currFrag) {
if (frag != null) {
frag.onShow();
}
}
}
}
});
public MyFragment getCurrentFragment() {
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (fragment instanceof MyFragment) {
return (MyFragment) fragment;
}
return null;
}
Question: Is there a way to get the last popped fragment? Or is there a better way to do what I'm trying to do? I can always keep some variable around to determine if I'm popping or pushing but I was hoping for a less hacky feeling way.
Thanks.
Thank you #DeeV for helping me understand fragment visibility.
If you're just stacking Fragments on top of each other, then they are technically all still "visible" and active as far as the FragmentManager is concerned. Assuming that your fragments are only visible at one time, you can call FragmentTransaction#hide(fragment) to hide a Fragment. Fragment#onHiddenChanged() will be called both when you hide it and when you unhide it.
I'll be transferring my code to what they suggested. I also realized that if you do need to do it manually like I was doing earlier, a variable of the last fragment count will help determine whether it was a push or a pop.
Say i have fragment's A,B,C and D. The normal movement between fragments is A -> B -> C -> D. Now suppose i want to jump from A -> D, but onBackPressed() from D i want to be able to navigate back to C and then B respectively. Is there a way of doing this? The code i was attempting was something like this but it ucrrently is not working.
public void showNestedFragment(LinkedList<Fragment> fragments, boolean allowBack)
{
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
if (allowBack == false) // pop all thats in the backstack
getSupportFragmentManager()
.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
for(Fragment fragment : fragments){
fragmentTransaction.add(R.id.fragment, fragment);
if (allowBack)
{
fragmentTransaction.addToBackStack(null);
}
try
{
fragmentTransaction.commit();
}
catch (IllegalStateException e)
{
e.printStackTrace();
}
}
}
You have fragments so you can do anything you want. You should follow these steps to achieve this.
First declare all fragments in manifest in order A than B than C than D
So you can achieve normal navigation from A>B>C>D using simple approach of set visibility on of next fragment.
When you want direct navigation from A to D simply hide B and C fragment.
When you want to move from D>C>B>A simply visible B and C again and follow the simple navigation again.
Currently I have one activity, and fragments are being added to it (search, song details, settings, etc). I implemented side based menu navigation, so now, as a side effect, tehre's no limit to how many Fragments get added to the Backstack. Is there any way I can limit the number of fragments, or remove older entries? Each song details fragment for instance, has a recommended song list, and through that you can go to another song details fragment. It's easily possible to have 30 fragments in the backstack, which if you have DDMS open, you can see the heap size slowly (but surely) increasing.
Edit: One thing I did try to do is if a User clicked one of the side menu options, if that fragment is already in the backstack try to go back to that fragment instead of instantiating a new one, but of course, if a user is on a Song Details page, then he would expect pressing back would take him to that Fragment so that won't work.
Edit 2:
This is my addFragment method (along with Phil's suggestion):
public void addFragment(Fragment fragment) {
FragmentManager fm = getSupportFragmentManager();
if(fm.getBackStackEntryCount() > 2) {
fm.popBackStack();
}
fm.beginTransaction()
.replace(R.id.fragment_container, fragment).addToBackStack("")
.commit();
}
I just tried it, and assuming my Fragment history is: A->B->C->D, going back from D, goes B->A->exit.
I just went 8 levels deep to test: A->B->C->D->E->F->G->H, and going back from H, same thing happened: H->B->A->exit.
All Fragments are getting added through that method above. What I would like to see is: H->G->F->exit.
You can programatically control the number of Fragments in your BackStack:
FragmentManager fm = getActivity().getSupportFragmentManager();
if(fm.getBackStackEntryCount() > 10) {
fm.popBackStack(); // remove one (you can also remove more)
}
Simply check how many Fragments there are in your Backstack and remove if there is an "overflow".
If you want to remove specific Fragments from the BackStack, you will have to implement your own BackStack and override onBackPressed(). Since the Fragment BackStack is a Stack (as the name indicates), only the top element (the last added) can be removed, there is no possibility of removing Fragments in between.
You could for example use
ArrayList<Fragment>
to realize your own stack. Simply add and remove Fragments from that "stack" (it's not really a stack anymore) whenever you desire and handle the loading of previous fragments by overriding the onBackPressed() method.
This is an old question but my answer might help someone.
Why not checking if the fragment is in the stack and pop it? This way you wouldn't have to worry with back stack size (unless you have a lot of fragments).
String backStateName = fragment.getClass().getName();
boolean fragmentPopped = false;
try {
// true if fragment is in the stack
fragmentPopped = fragmentManager.popBackStackImmediate(backStateName, 0);
} catch (Exception e) {
e.printStackTrace();
}
FragmentTransaction ft = fragmentManager.beginTransaction();
if ( !fragmentPopped ) { //fragment not in back stack, add it...
ft.setTransition( FragmentTransaction.TRANSIT_FRAGMENT_FADE );
ft.replace(R.id.main, fragment);
ft.addToBackStack( backStateName );
try {
ft.commit();
} catch (Exception e) {
e.printStackTrace();
}
}