Sunday, February 9, 2014

Nested Fragments and the Backstack - Part 3

This is the third post in the series about Nested Fragments and the Back Stack in Android. Read the previous posts here:

  1. Part 1
  2. Part 2

The first two posts have looked at the topic taking ViewPager as an example. I have also mentioned repeatedly that this is not the only use case for having to maintain the back-stack of nested fragments. One use case that I threw up often in comments was about Navigation Drawers. That is exactly what this post will look into.


EDIT: Some Google engineers, including the creators of the Android framework have expressed their reservations regarding this article. Read this G+ thread for more details. They point out that using an Application sub-class to save state is not a good idea, but also that saving Fragment instance state explicitly might in itself needs to be considered carefully. I hope to gather their thoughts and write a follow-up post in the coming weeks. Stay Tuned.


Re-cap

Just to re-cap the conclusion from the previous article:

  • Consider pro-actively saving your Fragment states in onPause, particularly is the Fragment happens to nest other fragments inside of it.
  • Do not rely solely on the system saving state for you in onSaveInstanceState.
  • Use FragmentManager#saveFragmentInstanceState to save the Fragment state including the back-stack of nested fragment transactions for you.
  • Do not hold on to the saved state any longer than necessary.

Adapting to Navigation Drawer

If you take the source code for Part 2 of the series, and adapt it as-is to a Navigation Drawer example, you’ll find that things don’t quite work as you’d expect. In particular, you’ll find that even though you have saved the state of the ContainerFragment in onPause, the next time you return to this fragment, its state is cleared.

Why is this? The alert reader might have spotted the reason.

