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]
JJD
  • 50,076
  • 60
  • 203
  • 339
  • 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? – Michael Feb 10 '16 at 19:38
  • @Michael 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. – JJD Feb 10 '16 at 22:55
  • 1
    There is no simple way to achieve this. Check this answer http://stackoverflow.com/a/22125826/1320616 – Ankit Aggarwal Feb 11 '16 at 09:46
  • try to find current details fragment, remove it first, and than add new details fragment to the same container. – Veaceslav Gaidarji Feb 11 '16 at 13:11

3 Answers3

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();
        }
    }
}
Daniel Nugent
  • 43,104
  • 15
  • 109
  • 137
  • 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`? – JJD Jan 23 '16 at 11:39
  • @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. – Daniel Nugent Jan 24 '16 at 19:21
  • 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. – JJD Jan 25 '16 at 16:38
  • @JJD 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. – Daniel Nugent Jan 25 '16 at 17:03
  • You can test it [here](https://github.com/tuxmobil/CampFahrplan/blob/master/app/src/main/java/nerd/tuxmobil/fahrplan/congress/MainActivity.java#L394) yourself. Run `./gradlew clean assembleCcc32c3Debug`. – JJD Jan 25 '16 at 21:21
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

Michael
  • 53,859
  • 22
  • 133
  • 139
  • 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](http://stackoverflow.com/questions/34953590/how-to-squash-fragments-on-the-backstack-which-match-by-their-fragment-tag?noredirect=1#comment57733286_34953991). - Also, why do you finish with `executePendingTransactions()`? – JJD Feb 12 '16 at 18:07
  • 1
    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. – Michael Feb 13 '16 at 11:24
  • 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. – JJD Feb 13 '16 at 12:39
  • 1
    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. – Michael Feb 13 '16 at 12:47
  • 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? – JJD Feb 13 '16 at 12:54
  • 1
    You hide the view in `onBackStackChanged()`. Fragments get popped synchronously, so this method is called and it hides just shown view. – Michael Feb 13 '16 at 12:59
  • 1
    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. – JJD Feb 13 '16 at 13:12
  • 1
    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. – Michael Feb 13 '16 at 13:25
  • Simplified code is even better. If you like you can directly offer a **pull request** against *tuxmobil/master* to resolve [the associated issue](https://github.com/tuxmobil/CampFahrplan/issues/130). Since you came up with the solution this would be the fairest. – JJD Feb 13 '16 at 13:34
  • 1
    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. – JJD Feb 14 '16 at 12:10
  • 1
    I added the final solution to the answer. – Michael Feb 14 '16 at 16:04
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.

gilgil28
  • 540
  • 5
  • 12
  • 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. – JJD Jan 24 '16 at 13:44
  • Maybe I did not understand your question. Was your problem not to retrieve a certain fragment from the backstack? – gilgil28 Jan 24 '16 at 13:50
  • The problem is that "the user needs to press the *BACK* button multiple times". – JJD Jan 24 '16 at 13:53
  • how about using `fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);` – gilgil28 Jan 24 '16 at 13:56
  • This would pop all back stack entries. I just want to pop the instances of the `DetailsFragment` class. – JJD Jan 24 '16 at 14:06
  • 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 `DetailsFragment` – gilgil28 Jan 24 '16 at 14:20
  • 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? – JJD Jan 24 '16 at 14:29
  • finding them with findFragmentByTag and then popBackStack – gilgil28 Jan 24 '16 at 14:36
  • 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. – JJD Jan 24 '16 at 21:44