How to squash fragments on the backstack which match by their fragment tag?
Asked Answered
P

3

6

The layout of my Android tablet app consists of a list of items and a details view. When a list item is selected the associated content is displayed in the details view.

+--------+-------------+ 
| Item 1 |             |
+--------+    Item     |
| Item 2 |   details   |
+--------+             |
| Item 3 |             |
+--------+-------------+

The details view is a Fragment which is programmatically inflated into a FrameLayout placeholder:

<FrameLayout
    android:id="@+id/detail_fragment_placeholder"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

Here is the Fragment operation:

getSupportFragmentManager()
    .beginTransaction()
    .replace(containerViewId, fragment, fragmentTag)
    .addToBackStack(backStackStateName)
    .commit();

Multiple instances [Dx] of the DetailsFragment are added to the backstack when the user selects one item after another.

                [D3]
        [D2]    [D2]
[D1] -> [D1] -> [D1]

Therefore, the user needs to press the BACK button multiple times to pop the instances from the backstack to empty the details view.

How can I replace an existing instance [Dx] of DetailsFragment on the backstack when the fragmentTag of the existing fragment matches the fragmentTag of a new fragment?

[D1] -> [D2] -> [D3]
Parent answered 22/1, 2016 at 18:30 Comment(4)
Do you want to replace only the top fragment if it has the same tag or any fragment with the same tag on the stack?Melda
@Melda If there is a group of fragments with the same tag aka. same type on top of the stack I want to reduce/replace them with the single fragment being passed in.Parent
There is no simple way to achieve this. Check this answer https://mcmap.net/q/605428/-android-reorder-fragment-backstackUniaxial
try to find current details fragment, remove it first, and than add new details fragment to the same container.Nephelinite
M
1

I'm not sure I understand your question correctly. If you want to replace multiple fragments with some tag from the top of the backstack with a single fragment with the same tag then you can use the following approach.

Instead of using tags for identifying fragments set different backstack names for fragments of different types. You can still use a fragment tag but it will not help in solving this particular problem. Then remove fragments from the backstack manually one by one until there's a fragment with a different backstack name on the top or no fragments left.

  public void addFragment(final int containerViewId, final Fragment fragment,
      final String backStackName, final boolean replace) {
    final FragmentManager fragmentManager = getSupportFragmentManager();

    if (replace) {
      while (fragmentManager.getBackStackEntryCount() > 0) {
        final int last = fragmentManager.getBackStackEntryCount() - 1;
        final FragmentManager.BackStackEntry entry = 
            fragmentManager.getBackStackEntryAt(last);
        if (!TextUtils.equals(entry.getName(), backStackName)) {
          break;
        }

        fragmentManager.popBackStackImmediate();
      }
    }

    fragmentManager
        .beginTransaction()
        .replace(containerViewId, fragment)
        .addToBackStack(backStackName)
        .commit();
    fragmentManager.executePendingTransactions();
  }

Now if you make the following calls your backstack will contain just fragment1 and fragment4.

addFragment(R.id.container, fragment1, "D2", false);
addFragment(R.id.container, fragment2, "D1", false);
addFragment(R.id.container, fragment3, "D1", false);
addFragment(R.id.container, fragment4, "D1", true);

UPDATE:

In this particular case the following code was enough:

getSupportFragmentManager().popBackStack(
    backStackStateName, FragmentManager.POP_BACK_STACK_INCLUSIVE);
getSupportFragmentManager()
    .beginTransaction()
    .replace(containerViewId, fragment, fragmentTag)
    .addToBackStack(backStackStateName)
    .commit();

https://github.com/tuxmobil/CampFahrplan/pull/148

