App Development

Android Development: Writing a ViewPager Adapter for Views

blog-featured-image how-to-write-ViewPager-Adapters FD-510x296

So, I recently needed to make a ViewPager that worked with plain old Android Views, rather than using fragments as is usually the case. It turns out, this is not a trivial thing to do.

Out of the box, the Android Framework provides a PagerAdapter, a FragmentPagerAdapter, and a FragmentStatePagerAdapter. Now, if you’re happy using fragments, life is easy, you just pick whichever FragmentAdapter works best for you and go on your way. If not, you’re stuck building on top of the raw PagerAdapter, which means implementing some things you could otherwise take for granted.

As you probably guessed, being the base class for all ViewPager adapters, PagerAdapter is pretty simple. It should look something like this to start:

public class MyViewPager extends PagerAdapter {


   @Override
   public Object instantiateItem(ViewGroup container, int position) {
       return super.instantiateItem(container, position);
   }


   @Override
   public int getCount() {
       return 0;
   }


   @Override
   public boolean isViewFromObject(View view, Object o) {
       return false;
   }
}

So, the first thing to note is that instantiateItem() takes a ViewGroup and returns an Object. Note that this differs from ListView adapters where the getView() method returns a View. In our case, the Object can be a View, but it doesn’t need to be; PagerAdapter doesn’t actually do anything with it. Instead, you have to add whatever view you make to that ViewGroup you get as a parameter. Note that you must also remove whatever was there before, since it might contain a view representing a different “page.” You’ll also provide your view with any data in this method, and you’re responsible for hooking all that up yourself.

Now, my ViewPager had the same view for all pages, so I used a simple object pool to recycle views. Your needs will probably differ, so I won’t include that here. However, even though our implementations will differ in this respect, the rest should apply to pretty much any setup.

By default, the Android Framework saves the state of the entire view hierarchy in a single SparseArray, mapping from view ID to the parcelable representing that views state. There’s a great article on that here: http://trickyandroid.com/saving-android-view-state-correctly/. ViewPagers throw a wrench in this. Since the PagerAdapter controls all the “child” views, and those views may not all be in the view hierarchy at any given time, the ViewPager delegates the entire responsibility of saving the state for pages to the adapter. Unfortunately, ViewPager doesn’t know anything about your views, and so doesn’t know how to save them; it’s up to you to take care of it.

Fortunately, all views have a nice method on them, saveHierarchyState( SparseArray ) that lets us save the state for not only that view, but all of its children as well. You provide it a SparseArray, and it shoves data into it. (As always, remember that the key is the ID, so all of your view ID’s must be unique, and that if you want a view to save its state, it must have an ID.) You can then call restoreHierarchyState (SparseArray ) to reverse this process, restoring the state to that view and all of its children.

So, we know we’ve got a bunch of these SparseArrays to keep track of. My first inclination was to create a SparseArray<SparseArray>, mapping the page position to that page’s save data. This failed rather spectacularly because although Parcel has a writeSparseArray (SparseArray<Object>) where Object is one of any number of things, including another SparseArray, it doesn’t seem to work. Instead, you should probably use ParcelableSparseArray, which is provided by the support library, or make your own wrapper object (like I did, since I didn’t know about ParcelableSparseArray at the time).

Now that we’ve got something to save our view state into, the only thing left is to actually save that state. We know that, by default, only 3 pages worth of views are attached to the view hierarchy at any given time. When a page is moved from outside of the “active” page range, the destroyItem PagerAdapter method is called for that page. Here, we can save the view state for any outgoing views into our SparseArray<ParcelableSparseArray>, as well as clean up the view container for the next view to use.

One important caveat here is that you only save the view hierarchy state starting at the view you created, rather than at the container. It appears that the ViewPager will sometimes pass itself into destroyItem as the container. If you then save that state, the ViewPager will, of course, attempt to save its state, which includes saving the state of the adapter. This loop will repeat until an eventual StackOverflow exception.

That takes care of any views that are pushed off-screen, but we still have to consider the pages that remain “in range” when saveState() is called, since they will never receive their respective destroyItem() calls. I just keep a Set of currently visible views, adding and removing as necessary, which I iterate over in saveState(), calling saveHierarchyState() on each. So, hopefully you’ve come out of this knowing a few more things than you did going in. As always, if you’ve got any questions, let me know in the comments and we’ll get everything sorted.

Quickstart-Guide-to-Kotlin-Multiplatform

A Quick Start Guide to Kotlin Multiplatform

Kotlin Multiplatform, though still experimental, is a great up-and-coming solution...

Read the article