Saturday, March 16, 2013

Android nested Fragments in practice

Last November I wrote about the new feature in rev11 of Android support package - Fragments nesting. Recently I had an opportunity to use this feature in practice and I'd like to share my experience with it.

The basics are simple: each FragmentActivity and each Fragment has it's own FragmentManager. Inside the Fragment you may call getFragmentManager() to get the FragmentManager this Fragment was added to, or getChildFragmentManager() to get the FragmentManager which can be used to nest Fragments inside this Fragment. This basic flow works fine, but I have found two issues.

If you have a Fragment with nested Fragments and you save its state with saveFragmentInstanceState() and try to use it in setInitialSavedState() on another instance of this Fragment, you'll get the BadParcelableException from onCreate. Fortunately it's an obvious bug which is easy to fix: you just need to set the correct ClassLoader for a Bundle containing this Fragment's state. There is a patch for it in support library project Gerrit, and if you need this fix ASAP you may use this fork of support lib on Github.

The second issue is related with the Fragments backstack. Inside each FragmentManager you may build stack of Fragments with FragmentTransaction.addToBackStack() and later on use popBackStack() to go back to the previous state. Pressing hardware back key is also supposed to pop the Fragments from the back stack, but it doesn't take into account any nested Fragments, only Fragments added to the Activity's FragmentManager. This is not so easy to fix, but you may use the following workaround:
String FAKE_BACKSTACK_ENTRY = "fakeBackstackEntry";

getFragmentManager()
    .beginTransaction()
    .addToBackStack(null)
    // call replace/add
    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
    .commit();

final FragmentManager rootFragmentManager = getActivity().getSupportFragmentManager();

rootFragmentManager
    .beginTransaction()
    .addToBackStack(null)
    .add(new Fragment(), FAKE_BACKSTACK_ENTRY)
    .commit();

rootFragmentManager.addOnBackStackChangedListener(new OnBackStackChangedListener() {
  @Override
  public void onBackStackChanged() {
    if (rootFragmentManager.findFragmentByTag(FAKE_BACKSTACK_ENTRY) == null) {
      getFragmentManager().popBackStack();
      rootFragmentManager.removeOnBackStackChangedListener(this);
    }
  }
});
Quick explanation: together with the actual backstack entry we want to add, we also add the fake backstack entry with empty Fragment to top level FragmentManager and set up OnBackStackChangedListener. When user presses hardware back button, the fake backstack entry is popped, the backstack listener is triggered and our implementation pops the backstack inside our Fragment. The backstack listeners are not persisted throughout the orientation change, so we need to setup it again inside onCreate().

Note that there are two issues with this workaround: it allows adding only one backstack entry and this setup won't be automatically recreated from state saved by saveFragmentInstanceState() (fortunately it does work with orientation change). Both issues probably can be solved by some additional hacks, but writing workarounds for workarounds is not something I do unless I really have to, and in this case I neither issue affected me.

Besides those bumps the nested Fragments are a real blessing which allows much more cleaner and reusable code.