Asynchronously loading data using Googles Paging Library
The recently released Paging Library from Google gives you an easy way to page data into memory off of the main thread. If you want to use it with Room, then the built-in support makes it trivial. However, if you’d like to page data that exists elsewhere - from the network or disk, for example - then you have to do a little extra work.
Here I demonstrate how to take existing code that loads a list of screenshots from disk and convert it to load asynchronously using the Paging Library. This example could easily be adapted for network calls.
Throughout this post we’ll be using a
Screenshot class defined as follows:
You will also need to import the library:
When the app launches, it shows the user a list of images from a directory they supply during setup. My existing code was a simple repository that loaded all screenshots (as a Uri and width/height values) from this directory via Android’s storage access framework. The code was similar to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
As you can see, we have to go to disk to get information about every screenshot. This is expensive! On my test device I’ve only got around 100 screenshots - on a device that’s been around longer a user could have many hundreds or thousands of screenshots in this folder. Rather than loading these all up front, I need to load these asynchronously. In addition, since these are in a list there’s a high likelihood that the items further down the list won’t even be needed. The Paging Library helps with these problems.
Paging Library Fundamentals
The Paging Library has 3 different “levels” that build on top of each other.
At the base, we have a kind of
DataSource. This can be either “keyed” (such that you need to know about the item at index N-1 in order to know about the item at index N - like a linked list), or “tiled” (such that you can access elements at arbitray indices - like an array list).
Above that, we have a
PagedList, which as its name suggests, is a list that pages its data in from a
Finally we have the
PagedListAdapter, which is a
RecyclerView.Adapter that neatly wraps a
PagedList, calling the correct
notifyItem... methods for you as your data changes (when you call
setList with a new
PagedList) or loads in. This is fairly standard
RecyclerView boilerplate. If you need some custom behaviour, you can duplicate its functionality - it’s just a handy wrapper around the
Constructing a Data Source
DataSource is reasonably simple. For a list like this, we implement a
TiledDataSource, doing the expensive disk IO in the
loadRange method. The
PagedList will call this from a background thread when it is time to load a new page of data.
Note that it does not clamp ranges for you - so if you have 10 items and a page size of 6, your second page will be
startPosition = 6 and
count = 6 - which will give you an
IndexOutOfBoundsException. Make sure to clamp your inputs as I do here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Building a Paged List
For my use-case, 8 elements on a ‘page’ was sufficient. I’m showing elements in a 2-column list; most of the elements are around half the window height. You’ll need to decide what works well for you.
Note that here we use the default prefetch distance of
pageSize - that is, as soon as the first item in a given page of data is requested, the next page will begin loading. Depending on your data, you may want this to be smaller or larger.
We also enable placeholders (this is actually enabled by default). Since we know exactly how many elements are going to be in our list (we have Uris for every screenshot, even if we don’t have any details about that screenshot yet) we can use null placeholders while the images load - helping avoid weird scrollbars. Our
onBindViewHolder has to deal with this later. I’d recommend reading the
PagedList docs - they go into more detail on placeholders.
You need to supply two
Executors - one for posting back to the main thread, and another for background work. In this example we create a main thread
Handler and post events to it directly, but our disk IO
Executor is injected from elsewhere.
Finally, this code has one subtle gotcha - the first two pages will be loaded immediately on whatever thread
build() is called from! From the docs:
Creating a PagedList loads data from the DataSource immediately, and should for this reason be done on a background thread. The constructed PagedList may then be passed to and used on the UI thread. This is done to prevent passing a list with no loaded content to the UI thread, which should generally not be presented to the user.
In this case, we’ll be going to disk 16 times (8 for the first page, and then another 8 as the second page is pre-fetched). I address this later when I wire everything together.
1 2 3 4 5 6 7 8 9 10 11
Adding the Adapter
The simplest part of all. We just extend
PagedListAdapter, supplying a simple
DiffCallback for comparing
Screenshot objects, and implement
onCreateViewHolder like normal.
Note if you have custom logic (such as a custom
BindingAdapter - not shown here) you need to be aware that the object returned from
getItem can be
null - these are the placeholders we enabled earlier, and will be
null while the data at that index loads. If your page sizes are appropriate, receiving a
null object will be rare, but you must handle it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Wiring it all up
Now we have all the pieces we can put them together. Note that I supply an async callback for loading the screenshots - we use the disk executor we supply in the constructor to do the actual building (since this loads the first page or more, as mentioned above!) and then set the result on our list once that load has finished.
For brevity, this code doesn’t consider configuration changes or other kinds of activity destruction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
- You may have to specify a
minHeighton your list items, or otherwise specify the height when a placeholder is used. If you don’t, the adapter will query for (and the list will try to load) your whole list of 0-height items.
- Don’t forget the first page is loaded when you call
PagedList.Builder.build()- do this off the main thread!
- Experiment with page sizes and prefetch windows. This is entirely dependent on your data.
- Already loaded objects are not unloaded. If the user scrolls to the bottom of the list, the first items stay in memory. This is potentially a problem if your objects are large, or your list is long. Here I let Picasso load (and unload) the actual memory-intensive bitmaps for me. Storing a list of Uris is cheap.
- You must handle null objects returned from
getItemif you enable placeholders, even if you never encounter them in your testing.
loadRangecall is not bounded to the size of the list; you need to do this yourself. It will happily handle results smaller than the requested count, however (i.e. when you’re at the end of the list).
- If you’re using
LiveData, look into
LivePagedListProvideras it will do most of this overhead for you.
- The library is still in alpha at the time of this writing; the APIs described here could still change before release.