Nested Fragments and The Back Stack - android

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

Related

Navigating back from nested fragments in ViewPager

I need to display multiple set of instructions to the user. For each instruction(FragmentA) the user can navigate to another screen (FragmentA1). I have used a ViewPager that hold list of fragments. When user navigates to the first fragment(FragmentA) the user can click a button and move to a (FragmentA1) detailed view of the instruction. So each page of the viewpager is capable of opening another fragment.
All works fine till here. Issue I am facing is with the backstack. The activity with the viewpager adapter handles the moveToNext() and moveToPrevious() methods. Below is my implementation of onBackPressed() method:
#Override
public void onBackPressed() {
FragmentManager fm = getSupportFragmentManager();
for (Fragment frag : fm.getFragments()) {
if (frag.isVisible()) {
FragmentManager fm = frag.getFragmentManager();
if (fm.getBackStackEntryCount() > 0) {
fm.popBackStack();
return;
} else {
moveToPrevious();
return;
}
}
}
super.onBackPressed();
}
With the above implementation is:
If I traverse FragA->FragA1->FragB->FragB1->FragC->FragC1
When I am at FragC1 and I press back button, then I am directly navigated to FragB1 instead of FragC and then to FragA1. I need to follow the same path backwards as traversed forward.
I am not sure what is wrong but it is not able to pop the nested fragment and display its parent fragment.
Shouldn't fm.popBackStack() show the parent fragment ?
I solved it this way. Get the fragment that is visible. When there are no child fragments to pop anymore just move to previous page.
#Override
public void onBackPressed() {
FragA fragment = (FragA) pagerAdapter.instantiateItem(viewPager, viewPager.getCurrentItem());
if (fragment.getChildFragmentManager().getBackStackEntryCount() != 0) {
fragment.getChildFragmentManager().popBackStack();
}
else {
moveToPrevious();
}
}

Exiting from Activity with only fragments

My app contains one empty activity and a couple of fragments. The onCreate of the activity replaces the empty view in activity_main.xml with a MainFragment that contains some buttons. Each button launches a separate fragment, and user can navigate from one fragment to another, etc.
On the press of back key, the current fragment correctly gets replaced with the previous fragment, until you get to the MainFragment. When user presses back from MainFragment, it hides the main fragment and you see the white empty background of the main activity. But I want to exit from the activity at this point, as that would be the sensible behaviour.
I am able to achieve this by calling super.onBackPressed() for a second time from onBackPressed if there are no fragments left in the fragment manager.
#Override
public void onBackPressed() {
super.onBackPressed();
FragmentManager manager = getSupportFragmentManager();
List<Fragment> fragments = manager.getFragments();
if (fragments == null || fragments.size() == 0) {
Log.d(TAG, "No more fragments: exit");
super.onBackPressed();
}
}
Is this acceptable thing to do - would it create any issues in the activity workflow? Is there a better/standard way to handle this scenario?
There is no problem to do that, but probably it would be easier if when you add the main fragment to the activity you do NOT call .addToBackStack()
You don't really need to override onBackPressed in your Activity. I would suggest implementing a method for adding fragments in your Activity:
protected void addFragment(Fragment fragment, boolean addToBackStack) {
String tag = fragment.getClass().getName(); //It's optional, may be null
FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction()
.add(R.id.your_container_id, fragment, tag);
if (addToBackStack) {
transaction.addToBackStack(tag);
}
transaction.commit();
}
And modify your onCreate method of activity like in the following snippet:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
// Add your fragment only if it is a first launch,
// otherwise it will be restored by system
addFragment(new YourFirstFragment(), false);
}
}
For all other fragments use:
addFragment(new OtherFragment(), true);

popBackStackImmediate not showing fragment when the current fragment transaction is not added in the backstack