Melda answered 11/2, 2016 at 20:19 Comment(11)
Thanks for the approach. However, it does not work for me: click on item 1 - detail 1 is shown, click on item 2 - detail views is empty. Please note that you can test the patch yourself. - Also, why do you finish with executePendingTransactions()?Parent
The code sample from the answer works fine but you just used it incorrectly. Call sidePane.setVisibility(View.VISIBLE); after popping fragments from the backstack, immediately before starting a transaction. executePendingTransactions() is necessary to avoid race conditions when popping and pushing fragments. We pop fragments synchronously, so we need to push them synchronously too.Melda
Interesting. I released also Daniel Nugent's 2. solution works if I add sidePane.setVisibility(View.VISIBLE);. Is there a particular reason why you suggest to toggle the visibility after popping fragments / before starting a transaction? It also worked when I call it after all transactions are through.Parent
You can show the view after committing the transaction. The only thing necessary is to show it after popping fragments. Daniel's solution will not work for fragments with different tags. And it contains a race condition that I mentioned in the previous comment. So when using it you may end up with having multiple detail fragments in the backstack.Melda
Fine. I asked because it is easier to extract the remaining calls into a separate method when I can append sidePane.setVisibility(View.VISIBLE); Can you state why the view is not visible automatically when a replacement happens?Parent
You hide the view in onBackStackChanged(). Fragments get popped synchronously, so this method is called and it hides just shown view.Melda
Great finding (I did not write most of the code). So I can move sidePane.setVisibility(View.VISIBLE); into onBackStackChanged() - just tested it - seems to work.Parent
After analyzing your code a little I can assume that you don't need all this complexity to pop detail fragments from the stack. Just use a separate backstack name for all detail fragments and do popBackStack(FragmentStack.DETAIL, FragmentManager.POP_BACK_STACK_INCLUSIVE);. No executePendingTransactions() is necessary in this case.Melda
Simplified code is even better. If you like you can directly offer a pull request against tuxmobil/master to resolve the associated issue. Since you came up with the solution this would be the fairest.Parent
I like to grant the answer flag - although the final solution appear to be much shorter. Thank you! Please note that tuxmobil is in charge of merging pull requests - it might take a while.Parent
I added the final solution to the answer.Melda
B
1

You could use the tag and handle it in onBackPressed(), but I think it would be a cleaner solution to handle it while constructing the back stack. Selectively add to the back stack for each FragmentTransaction, and only add to the back stack if it's the first instance of the DetailsFragment.

Here is a simple example that prevents any given Fragment from being added to the back stack twice in a row:

public void replaceFragment(Fragment frag) {
    FragmentManager fm = getSupportFragmentManager();

    if (fm != null){
        FragmentTransaction t = fm.beginTransaction();
        //you could also use containerViewId in place of R.id.detail_fragment_placeholder
        Fragment currentFrag = fm.findFragmentById(R.id.detail_fragment_placeholder);
        if (currentFrag != null && currentFrag.getClass().equals(frag.getClass())) {
            t.replace(R.id.detail_fragment_placeholder, frag).commit();
        } else {
            t.replace(R.id.detail_fragment_placeholder, frag).addToBackStack(null).commit();
        }
    }
}
Bilocular answered 22/1, 2016 at 18:59 Comment(5)
Does your content_frame match my detail_fragment_placeholder - if so please update your code snippet so it is easier to understand. Does your solution mean that one or more instances of the DetailFragment class are represented as one backstack entry - as in n:1?Parent
@jjd yes, content_frame matches your detail_fragment_placeholder id, and you could just use containerViewId as you are now. I'll update the answer! As for how it works, it only adds the first Fragment of the same type to the backstack, so now matter how many levels deep you go with DetailsFragments, if you tap back, you go back to the top level before navigating to the DetailsFragment for the first time.Bilocular
None of both approaches does exactly what I want. Your 1st approach - click on item 1 - detail 1 is shown, click on item 2 - detail 2 is shown, click on BACK - detail 2 is still shown, click on BACK - app goes into background. --- Your 2nd approach: click on item 1 - detail 1 is shown, click on item 2 - detail views is empty.Parent
@Parent that is strange, the first approach works well for me. I haven't tested the second approach, I'll see if I can get it working when I have time.Bilocular
You can test it here yourself. Run ./gradlew clean assembleCcc32c3Debug.Parent
M
1

I'm not sure I understand your question correctly. If you want to replace multiple fragments with some tag from the top of the backstack with a single fragment with the same tag then you can use the following approach.

Instead of using tags for identifying fragments set different backstack names for fragments of different types. You can still use a fragment tag but it will not help in solving this particular problem. Then remove fragments from the backstack manually one by one until there's a fragment with a different backstack name on the top or no fragments left.

  public void addFragment(final int containerViewId, final Fragment fragment,
      final String backStackName, final boolean replace) {
    final FragmentManager fragmentManager = getSupportFragmentManager();

    if (replace) {
      while (fragmentManager.getBackStackEntryCount() > 0) {
        final int last = fragmentManager.getBackStackEntryCount() - 1;
        final FragmentManager.BackStackEntry entry = 
            fragmentManager.getBackStackEntryAt(last);
        if (!TextUtils.equals(entry.getName(), backStackName)) {
          break;
        }

        fragmentManager.popBackStackImmediate();
      }
    }

    fragmentManager
        .beginTransaction()
        .replace(containerViewId, fragment)
        .addToBackStack(backStackName)
        .commit();
    fragmentManager.executePendingTransactions();
  }

