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 reimplementonRestoreInstanceState
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.