State Preservation in Backstack Fragments

Jun 28, 2015   //   by Theo   //   Blog  //  No Comments

Background

When adding and restoring fragments to the backstack, additional care must be taken that the fragment lifecycle is properly handled, so instance state preservation works correctly. This post looks at a couple of gotchas involving state preservation and backstack fragments.

Gotcha #1: Instance state not restored when returning from backstack

Consider the following code for a fragment which displays a list of countries received from the server. It overrides onSaveInstanceState to save its state (the list of countries) and onCreateView to restore its state.

private List<String> mCountries;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    View view = inflater.inflate(R.layout.fragment_countries, container, false);

    if (savedInstanceState == null)
    {
        // Populate countries by calling AsyncTask
    }
    else
    {
       // Populate countries by extracting them from saved instance state bundle
    }

    return view;
}

public void onSaveInstanceState(Bundle outState)
{
    // Save countries into bundle
}

While testing this code, we note that something is not quite right with instance state restore in the following scenario:

  1. Another fragment moves to the foreground and the countries fragment is added to the backstack
  2. We press the back button to move the countries fragment from the backstack to the foreground
  3. The countries fragment reloads the data from the server instead of using the saved instance state data

We would like to avoid reloading data from the server when the fragment is restored from the backstack because that creates additional traffic on the webserver, and causes a 1-2 second delay in the UI while the data is loaded.

So why does this happen? To find out, we will need to take a close look at the fragment lifecycle.

With the regular fragment lifecycle used during fragment recreation, both the view and the fragment are destroyed, meaning that onSaveInstanceState, onDestroyView and then onDestroy are called (among others). During re-creation, both the fragment and the view are re-created, so onCreate and onCreateView are called. Both provide the instance state saved before destruction.

fragment_recreation

On the other hand, with the backstack fragment lifecycle, when the fragment is added to the backstack only the view is destroyed, while the rest of the fragment is preserved on the backstack with its member variables intact. This means that only onDestroyView is called, while onSaveInstanceState and onDestroy are not. When the fragment is brought back to the foreground, only the view has to be re-created, so onCreateView is called. Since onSaveInstanceState was not called, the saved instance state in onCreateView is null.

fragment_backstack

This is the root cause why our code is not working correctly. Our code above assumes that a null instance state means that the data is not loaded, so it reloads the data from server. Clearly this is not appropriate for the backstack case.

So how do we fix this? One easy way is to stop using the instance state as a proxy for whether the data is loaded, and just check the data itself. This means that instead of checking whether savedInstanceState is null, we will check whether mCountries is null. The revised code is shown below:

private List<String> mCountries;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    View view = inflater.inflate(R.layout.fragment_countries, container, false);

    if (mCountries == null) // Updated check
    {
        // Populate countries by calling AsyncTask
    }
    else
    {
        // Populate countries by extracting them from saved instance state bundle
    }

    return view;
}

public void onSaveInstanceState(Bundle outState)
{
 // Save countries into bundle
}

The key takeaway here is that you should directly check the state of the fragment rather than the saved instance state, since a null instance state in onCreateView does not necessarily mean that the state is uninitialized.

Gotcha #2: Instance state not restored when rotating the device twice

So we’re pretty satisfied with the fix, but while testing the code above we come across a strange scenario involving rotation.

  1. Another fragment moves to the foreground and the countries fragment is added to the backstack
  2. We rotate the device twice, while the other fragment is in the foreground
  3. We press the back button to restore the countries fragment from the backstack
  4. The countries fragment reloads the data from the server, instead of using the saved instance state data

What’s going on here? All other scenarios seem to work properly.

Once again, understanding the root cause for this behavior requires a deep dive into the fragment lifecycle. This time we will look at how the regular fragment lifecycle and the backstack fragment lifecycle interact.

  1. When the fragment is added to the backstack, we pass through the events shown above in the first part of Figure 2. The view is destroyed and the fragment is added to the backstack with its member variables intact. onDestroyView is called, but onSaveInstanceState and onDestroy are not.
  2. When the device is rotated the first time, the entire activity that contains the fragments has to be re-created. That means that all the fragments found on the backstack have to be re-created too. During tear-down, our countries fragment receives the onSaveInstanceState and onDestroy events. Then during re-creation, the fragment is re-created, but because the fragment is on the backstack, the view is not. That means that the onCreate event is called, but onCreateView is not. Without onCreateView, the mCountries member variable is not populated.
  3. When the device is rotated a second time, onSaveInstanceState and onDestroy are called. Since mCountries has not been populated from the previous instance and is null, the saved instance state will now contain null for the countries data.
  4. When the countries fragment is restored from the backstack, onCreate and onCreateView are called. For both events the countries data is null, so the countries data will not be restored.

So how do we fix this? Obviously, restoring the state in onCreateView is the main reason why the instance state is lost, since that event will not be called when the fragment is on the backstack. One easy fix is to restore our instance state in the fragment’s onCreate instead of onCreateView. The revised code is shown below:

private List<String> mCountries;</pre>
@Override
public void onCreate(Bundle savedInstanceState)
{
    if (savedInstanceState != null)
    {
        // Populate countries from bundle
    }
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    View view = inflater.inflate(R.layout.fragment_countries, container, false);

    if (mCountries == null)
    {
        // Populate countries by calling AsyncTask
    }

    return view;
}

public void onSaveInstanceState(Bundle outState)
{
    // Save countries into bundle
}

The key takeaway here is that the instance state should be restored in onCreate rather than onCreateView, since onCreate is always called on fragment creation, while onCreateView is not necessarily.

Conclusion

Hopefully this article was helpful in dealing with instance state and backstack fragments in your apps. Let us know about any questions or suggestions in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>