Fragment state restoration and the FragmentStatePagerAdapter

In the process of building an application for Android I encountered an interesting requirement with a non-trivial solution.

I wanted to implement horizontally scrollable 'pages' in such a way that when the user then added a new item, a new page would be added at the start.

The reason that this is not trivial is based on the way that the ViewPager class caches and restores its adapter.

Each page in my example will be a Fragment. As such it seemed appropriate to use the FragmentStatePagerAdapter class provided by Google.

The documentation states "When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment." - the way this saved state is restored is the cause of the issue. How do you restore the first fragment if the first fragment has changed?

In the ViewPager source (from which FragmentStatePagerAdapter extends) the onRestoreInstanceState method contains the following block:

if (mAdapter != null) {
            mAdapter.restoreState(ss.adapterState, ss.loader);
             setCurrentItemInternal(ss.position, false, true);
         }

The documentation for the Activity class states that onRestoreInstanceState is called after onStart. It is this consideration which causes my issue.

What I want to do

In my application I display various fragments within a container, and maintain a backstack to allow back navigation between them.

When the fragment containing the FragmentStatePagerAdapter is initially created we set our adapter within onCreateView or onActivityCreated as appropriate.

We then click our 'Add' button in our toolbar and the fragment is replaced within the container with our 'Add new page' fragment. When this happens we add our FragmentTransaction to the backstack. This causes our 'pager' fragment NOT to be destroyed as outlined here.

We complete our 'Add' process and now we want to popBackstack to the initial pager fragment AND add a new page at the start.

We could do this by accessing the pager fragment from our FragmentManager and updating the adapter prior to popping back to it? When we then pop to it and notifyDataSetChanged, our new page should appear.. right? Nope.

When we pop to the pager fragment the view heirachy is automatically restored by android. The code snippet above shows that for a ViewPager the restore process checks if the pager has an adapter (which it does - we set it in onCreateView) and if it does uses the saved adapter state. The saved adapter state is the adapter state without our new page. This results in various weird things such as the correct number of pages but the incorrect fragment contents.

This happens because FragmentStatePagerAdapter caches its own contained fragments, and when we call restoreState (in the above snippet), these fragments are reused.

Interestingly, if you swipe to the right a few times, and swipe back then you will see the correct new Fragment. Why is this? Well.. as emphasized by the quote above, the FragmentStatePagerAdapter destroys its fragments as appropriate when they are offscreen. When offscreen the cached page one is destroyed. When we scroll back to it it is recreated. This time however it uses the correct new fragment at position one.

Resolution

There are many potential resolutions for this particular use case.

  • You could create a custom ViewPager and reimplement onRestoreInstanceState so as not to restore the pager in the way outlined above.

  • You could unset the adapter before moving to the 'add' fragment - this way, when you return the various restoration conditions triggered by the existance of an adapter will still be called but their wont be any data to reuse.

  • You could create a custom FragmentStatePagerAdapter and add a cache clearing mechanism for such a use case.

I went for the final option, so I'll explain a little more how I approached it.

The FragmentStatePagerAdapter source is open source. It is not superbly complex and as such in my opinion is perfectly suitable for reimplementing as long as you know what you are doing.

I implemented the following snippet within my custom adapter:

Boolean clearCache = false;

    public void clearCache() {
        mFragments.clear();
        clearCache = true;
    }

What this does is it clears the cached list of fragments in the adapter. It then sets a Boolean indicating this.

The reason for this Boolean is that in restoreState, the adapter will try and restore mFragments by getting the fragments from the saved bundle. In this case we do not want to restore them and as such we change the line if (state != null) { to if (state != null && !clearCache) { in onRestoreState and reset the clearCache variable to false at the end.

Now when the adapter goes to access a fragment at a particular position, due to its non-existance in the cache it will create it. Our new page will be displayed correctly.

This approach has the benefit that you can conditionally destroy the cache. In normal usage when a user scrolls between fragments the caching mechanisms will work as normal. When however you add a new page you can invalidate the cache.

Other resolutions to similar problems are not appropriate for working with large adapters. This approach should however be quite performant. If you have one hundred fragments, it is safe to say that Android will not keep them all in memory. It may well retain state for individual fragments yet from my brief experimentation this does not seem to be the case.

One small hiccup

There does seem to be one small hiccup with this approach - namely that the FragmentStatePagerAdapter does not maintain the correct references to its adapter instance. That is to say if you set the adapter of the pager and then simply clear its cache prior to popping back to it, you will not see the result that you expect. This happens because the adapter has its own copy of the old adapter.

So as to not have to create a new adapter instance you simply need to set the adapter on the pager again. I use the following in onCreateView:

if (adapter == null) {
            adapter = new PhotoAdapter(getChildFragmentManager());
        }
        adapter.clearCache();
        pager.setAdapter(adapter);

The long and the short

The long and the short of it seems to be that in providing a clever caching mechanism for optimal memory management, the Android team have made implementing a relative simple requirement somewhat complex. Memory management is however superbly important, and I am just happy that I dont have to implement the FragmentStatePagerAdapter concept in its entirity :)

Hopefully this will help someone trying to achieve a similar thing.