Popbackstack is working fine when all the fragments in the sequence are added in the backstack but isnt working when one of the transactions is not added in the backstack.
Here is my navigation:
1.Replace fragment to load home fragment. This transaction not added to backstack.
Replace fragment to load login fragment. This transaction is added to backstack.
3.Replace fragment to load loggedin fragment. This transaction is not added to backstack.
Now, when i press back button once nothing happens. Whereas ideally it should go to the home fragment from logged in fragment.
Here is my onbackpressed method in main activity:
#Override
public void onBackPressed() {
if(getSupportFragmentManager().getBackStackEntryCount()>0)
{
FragmentManager.BackStackEntry backStackEntry = getSupportFragmentManager().getBackStackEntryAt(getSupportFragmentManager().getBackStackEntryCount() - 1);
String str = backStackEntry.getName();
FragmentManager fm=getSupportFragmentManager();
//getSupportFragmentManager().popBackStackImmediate();
fm.popBackStack(str, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
else {
super.onBackPressed();
}
}
popBackstack only 'pop' what is in the backstack.
Since you haven't add the transaction when replacing the LoginFragment by the LoggedInFragment when you press back:
the LoggedInFragment remains,
the LogInFragment is popped
the HomeFragment is displayed
But because the LoggedInFragment as been added after the HomeFragment, the HomeFragment is displayed underneath it. So you can't see it as hidden by the LoggedInFragment.
One solution is to add the transaction to the back stack when you replace the LogInFragment by the LoggedInFragment.
Then in onBackPressed you test if the current fragment is the LoggedInFragment. If it's the case you pop the back stack up to HomeFragment (not inclusive). Like that both LoggedInFragment and LogInFragment will be pop.
EDIT
#Override
public void onBackPressed() {
FragmentManager manager = getSupportFragmentManager();
Fragment fragment = manager.findFragmentById(R.id.my_fragment_container);
// If there is something in the back stack AND the current fragment is the LoggedInFragment
if (manager.getBackStackEntryCount() > 0
&& fragment instanceof LoggedInFragment) {
manager.popBackStack(HomeFragment.class.getSimpleName(), 0);
} else {
super.onBackPressed();
}
}
In order to retrieve the HomeFragment by name you need to tag your transaction when you replace the current fragment by the HomeFragment. Generally I tag all transactions with the fragment's class simple name so like that I can retried any fragment:
transaction.replace(R.id.my_fragment_container, fragment, fragment.getClass().getSimpleName());
Eselfar's explanation of the problem is correct, but the code he provided wasn't generic enough for me.
I (hopefully) resolved this issue in a general case by the following code:
#Override
public void onBackPressed() {
Fragment currentFragment = getCurrentFragment();
if (mFragmentManager.getBackStackEntryCount() > 0) {
// In a normal world, just popping back stack would be sufficient, but since android
// is not normal, a call to popBackStack can leave the popped fragment on screen.
// Therefore, we start with manual removal of the current fragment.
removeCurrentFragment();
mFragmentManager.popBackStack();
} else {
super.onBackPressed();
}
}
private Fragment getCurrentFragment() {
return mFragmentManager.findFragmentById(getContentFrameId());
}
private void removeCurrentFragment() {
FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.remove(getCurrentFragment());
ft.commit();
// not sure it is needed; will keep it as a reminder to myself if there will be problems
// mFragmentManager.executePendingTransactions();
}

Android Fragment setRetainInstance(true) is not working on backstack

Hi I want to keep a Fragment alive even if it is not shown anymore. Because I have some AsyncTasks going there.
Firstly I am adding a starting point Fragment
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.fragmentMenu, menuFragment);
fragmentTransaction.commit();
Later on I replace menuFragment with the Fragment which should stay alive
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.fragmentMenu, btFragment).addToBackStack(null).commit();
Lastly I override the onBackPressed() method for using popBackStack()
#Override
public void onBackPressed() {
if(getFragmentManager().getBackStackEntryCount() != 0){
getFragmentManager().popBackStackImmediate();
}else{
super.onBackPressed();
}
}
Inside my btFragment which should stay alive even if I pop another Fragment I have set setRetainInstance(true) inside the onCreate() method.
But it is getting destroyed as soon as I pop the backstack.
Am I doing something wrong? thx
The documentation for setRetainInstance() explains that it applies only to activity recreation:
Control whether a fragment instance is retained across Activity
re-creation (such as from a configuration change)
I solved it with a little bit of a workaround. Here is the code if anyone is interested in it.
// little work-around to not let btFragment die.
Fragment fragment = getFragmentManager().findFragmentById(R.id.fragmentMenu);
if (fragment instanceof BTFragment) {
getFragmentManager().beginTransaction().hide(fragment).add(R.id.fragmentMenu, menuFragment).commit();
} else if (getFragmentManager().getBackStackEntryCount() != 0) {
getFragmentManager().popBackStackImmediate();
} else {
super.onBackPressed();
}
Basically I'm hiding the Fragment instead of removing it and by checking the FrameLayout for the current Fragment I can handle different Fragments. With this way, the onDestroy() method is not called on my btFragment.

In Fragment on back button pressed Activity is blank