In the case of the ViewPager example, we clear the saved fragment state in onDestroy(). This is because of the way ViewPager works (or rather, FragmentPagerAdapter or FragmentStatePagerAdapter works): When you navigate away from a tab, the Fragment’s `onPause is called but none of the other life-cycle methods are called. This means onDestroy is skipped and the Fragment is simply torn down. onDestroy is only called when the hosting Activity is destroyed.

    @Override
    public void onPause() {
        super.onPause();
        ((NestedFragApp)getActivity().getApplication()).setFragmentSavedState(SAVED_STATE_KEY, getFragmentManager().saveFragmentInstanceState(this));
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        ((NestedFragApp)getActivity().getApplication()).setFragmentSavedState(SAVED_STATE_KEY, null);
    }

However, when you use a Navigation Drawer, the case is different. In this situation, there is no PagerAdapter to deal with. When you navigate from one item in the navigation drawer to another, the “old” Fragment undergoes the complete life-cycle - onPause all the way to onDestroy and onDetach. As a consequence, since you’re clearing the saved Fragment state in onDestroy of the ContainerFragment, you end up clearing the state that you had just saved in onPause.

Solution?

Well, the solution is rather simple - just don’t clear the state in onDestroy of the parent Fragment! In addition, there are a few other minor changes - like the way you set the initial state of the ContainerFragment (instead of retrieving the saved state in one of the life cycle methods of the Fragment, you use setInitialSavedState in the static creator method). The source code for this is available at the github repo for this series.

    public static ContainerFragment newInstance(SavedState savedState) {
        ContainerFragment frag = new ContainerFragment();
        frag.setInitialSavedState(savedState);
        return frag;
    }
    ...
    ...
    @Override
    public void onPause() {
        super.onPause();
        ((NestedFragApp)getActivity().getApplication()).setFragmentSavedState(SAVED_STATE_KEY, getFragmentManager().saveFragmentInstanceState(this));
    }

Here’s a video showing this in action (Unfortunately the Android screenrecord tool doesn’t like it if you rotate the device during the recording, but I think the video demnostrates the point sufficiently):

Forgetting the saved state?

The bullet points that we established in the previous post (re-capped at the beginning of this post) say that you should not hold on to the saved state any longer than necessary. However, we had to violate that rule in this solution because - well - it is pointless to save the state only to immediately clear it!

However, depending on your use case you might approach this in a different manner. For example, you might only clear the fragment saved state when the hosting Activity is destroyed. This is not demonstrated in the sample code on github but should be straightforward to implement.

Sunday, February 2, 2014

Nested Fragment and the BackStack - Part 2

This article is the second in this series about Nested Fragments and the Back Stack in Android. You can read Part 1 here. To get this post into context, take a look at the video embedded in the previous post, if nothing else.

Edit: Later posts in this series at

  1. Part 3

EDIT: Some Google engineers, including the creators of the Android framework have expressed their reservations regarding this article. Read this G+ thread for more details. They point out that using an Application sub-class to save state is not a good idea, but also that saving Fragment instance state explicitly might in itself needs to be considered carefully. I hope to gather their thoughts and write a follow-up post in the coming weeks. Stay Tuned.


At the risk of sounding repetitive, I’ll start off this post by once again stating the gist of the previous post:

A Fragment’s onSaveInstanceState method is not guaranteed to be called when it is “removed”. The Fragment might simply be torn down. The only time its state might be saved is when the hosting Activity saves its state.

We also saw how this could be a problem when you use nested fragments and a FragmentManager doesn’t save its backstack of fragment transactions. In this part, we’ll look at one possible solution to this problem.

Save state in onPause

This is the obvious solution to the problem. The Android docs also state this time and again: it is a best practice to proactively save state. Also, since onPause is the only callback that is guaranteed to be called, it makes sense to save your instance state here.

Having said that, it is easy to save view states, scroll positions and even entire arbitrary objects in onPause. But, how does one save a back stack of fragment transactions?

Enter Fragment.SavedState. You can ask the FragmentManager to save the state of a Fragment using saveFragmentInstanceState. The back stack being managed by a Fragment’s nested FragmentManager is included in the state saved by this method.

The Application sub-class

This post shows how you could use a sub-class of the Application class to save the state, but you might choose another mechanism to do so. The important thing is that the state has to be saved. We use a Map of strings as keys and the saved state as values in this example.

public class NestedFragApp extends Application {

    Map<String, Fragment.SavedState> savedStateMap;

    @Override
    public void onCreate() {
        savedStateMap = new HashMap<String, Fragment.SavedState>();
        super.onCreate();
    }

    public void setFragmentSavedState(String key, Fragment.SavedState state){
        savedStateMap.put(key, state);
    }

    public Fragment.SavedState getFragmentSavedState(String key){
        return savedStateMap.get(key);
    }

}

Explicitly saving Fragment state

Then, you save the state of the container fragment when it pauses as follows:

@Override
public void onPause() {
    super.onPause();
    ((NestedFragApp) getActivity().getApplication()).setFragmentSavedState(
            SAVED_STATE_KEY, getFragmentManager().saveFragmentInstanceState(this));
}

Initializing the fragment transaction

Finally, remember to check whether there is a saved state for this fragment before “initializing” the fragment transaction:

SavedState fragmentSavedState = ((NestedFragApp)getActivity().getApplication())
        .getFragmentSavedState(SAVED_STATE_KEY);
if(fragmentSavedState == null){
    if (savedInstanceState == null) {
        getChildFragmentManager().beginTransaction().replace(R.id.nested_fragment_container, 
                NestedFragmentOne.newInstance()).commit();
    } else {
        // use savedInstanceState here to restore state saved in onSaveInstance
    }
}

Note that there are two “saved states” here:

  1. The instance state saved in onSaveInstanceState, which is provided to you by the system via savedInstanceState.
  2. The state you explicitly saved in onPause, which you retrieve from the Application object as fragmentSavedState.

The flow you follow for initializing the fragment is as follows:

  • You first check to see if you had previously explicitly saved state. If true, then you don’t need to do anything.
  • If not, then you proceed to check if the system had saved state for you. If true, then you use the savedInstanceState to restore system-saved state.
  • Only if neither is true, then you initiate the fragment transaction.

Letting go of the saved state

One thing you need to be careful of is to not hold on to the saved fragment state any longer than necessary. For example, when the container Fragment is destroyed, you want to invalidate the back-stack associated with it as well. This sounds obvious but I overlooked it and ended up with strange behaviors.

The best way I found was to “forget” the saved state of a container fragment in its onDestroy:

@Override
public void onDestroy() {
    super.onDestroy();
    ((NestedFragApp) getActivity().getApplication()).setFragmentSavedState(
            SAVED_STATE_KEY, null);
}

With all these steps in place, the app now behaves as one would expect it to. Your position within a back-stack, even within a nested fragment, is remembered even when you navigate away and return to the top level fragment.

Here’s a video showing how the app now behaves:

The source code for the entire series is at github.

Conclusion

  • Consider pro-actively saving your Fragment states in onPause, particularly is the Fragment happens to nest other fragments inside of it.
  • Do not rely solely on the system saving state for you in onSaveInstanceState.
  • Use FragmentManager#saveFragmentInstanceState to save the Fragment state including the back-stack of nested fragment transactions for you.
  • Do not hold on to the saved state any longer than necessary.

This article looked at ActionBar tabs with a ViewPager, but this concept applies to other situations where one would use nested Fragments (Navigation Drawers for example).

Saturday, January 25, 2014

Nested Fragments and the Back Stack

This article is not about the back stack of activities that Android maintains for every task. That stuff has been written about adequately elsewhere. This post is about the back stack of fragment transactions maintained by the FragmentManager and how they relate to nested fragments.

Edit: Other posts in this series at

  1. Part 2
  2. Part 3

Heads-up: If you are using nested fragments, you need to use the support library, even if your app only targets API level 14 and above. This is because nested fragment support was added in API 17, and the feature was back-ported to the support library (revision 11 and later).

TL;DR

The gist of this post can be stated as follows:

There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state.

This is from the docs (emphasis mine). Overlooking this can lead to bugs especially when you use nested fragments since the back stack of a child fragment manager could be reset when you least expect it. Remember - if the state of a Fragment is not saved, then by definition, the back stack of fragment transactions managed by that fragment’s child FragmentManager is not saved either.

The Problem

With the advent of fragments, more so nested fragments, the general advice one gets from the developer community is this:

Fragmentize all the everythings!

And with good reason too. Consider the following:

  • If you use ActionBar tabs, the content of each tab is implemented as a Fragment.
  • Each “page” in a ViewPager is often implemented as a Fragment.
  • In navigation drawers, the “content” of each navigation item is expected to be a Fragment.

What this translates to is that what would once be implemented as an Activity now needs to be implemented as a Fragment. This also means that a flow within that Activity, that might have been implemented using Fragments, now needs to be implemented using nested Fragments. Note that by “flow” I simply mean a sequence of screens to establish a particular task.

Now here’s the thing with flows: If a user “goes away” from a flow and later returns to it, it is expected that the user continues from the screen where they left off. Translated into Fragment terminology, this means that if a user navigates away and returns to a flow that is implemented using Fragments, its is expected that the user’s position in the backstack of fragment transactions is retained. However, this isn’t always the case.

Here is a video demonstrating the problem:

The video shows an Activity with three tabs. It is a modified version of an Activity created using the “New Activity” wizard in ADT or Android Studio and specifying “Fixed Tabs + Swipe” navigation. The modification is as follows:

  • The content of the first tab has been modified to make it a “Container” Fragment that in turn contains two nested fragments.
  • When the container fragment is first created, it shows a nested fragment asking you to enter your name.
  • On entering the name and Clicking “Next”, you are presented with another nested fragment asking you to enter your GitHub username.
  • The other two tabs are just simple Fragments - no nesting business there.

Now, notice what happens when I follow this sequence:

  1. Enter name, press Next. Then, enter a github username.
  2. Navigate to the tab titled “Section 2” and then back to “Section 1”.
  3. Navigate to the tab titled “Section 3” and then back to “Section 1”.

Uh! In step 3 above, the back stack was nuked. But hey, it didn’t happen in Step 2. Why so?

Explanation

This example uses a ViewPager. By default, a ViewPager has an “off screen limit” of 1. This means that in addition to the page being displayed, one adjacent page in each direction is kept in memory. So, when you navigate to “Section 2”, everything in “Section 1” is still intact in memory.

When you navigate to “Section 3”, the page corresponding to “Section 1” is torn down. More importantly, since at this point the Activity instance state is not being saved, the Fragment state isn’t saved either. This ties in with what we saw in the “TL;DR” section above. As a result, when you navigate back to “Section 1”, the nested fragment back stack is reset.

Rotation? Task Switching?

Try following this sequence of steps:

  1. Enter name, press Next. Then, enter a github username.
  2. Rotate the device; or switch to another app and return back to this app

Now you’ll see that the back stack is retained. This is because when you rotate the device or switch to another task, the Activity saves its instance state. As a consequence the container fragment does too.

Conclusion

Re-iterating what we started off this post with, keep in mind when you are using nested fragments that a Fragment is guaranteed to save state only when the containing Activity saves its instance state. At other times, the Fragment might simply be torn down.

The code for a sample app illustrating the problem is available at github. The next part of this series will explore ways to overcome this problem.