Getting Started With The Paging Library
Update: The paging library has changed a lot since this post. If you want to see an updated example based on the 1.0 release, then click here
Android development just keeps getting easier. Google has now released another Architecture Components library and this time it is aimed at making loading pages of data into a RecyclerView as easy as possible.
Let's take a brief look at some of the more important classes in the Paging Library and then we can take them for a test drive in a simple Android app.
TiledDataSource - Represents a DataSource that has a fixed size but allows for arbitrary loading from any position within the data set.
KeyedDataSource - Represents a DataSource where getting data for item N requires data loaded from items N-1
PagedList - Holds pages of data loaded from a DataSource. It is capable of representing data that has not yet loaded with placeholders.
PagedListAdapter - A convenience class that gives you an easy to use RecyclerViewAdapter capable of supporting and using PagedLists.
PagedListAdapterHelper - A convenience class for working with PagedLists when you have constraints that prevent you from using the PagedListAdapter. It works with regular RecyclerViewAdapters and DiffUtil to help you handle the PagedList containing your data.
- This class can create a LiveData object for us backed by a PagedList. The data that they load into the PagedList comes from one of the two DataSource objects provided by the library.
We'll create a simple project that just displays a list of football teams in a
RecyclerView. Nothing fancy since we just want to focus on the tools the Paging Library gives us. You can find the project here.
Our project will use Room (another architecture component library) to store the list of teams in a sqlite database. This was a conscious choice because we get some synergy when we combine the Paging Library with Room. Room Dao objects can actually generate a LivePagedListProvider
for us.
@Query("SELECT * FROM teams order by name ASC")
fun getTeamsAsLivePagedListProvider(): LivePagedListProvider<Int, Team>
Once we have our Dao returning a LivePagedListProvider to us we can use it in our ViewModel object to create and expose a LiveData object to our activity/fragment class.
You can see in the snippet that that we use a PagedList.Config
object to provide some rules about how we would like our PagedList
to load the data. We tell it that a page will contain 30 teams, and we would like to prefetch at least 30 teams in advance, that way we should always have another page of data ready to go.
The documentation states that your page size should be large enough to contain several times the number of items currently visible on the screen.
We also set our prefetch distance to 30 which is actually what it would default to if we hadn't set it. We want these values to be large enough that even if a user flings our data list they will hopefully not be left waiting for data to load.
We could also set an initial load size hint. This value defaults to three times your page size and is used to make sure that your PagedList
initially has plenty of data ready to go.
One import aspect of the creating a PagedList
configuration is the setEnablePlaceholders()
method. It is set to true by default and means that the PagedList
always has the full size of the dataset. Our PagedList
will by default return null for data that isn't loaded yet. This means that in our adapter we do need to remember to check for null in our bind method. You can control this behavior when you setup your configuration object by calling setEnablePlaceholders()
.
Now let's have a look at just using a TiledDataSource
returned from our Dao. This exercise will show us what the LivePagedListProvider
is doing for us behind the scenes. First we'll need a new Dao method
@Query("SELECT * FROM teams WHERE teamId >= :teamId order by name ASC")
fun getTeamsAsTiledDataSource(teamId : Int) : TiledDataSource<Team>
Our Dao is now returning a TiledDataSource
of teams and our query is using a team ID to determine where to begin loading. Our view model object will expose this data source as a property much like our previously exposed property of LiveData<PagedList<Team>>
.
In our activity though we will have a lot more work to do. We'll need to setup our PagedList
configuration still but then we will also need to manually create our PagedList
.
The first thing you'll notice in our example code is that we need to kick off to a background thread in order to set our PagedList
up. If we didn't do this then we would get a crash at runtime warning us that we can't query the database on the UI thread(One of my favorite features of Room is that it helps you avoid making mistakes like this).
Once we are safely on our IO thread we just need to create a config object similar to the one we made previously, and then we are use it when we configure our PagedList's builder. The builder will need two generic objects. One of them represents the key and the other represents the type of data that we want to fill our PagedList. The key in our case will be the teamId because that is how we will query our database. If you remember from above we are going to ask for teams that have an id >= to the one we send in our query.
Next we provide the PagedList builder with our PagedList.Config
as well as setting the data source to the one returned from our Dao. The last two things we need to provide are a background executor and a main thread executor. These will be used by our PagedList
when it is querying and returning results to our adapter.
The last thing we do is use our AppToolkitTaskExecutor
to kick us back to the main thread give our PagedList
object to our adapter. The magic of the PagedListAdapter
will then take over for us and we can begin to scroll through our data.
It should be noted that our IDE warns us not to make use of AppToolkilTaskExecutor
since it is marked as @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
, you can avoid this problem by simply creating your own executors and using them instead.
Now let's look at what it would take to write our own TiledDataSource
from scratch.
Our custom TiledDataSource
will need to inherit from TiledDataSource
and then we'll have a few things to do. We'll need to return the total possible count of our dataset from the countItems
method and a way to load a particular page of data via our loadRange
method. The only other thing we'll need to do is setup an observer for our room database instance's InvalidationTracker
, that way we can be notified when the database table we are watching has a data change event which will cause our datasource to invalidate itself.
Back in our activity we can simply modify the code we used in our previous example to use our new custom TiledDataSource
Our last example will show how you can make use of a KeyedDataSource
. Room does not yet auto implement one of these for you but they do plan to do so in the future. We will need to modify our Dao to provide some additional queries that we can use with our KeyedDataSource
.
@Query("SELECT * FROM teams ORDER BY name DESC LIMIT :limit")
fun getInitialData(limit: Int): Array<Team>
@Query("SELECT * FROM teams WHERE name < :name ORDER BY name DESC LIMIT :limit")
fun getDataAfterName(name: String, limit: Int): Array<Team>
@Query("SELECT * FROM teams where name > :name ORDER BY name ASC LIMIT :limit")
fun getDataBeforeName(name: String, limit: Int): Array<Team>
You will notice that we have an initial query that grabs a list of teams and sorts them according to their name. The other two queries make use of a team name argument to determine how to load data either before or after that name in the larger team dataset.
Implementing the KeyedDataSource
is now a trivial exercise thanks to these query methods on our Dao.
We'll add an InvalidationTracker
observer just as we did in our TiledDataSource
to make sure that we can watch for data changes in the table. We'll then implement the getKey
method to help the DataSource know how to get the key it needs for the queries given an already loaded Team object. We do this by simply returning the team's name.
The loadInitial
method will use our getInitialData
method in our Dao to get the first page of data. The loadBefore
and loadAfter
methods will make use of their matching Dao query methods to load additional teams depending on where in the data we currently are and how the user is scrolling through the paged data. Our activity will simply make use of our KeyedDataSource
instead of our TiledDataSource
as well as provide a different key. Everything else will remain the same.
As you can see the new paging library makes it easier than ever to work with large sets of paged data in your android app. When combined with Room it becomes an almost trivial exercise to implement. This allows you to devote more time to focus on your app's user experience and less time on boring never seen technical architecture.
Here is a list of links to help guide you in your own implementation.
https://developer.android.com/topic/libraries/architecture/paging.html
https://developer.android.com/topic/libraries/architecture/adding-components.html
https://developer.android.com/reference/android/arch/paging/PagedList.html
https://developer.android.com/reference/android/arch/paging/TiledDataSource.html
https://developer.android.com/reference/android/arch/paging/KeyedDataSource.html