I have an Activity and many fragments inflated in same FrameLayout
<FrameLayout
android:id="#+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
example: mainActivity > any fragment (press back button) > activity is blank.
In onCreate:
layout = (FrameLayout)findViewById(R.id.content_frame);
layout.setVisibility(View.GONE);
When I start a fragment:
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, profileFragment);
ft.addToBackStack(null);
ft.commit();
layout.setVisibility(View.VISIBLE);
I suppose I need to make the frameLayout's visibility GONE again on back pressed, but how do I do this?
I tried onBackPressed and set layout.setVisibility(View.GONE); but I cannot go back through fragments, as I go directly to main page.
If you have more than one fragment been used in the activity or even if you have only one fragment then the first fragment should not have addToBackStack defined. Since this allows back navigation and prior to this fragment the empty activity layout will be displayed.
// fragmentTransaction.addToBackStack() // dont include this for your first fragment.
But for the other fragment you need to have this defined otherwise the back will not navigate to earlier screen (fragment) instead the application might shutdown.
#Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
}
else {
int fragments = getSupportFragmentManager().getBackStackEntryCount();
if (fragments == 1) {
finish();
} else if (getFragmentManager().getBackStackEntryCount() > 1) {
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
}
To add a fragment
getSupportFragmentManager().beginTransaction()
.replace(R.id.layout_main, dashboardFragment, getString(R.string.title_dashboard))
.addToBackStack(getString(R.string.title_dashboard))
.commit();
Sorry for the late response.
You don't have to add ft.addToBackStack(null); while adding first fragment.
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, profileFragment);
// ft.addToBackStack(null); --remove this line.
ft.commit();
// ... rest of code
If you want to track by the fragments you should override the onBackPressed method, like this
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() == 1) {
finish();
} else {
super.onBackPressed();
}
}
You can override onBackPressed and check to see if there is anything on the backstack.
#Override
public void onBackPressed() {
int fragments = getFragmentManager().getBackStackEntryCount();
if (fragments == 1) {
// make layout invisible since last fragment will be removed
}
super.onBackPressed();
}
Just don't add the first fragment to back stack
Here is the Kotlin code that worked for me.
val ft = supportFragmentManager.beginTransaction().replace(container, frag)
if (!supportFragmentManager.fragments.isEmpty()) ft.addToBackStack(null)
ft.commit()
On a recent personal project, I solved this by not calling addToBackStack if the stack is empty.
// don't add the first fragment to the backstack
// otherwise, pressing back on that fragment will result in a blank screen
if (fragmentManager.getFragments() != null) {
transaction.addToBackStack(tag);
}
Here's my full implementation:
String tag = String.valueOf(mCurrentSectionId);
FragmentManager fragmentManager = mActivity.getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment != null) {
// if the fragment exists then no need to create it, just pop back to it so
// that repeatedly toggling between fragments doesn't create a giant stack
fragmentManager.popBackStackImmediate(tag, 0);
} else {
// at this point, popping back to that fragment didn't happen
// So create a new one and then show it
fragment = createFragmentForSection(mCurrentSectionId);
FragmentTransaction transaction = fragmentManager.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.replace(R.id.main_content, fragment, tag);
// don't add the first fragment to the backstack
// otherwise, pressing back on that fragment will result in a blank screen
if (fragmentManager.getFragments() != null) {
transaction.addToBackStack(tag);
}
transaction.commit();
}
irscomp's solution works if you want to end activity when back button is pressed on first fragment. But if you want to track all fragments, and go back from one to another in back order, you add all fragments to stack with:
ft.addToBackStack(null);
and then, add this to the end of onCreate() to avoid blank screen in last back pressed; you can use getSupportFragmentManager() or getFragmentManager() depending on your API:
FragmentManager fm = getSupportFragmentManager();
fm.addOnBackStackChangedListener(new OnBackStackChangedListener() {
#Override
public void onBackStackChanged() {
if(getSupportFragmentManager().getBackStackEntryCount() == 0) finish();
}
});
Final words: I don't suggest you to use this solution, because if you go from fragment1 to fragment 2 and vice versa 10 times, when you press back button 10 times it will do it in back order which users will not want it.
Almost same as Goodlife's answer, but in Xamarin.Android way:
Load fragment (I wrote helper method for that, but it's not necessary):
public void LoadFragment(Activity activity, Fragment fragment, string fragmentTitle = "")
{
var fragmentManager = activity.FragmentManager;
var fragmentTransaction = fragmentManager.BeginTransaction();
fragmentTransaction.Replace(Resource.Id.mainContainer, fragment);
fragmentTransaction.AddToBackStack(fragmentTitle);
fragmentTransaction.Commit();
}
Back button (in MainActivity):
public override void OnBackPressed()
{
if (isNavDrawerOpen()) drawerLayout.CloseDrawers();
else
{
var backStackEntryCount = FragmentManager.BackStackEntryCount;
if (backStackEntryCount == 1) Finish();
else if (backStackEntryCount > 1) FragmentManager.PopBackStack();
else base.OnBackPressed();
}
}
And isNavDrawerOpen method:
bool isNavDrawerOpen()
{
return drawerLayout != null && drawerLayout.IsDrawerOpen(Android.Support.V4.View.GravityCompat.Start);
}
I still could not fix the issue through getBackStackEntryCount() and I solved my issue by making the main page a fragment too, so in the end I have an activity with a FrameLayout only; and all other fragments including the main page I inflate into that layout. This solved my issue.
I had the same problem when dealing with Firebase's Ui Login screen. When back button was pressed it left a blank screen.
To solve the problem I just called finish() in my onStop() method for said Activity. Worked like a charm.
If you have scenario like me where a list fragment opens another details fragment, and on back press you first need to show the list fragment and then get out the whole activity then, addToBackStack for all the fragment transactions.
and then on the activity, do like this (courtesy: #JRomero's answer, #MSaudi's comment)
#Override
public void onBackPressed() {
int fragments = getFragmentManager().getBackStackEntryCount();
if (fragments == 1) {
// make layout invisible since last fragment will be removed
}
super.onBackPressed();
}
Just Comment or Remove transaction.addToBackStack(null) in your code.Below is code to change fragment in kotlin.
fun ChangeFragment(activity: MainActivity, fragment: Fragment) {
val transaction: FragmentTransaction =
activity.getSupportFragmentManager().beginTransaction()
transaction.replace(R.id.tabLayoutContainer, fragment)
transaction.commit()
}

Categories

Resources