PagedList in ViewPager
Or how to share common PagedList between different types lists
Recently I faced with a next test task:
You need to write a test app, that has 2 pages. On the first page there is a search bar, where you can write something. When you finished an press appropriate button, you need to load images from Flickr service and show them. When you click on image, a second screen with clicked photo is shown.
So during execution of this test task, I thought “It would be nice to have an ability of swiping photos in a second page, because it will help user to not go back every time, when he wants to see full size photo”.
I thought, it is a great idea, but then I started to think, how to implement it. You need to not only pass list of photos instead of one photo, but you need also give an opportunity for second page to download a new portion of images every time user nearly to the end of the list.
For the first page I’ve used a recently presented PagedList
. Not only because it handy, but also because of my curiosity and a wish to try something new. And I thought “Could we just reuse this mechanism for ViewPager
?”.
I didn’t find any solutions in the internet and there was no such adapter in the android paging library, so I’ve decided to write the simplified version of this one.
First version
Suppose, that we have an instance of PageList
and we just need to make an adapter for it, with ability of downloading nearest items. So our first version of adapter will approximately look like:
class FullSizePhotoAdapter(
val photos: PagedList<Photo>,
) : PagerAdapter() {
override fun isViewFromObject(view: View, obj: Any) = view == obj
override fun getCount() = photos.size
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val root = LayoutInflater.from(container.context).inflate(R.layout.item_full_screen_photo, container, false)
photos.loadAround(position)
... //prepare your view to be shown
return root
}
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) =
container.removeView(obj as View)
}
Here you can see a full example of such adapter. So, as you can see, we just try to load a photos ‘around’ if we can on every instantiation of item. Internally loadAround
method, roughly speaking, just takes passed index
and prefetchDistance
— count of items that need to be fetched (you can specify it in PagedList.Config
instance like here, if it is not set, then it will be equal to pageSize
setting). Then loadAround
method just checks is last loaded item more than index + prefetchDistance
, and if it is, then it loads new page of items.
That’s why we don’t need to worry about too often loadAround
calls. If index + prefetchDistance
will be higher than current last loaded index, then no downloading will happen.
What’s wrong with this solution
As for me, this solution is pretty obvious, concise (just one line) and effective, so it covers most of cases, but it has one big (for my task) flaw:
I have PagedList
with placeholders, so if item is not loaded now, I need just to show loading placeholder instead of actual item, but when its loading will be finished, I need update currently visible item with loading state to present user a loaded image. But in a gif above you can see, that item is not shown, until you swipe (to see that effect clearly you need just set page size in PagedList.Config
to 1 or 2 and then start rapidly swiping items).
So how we can resolve that?
Sharing PagedList
But first I want to digress and tell about sharing PagedList
. In my test app I had two activities. In the first one I showed a list of images on RecyclerView
, which is stored and partially loaded by PagedList
and presented by PagedListAdapter inheritor. In the second one I showed a list of full screen images on ViewPager
, which is stored also by PagedList and presented by PagerAdapter inheritor.
And the logic question, could we share same PagedList
? Yes, but we need to remember few things.
PagedList
storeWeakReference
to its callbacks, which can be, for example,PagedListAdapter
. So we need to be sure that our instance ofPagedList
has appropriate callbacks every time to avoid memory leaks.PagedList
does not have to save and restore its state, since it has storage, and thus the ability to re-download items.
Final Version
Now we can re-think both paragraphs and formulate two important wishes:
- We want to update ViewPager when currently visible item is loaded
- We want to support subscribing and unsubscribing callbacks
Those statements lead us to next snippet of code:
Here we can see, that instantiateItem
and destroyItem
methods are used to determine, which pages are now visible, and which are gone. Positions passed through instantiateItem
method are stored in TreeSet
which formulates window of currently visible items.
When we set a new list to adapter, first, we remove inner callback from the old one and then set this callback to the new one. In this callback we receive the information about inserted, removed or updated ranges of data. Here we analyze and compare those ranges with our window of currently visible items and understand do we need to call notifyDataSetChanged
or not.
Note that as we have shared PagedList between two activities (by service locator, or kodein or other DI frameworks), we need to remove adapter’s inner callback, when we finished work with activity, so we need to call adapter?.submitList(null)
afterwards.
Conclusion
PagedList
is a great architecture component added recently. And I think its very powerful. Moreover it flexible enough to be built in ViewPager
's system.
If you want to see the whole project and play with it, you can visit this repository. Hope you enjoyed. :)