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.

LivePagedListProvider

  • 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

https://github.com/bltuckerdevblog/paging-library-testdrive

http://www.football-data.org/index