Adventures with FragmentStatePagerAdapter

A lot of Android developers are confused or don’t even know about the difference between FragmentPagerAdapter and FragmentStatePagerAdapter. Also getting notifyDatasetChanged() to work can be frustrating sometimes. Memory leaks can happen quite easily by using these adapters. I will start with these basics and then dive deeper into implementation details to explain and also point out some less known gotchas (did you know that Fragments in FragmentPagerAdapter are only released from memory if you finish the Activity? Read on :-)).

The Basic difference

FragmentPagerAdapter

Good for a limited (fixed) number of items (Fragments). Why? Because it never removes a fragment instance from FragmentManager once it’s created (unless that Activity is finished). It only detaches the Views from Fragments which are currently not visible. onDestroyView() will be called on your Fragment once it’s out of reach and later onCreateView() will be called once you go back to this Fragment.

FragmentStatePagerAdapter

A FragmentStatePagerAdapter is more memory savvy. It completely removes Fragment instances from the FragmentManager once they are out of reach. The state of the removed Fragments is stored inside the FragmentStatePagerAdapter. The Fragment instance is recreated once you return back to an existing item and the state is restored. This adapter is suitable for lists with an unknown count or for lists where the items change a lot.

FragmentPagerAdapter — memory leak danger

Fragments in the FragmentPagerAdapter are only detached and never removed from the FragmentManager (unless the Activity is finished). When using FragmentPagerAdapter you must make sure to clear any references to the current View or Context in onDestroyView(). Otherwise the Garbage Collector can’t release the whole View or even the Activity . This means setting any View/Context related fields to null (Butterknife can unbind them automatically) and also removing any listeners that could leak the Context or View.

Failing to do so can possible exhaust the memory — imagine you have 10 items in your FragmentPagerAdapter. Swiping through all of them would keep 10 Views in the memory instead of just the last three (depending on setOffScreenPageLimit() setting), rotating the screen would make it even worse (7 out of 10 would still keep a reference to the “destroyed” Activity).

Zombie detached Fragments

You also need to think about the implications that the Fragments are never removed from the FragmentManager. You can have hundreds of detached instances in the memory just because you are constantly changing a small list of items which the adapter is bound to. It will create the new items and keep the old detached Fragments.

This is less of a problem with FragmentStatePagerAdapter because it’s removing whole Fragment instances from the FragmentManager. You could run into trouble if you swipe through a lot of Fragments and since each one will add a new Bundle instance into the map it could grow quite big (possibly leading to a TransactionTooLargeException during an orientation change).

Troubles with notifyDatasetChanged()

I guess every one of us ran into issues when calling notifyDatasetChanged() on one of these adapters. Doing so will not refresh the current visible fragments and you have to swipe “back and forth” to force the adapter to recreate the current fragment. Both of these PagerAdapters are caching and reusing Fragment instances and that’s good because otherwise calling notifyDataSetChanged would recreate them again (even if not necessary).

It’s also very important to note that notifyDataSetChanged() is for situations when the dataset changes — that means if some items are removed or added. The notifyDatasetChanged() method is not meant to be used for refreshing the current displayed Fragments or their views. You need to add some listeners / callbacks to your Fragments if you want them to refresh their views if some of your data changes.

FragmentPagerAdapter & notifyDatasetChanged()

You need to override two methods in your FragmentPagerAdapter in order to support dataset changes.

int getItemPosition(Object object)

Called when the host view is attempting to determine if an item’s position has changed. Returns POSITION_UNCHANGED if the position of the given item has not changed or POSITION_NONE if the item is no longer present in the adapter.
The default implementation assumes that items will never change position and always returns POSITION_UNCHANGED.

You need to implement getItemPosition(Object object)with some sort of logic that determines the current Fragment’s position in your dataset (e.g. iterating of the dataset and search the entry position that corresponds to the Fragment).

Always returning POSITION_NONE is memory and performance inefficient — it will always detach the current visible fragments and recreate them even if their position in the dataset hasn’t changed. And the old Fragments will be kept in the memory until you leave the Activity.

long getItemId(int position)

Will return a unique identifier for the item at the given position. The default implementation returns the given position. Subclasses should override this method if the positions of items can change.

This method is used inside of instantiateItem() to search for an existing detached Fragment instance in the FragmentManager. Calling notifyDataSetChanged() without overriding this method will just return the existing Fragment instance on the current index. You need to return a unique identifier for that Fragment

FragmentStatePagerAdapter — state bundle “bug”

You only need to override the getItemPosition() to support dataset changes in your FragmentStatePagerAdapter (there is no getItemId() method here). The same things apply here as already mentioned for the FragmentPagerAdapter (don’t always return POSITION_NONE).

There is one gotcha here — and it’s rather unpleasant… The issue is described in detail in this blog. The problem is that FragmentStatePagerAdapter is keeping an ArrayList of state Bundles. The instantiateItem() method will create a new Fragment instance if it doesn’t exist — but then it will look into the ArrayList and pick the Bundle based on the item index! This can be a Bundle that belonged to a previous (different) Fragment instance on that index. You can run into this if you are changing the dataset and updating the adapter. Read the linked blog for some possible solutions.

Fragment {} is not currently in the FragmentManager

This issue has been open and not fixed for over 4 years! It’s described in detail in this blog. It’s really frustrating and after this point you may think about having your own “fixed” version of FragmentStatePagerAdapter. (EDIT: We are working on it — it will be available at https://github.com/inloop/UpdatableFragmentStatePagerAdapter).

Adam Powell replied on G+:

FragmentStatePagerAdapter predates the data set change feature for PagerAdapters entirely. If you have a PagerAdapter that needs to deal with data set changes you’re probably going to have an easier time implementing the contract from PagerAdapter directly than starting from FragmentStatePagerAdapter.

Well… that’s nice, but there should be some big warning about this. A simple workaround is to return POSITION_NONE in getItemPosition() at all times. But this comes with a performance hit.

There is also the PagerAdapter…

And maybe I should have stated this in the beginning — but remember you don’t always need to use Fragments in case you have a ViewPager in your application. It’s totally fine and less complex if you use some composite View layouts and a simple PagerAdapter. It depends whether you need to have all the life-cycle callbacks.