I have strange behavior happening when I try to use the backstack of nested fragments in My app.
Say I have this fragment, FragmentA, and it is placed programatically inside of another rootFragment with a linearlayout designed to hold fragments.
rootFragment's layout contains:
<LinearLayout
android:id="#+id/map_fragmentHolder"
android:layout_height="0dp"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_weight="1.5" />
I use my switchToFragment method (outlined below) to replace FragmentA with a fragment of type FragmentB.
public void switchToFragment(Fragment fragment, boolean addtoBackStack) {
//Perform the fragment switch
FragmentTransaction childTransaction = rootFragment.getChildFragmentManager().beginTransaction();
childTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE |
FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
childTransaction.replace(R.id.map_fragmentHolder, fragment);
if(addtoBackStack){
if(currentFragment != null)
fragmentStack.push(currentFragment);
childTransaction.addToBackStack(null);
}
currentFragment = fragment;
childTransaction.commit();
}
This works perfectly when the fragment replacing the currentFragment is of different types. However for some strange reason, if I try to switch in another fragment of type FragmentB when there is one currently added, the back stack suddenly stops working. Instead of replacing all fragments within the linearlayout, the old fragment is placed below the current one.
Here is how I call the back stack to pop. It is done this way because the onBackPressed() method does not handle backstacks on nested fragments by default.
public boolean onBackPressed() {
if(fragmentStack.size() == 0)
return false;
//ChildFragmentManagers don't pop the backstack automatically, so we gotta do it manually...
rootFragment.getChildFragmentManager().popBackStack();
currentFragment = fragmentStack.pop();
return true;
}
I have managed to make my app work if I simply replace my onBackPressed method with the following, however I would really like to know what is happening, and why it only happens when I am replacing the fragment with one of the same type.
public boolean onBackPressed() {
if(fragmentStack.size() == 0)
return false;
switchToFragment(fragmentStack.pop(), false);
return true;
}
Any ideas or insights would be greatly appreciated.
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 an Android activity that holds and manages six fragments, is fragment is a step in a flow, some of the fragments are replaced and some of them are added.
The Activity just uses a Framelayout as the container for the fragments as follows:
<FrameLayout
android:id="#+id/content"
android:layout_below="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Then the flow of the fragments is like this:
//Activity starts, add first Fragment
fragmentManager.beginTransaction().replace(R.id.content, FirstFragment.newInstance(listOfItems)).commit();
then
//User pressed button, activity got callback from first fragment
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.content, fragment2);
transaction.addToBackStack("frag2");
transaction.commit();
then
//Another callback from Frag2, perform the add of frag 3
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.content, fragment3);
transaction.addToBackStack("frag3");
transaction.commit();
And so on....
I also manage the back stack from the Activity like this:
//Controlling the back stack when the user selects the soft back button in the toolbar
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
if (fragmentManager.getBackStackEntryCount() == 0) {
super.onBackPressed();
overridePendingTransition(R.anim.no_change, R.anim.slide_down);
} else {
if(!BaseFragment.handleBackPressed(getSupportFragmentManager())){
super.onBackPressed();
Fragment fragment = fragmentManager.getFragments()
.get(fragmentManager.getBackStackEntryCount());
fragment.onResume(); //Make sure the fragment that is currently at the top of the stack calls its onResume method
}
}
return true;
}
return super.onOptionsItemSelected(item);
}
//Controlling the back stack when the user selects the "hardware" back button
#Override
public void onBackPressed() {
if (fragmentManager.getBackStackEntryCount() == 0) {
super.onBackPressed();
overridePendingTransition(R.anim.no_change, R.anim.slide_down);
} else {
if(!BaseFragment.handleBackPressed(getSupportFragmentManager())){
super.onBackPressed();
Fragment fragment = fragmentManager.getFragments()
.get(fragmentManager.getBackStackEntryCount());
fragment.onResume(); //Make sure the fragment that is currently at the top of the stack calls its onResume method
}
}
}
My problem is that I open the app and go to this Activity which loads the fragments and then go through the flow to a certain stage ( I haven't narrowed it down yet) then I press the home button and blank my screen. Now after a certain amount of time when I open the app again it opens on the fragment I left but everything seems to be messed up, when I press back it seems to pop the wrong fragment and the UI becomes mixed up with the different fragments.
My guess is that when I open the app again the Activity onResume or the Fragment onResume or some lifecycle event is being called that I am not handling correctly?
So I was wondering is there best practices, guidelines or patterns that should be adhered to when using a Fragment pattern like I am doing so?
Since you have so many fragments in one activity, and they use the same container, that means all fragments are in the same place, and only one fragment will show at a time.
So why don't you use ViewPager and let FragmentPagerAdapter manager these fragments? In this way, you do not need to manager fragment lifecycle by yourself, you just need to override FragmentPagerAdapter methods:
to create fragment instance by getItem,
to update fragment by getItemPosition and Adapter.notifyDataSetChanged(),
to show selected fragment by mViewPager.setCurrentItem(i)
Code snippets, detail refer to https://github.com/li2/Update_Replace_Fragment_In_ViewPager/
private FragmentPagerAdapter mViewPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) {
#Override
public int getCount() {
return PAGE_COUNT;
}
// Return the Fragment associated with a specified position.
#Override
public Fragment getItem(int position) {
Log.d(TAG, "getItem(" + position + ")");
if (position == 0) {
return Page0Fragment.newInstance(mDate);
} else if (position == 1) {
return Page1Fragment.newInstance(mContent);
}
return null;
}
#Override
// To update fragment in ViewPager, we should override getItemPosition() method,
// in this method, we call the fragment's public updating method.
public int getItemPosition(Object object) {
Log.d(TAG, "getItemPosition(" + object.getClass().getSimpleName() + ")");
if (object instanceof Page0Fragment) {
((Page0Fragment) object).updateDate(mDate);
} else if (object instanceof Page1Fragment) {
((Page1Fragment) object).updateContent(mContent);
}
return super.getItemPosition(object);
};
};
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.
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.
Does the Back Stack support interaction with nested Fragments in Android?
If it does, what am I doing wrong? In my implementation, the back button is completely ignoring the fact that I added this transaction to the back stack. I'm hoping it is not because of an issue with nested fragments and just me doing something incorrectly.
The following code is inside of one of my fragments and is used to swap a new fragment with whatever nested fragment is currently showing:
MyFragment fragment = new MyFragment();
FragmentTransaction ft = getChildFragmentManager().beginTransaction();
ft.setCustomAnimations(R.animator.slide_in_from_right, R.animator.slide_out_left, R.animator.slide_in_from_left, R.animator.slide_out_right);
ft.addToBackStack(null);
ft.replace(R.id.myFragmentHolder, fragment);
ft.commit();
I have the same problem, I would like to nest fragments, and to keep a back stack for each nested fragment.
But... it seems that this case is not handled by the v4 support library. In the FragmentActivity code in the library, I can find :
public void onBackPressed() {
if (!mFragments.popBackStackImmediate()) {
finish();
}
}
The mFragments represents the FragmentManager of the activity, but it does not seem this manager "propagates" the pop to children managers.
A workaround would be to manually call the popBackStackImmediate() on the child manager, like this in the activity inherited from FragmentActivity :
private Fragment myFragmentContainer;
#Override
public void onBackPressed() {
if (!myFragmentContainer.getChildFragmentManager().popBackStackImmediate()) {
finish(); //or call the popBackStack on the container if necessary
}
}
There might be a better way, and a more automated way, but for my needs it is allright.
In my current project we have multiple "nested layers" so I've come up with following workaround to automatically pop backstack only for top level fragment managers:
#Override
public void onBackPressed() {
SparseArray<FragmentManager> managers = new SparseArray<>();
traverseManagers(getSupportFragmentManager(), managers, 0);
if (managers.size() > 0) {
managers.valueAt(managers.size() - 1).popBackStackImmediate();
} else {
super.onBackPressed();
}
}
private void traverseManagers(FragmentManager manager, SparseArray<FragmentManager> managers, int intent) {
if (manager.getBackStackEntryCount() > 0) {
managers.put(intent, manager);
}
if (manager.getFragments() == null) {
return;
}
for (Fragment fragment : manager.getFragments()) {
if (fragment != null) traverseManagers(fragment.getChildFragmentManager(), managers, intent + 1);
}
}
As of API 26 there is a setPrimaryNavigationFragment method in FragmentTransaction that can be used to
Set a currently active fragment in this FragmentManager as the primary navigation fragment.
This means that
The primary navigation fragment's child FragmentManager will be called first to process delegated navigation actions such as FragmentManager.popBackStack() if no ID or transaction name is provided to pop to. Navigation operations outside of the fragment system may choose to delegate those actions to the primary navigation fragment as returned by FragmentManager.getPrimaryNavigationFragment().
As mentioned by #la_urre in the accepted answer and as you can see in FragmentActivity's source, the FragmentActivity's onBackPressed would call its FragmentManager's popBackStackImmediate() which in turn would check whether there is an mPrimaryNav (primary navigation, I assume) fragment, get its child fragment manager and pop its backstack.
#Override
public void onBackPressed() {
FragmentManager fm = getSupportFragmentManager();
if (fm.getBackStackEntryCount() > 0) {
fm.popBackStack();
return;
}
finish();
}