Now if you make the following calls your backstack will contain just fragment1 and fragment4.

addFragment(R.id.container, fragment1, "D2", false);
addFragment(R.id.container, fragment2, "D1", false);
addFragment(R.id.container, fragment3, "D1", false);
addFragment(R.id.container, fragment4, "D1", true);

UPDATE:

In this particular case the following code was enough:

getSupportFragmentManager().popBackStack(
    backStackStateName, FragmentManager.POP_BACK_STACK_INCLUSIVE);
getSupportFragmentManager()
    .beginTransaction()
    .replace(containerViewId, fragment, fragmentTag)
    .addToBackStack(backStackStateName)
    .commit();

https://github.com/tuxmobil/CampFahrplan/pull/148

Melda answered 11/2, 2016 at 20:19 Comment(11)
Thanks for the approach. However, it does not work for me: click on item 1 - detail 1 is shown, click on item 2 - detail views is empty. Please note that you can test the patch yourself. - Also, why do you finish with executePendingTransactions()?Parent
The code sample from the answer works fine but you just used it incorrectly. Call sidePane.setVisibility(View.VISIBLE); after popping fragments from the backstack, immediately before starting a transaction. executePendingTransactions() is necessary to avoid race conditions when popping and pushing fragments. We pop fragments synchronously, so we need to push them synchronously too.Melda
Interesting. I released also Daniel Nugent's 2. solution works if I add sidePane.setVisibility(View.VISIBLE);. Is there a particular reason why you suggest to toggle the visibility after popping fragments / before starting a transaction? It also worked when I call it after all transactions are through.Parent
You can show the view after committing the transaction. The only thing necessary is to show it after popping fragments. Daniel's solution will not work for fragments with different tags. And it contains a race condition that I mentioned in the previous comment. So when using it you may end up with having multiple detail fragments in the backstack.Melda
Fine. I asked because it is easier to extract the remaining calls into a separate method when I can append sidePane.setVisibility(View.VISIBLE); Can you state why the view is not visible automatically when a replacement happens?Parent
You hide the view in onBackStackChanged(). Fragments get popped synchronously, so this method is called and it hides just shown view.Melda
Great finding (I did not write most of the code). So I can move sidePane.setVisibility(View.VISIBLE); into onBackStackChanged() - just tested it - seems to work.Parent
After analyzing your code a little I can assume that you don't need all this complexity to pop detail fragments from the stack. Just use a separate backstack name for all detail fragments and do popBackStack(FragmentStack.DETAIL, FragmentManager.POP_BACK_STACK_INCLUSIVE);. No executePendingTransactions() is necessary in this case.Melda
Simplified code is even better. If you like you can directly offer a pull request against tuxmobil/master to resolve the associated issue. Since you came up with the solution this would be the fairest.Parent
I like to grant the answer flag - although the final solution appear to be much shorter. Thank you! Please note that tuxmobil is in charge of merging pull requests - it might take a while.Parent
I added the final solution to the answer.Melda
J
0

Simply find the fragment on the backstack and replace it:

Fragment fragment = getSupportFragmentManager().findFragmentByTag("your tag");
FragmentTransaction transaction = fm.beginTransaction();
transaction.replace(R.id.fragment_container, fragment, fragment.getClass().getName());
transaction.addToBackStack("your tag");
transaction.commit();

And in your OnClick event, check if the position of the item does not match the current item that is displayed already. If it does, do nothing.

Jaime answered 24/1, 2016 at 13:2 Comment(9)
You answered with the same code snippet which you can find in my question. Moreover, I do not click the same item but each item is associated with an instance of the DetailsFragment class.Parent
Maybe I did not understand your question. Was your problem not to retrieve a certain fragment from the backstack?Jaime
The problem is that "the user needs to press the BACK button multiple times".Parent
how about using fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);Jaime
This would pop all back stack entries. I just want to pop the instances of the DetailsFragment class.Parent
well, that would defeat the purpose of the "stack" data structure. a workaround will be to pop each transaction from the stack and add back the ones that are not instances of DetailsFragmentJaime
Partly true - but in my case these instances are the top most entries. - Out of interest: How would you get access to the popped entries to add them back?Parent
finding them with findFragmentByTag and then popBackStackJaime
Using findFragmentByTag would require to keep track of all kind of tags which can be added to the backstack. In fact there are other instances shown in place of the details view besides DetailsFragment instances.Parent

© 2022 - 2024 — McMap. All rights reserved.