An updated look at the Paging Library
The paging library has come a long way since I first wrote about it last September. It recently had a 1.0 release and so I thought now would be a good time to review.
We'll go over whats changed and give an example of how to use each of the DataSource objects that library provides. We'll even look at how to use them with a remote api instead of just a local sqlite database.
Let's start by giving a run down of each of the three data sources in the 1.0 release.
PositionalDataSource : Useful when you have a fixed size countable dataset. If you have a table full of data in sqlite this is probably what you want to use. Room can even generate it for you in your DAO interface.
ItemKeyedDataSource : Use this data source when you have a data set where you need data from items already loaded in order to load additional items.
PageKeyedDataSource : Use this data source when the most recently loaded page of data includes information that points you to the next page. I imagine this will be the most useful data source when it comes to interacting with remote apis. Every search api I have ever had to write a client for has included a meta object with a url linking to the next page of results. PageKeyedDataSource will now make loading results in a page at a time in trivial.
In addition to the DataSource objects, the paging library also has some additional components we'll want to be familiar with in order to use our data source's effectively.
ListAdapter : A specialized RecyclerView adapter that wraps an AsyncListDiffer.
PagedList : An implementation of the List interface that loads data in lazily using DataSource objects. This is where the paging library's magic happens.
DataSource.Factory - Creates DataSource objects.
LivePagedListBuilder : This object will produce a LiveData wrapper around your PagedList objects when you give it your DataSource's factory and a simple configuration object. If Rx is more your self then you might wanna checkout RxPagedListBuilder.
PagedListAdapter : A RecyclerView adapter made to handle PagedList objects. Once we have our PagedList object backed by our DataSource, we'll use these to get the data into our RecyclerViews.
Let's look at how we can use a PositionalDataSource to load data from sqlite using room. Room will actually generate everything for us so this will be the simplest one to implement. The first thing we do is simply add a declaration to our Dao interface that returns a DataSource.Factory<Integer, Team>. The Integer is because that is what our entity's primary key is and Team refers to the entity itself returned by the data source.
We'll use this method in our viewmodel and expose a LiveData PagedList backed by the data source factory that Room is going to generate for us. We'll use a LivePagedListBuilder to create the LiveData object. The builder will need some configuration that will be used to determine how to page the data.
First we'll enable place holders by calling setEnablePlaceholders()
with true. We can do this because we know how many items total are in our data source and by setting this to true our PagedList can provide null data to our adapter for items that have not yet loaded from the datasource.
This will enable our adapter to more nicely present the data on the UI. Our scrollbar for example will always be in the correct place and won't jump around as data loads in. If you can enable placeholders you will generally want to do so, but you will need to remember to check for null in your adapter's onBindViewHolder()
method and handle it accordingly.
Next we let the config object know how many items to load in our initial load by calling setInitialLoadSizeHint()
. We then set the page size, this value will tell the datasource how many items to load at any given time whenever the user nears the edge of the loaded data set.
In our example we set both the initial load size and the page size to 10 but you would really want the initial load size to be a small multiple of your page size so that the user is less likely to run into the edge of your data set and have to wait for the next page to load in. You can also specifiy how far from the edge of loaded data you should be before the next set is paged in by calling setPrefetchDistance()
. This value defaults to the same as your page size.
We then call build on our config builder and pass the created config objects along with our data source factory to the LivePagedListBuilder and then assign the LiveData object that it creates to a public property on our viewmodel.
In our activity's onCreate()
method we can now simply observe the LiveData property on our viewmodel and update our adapter each time it changes.
viewModel.liveTeamData.observe(this, Observer<PagedList<Team>>{
teamNameAdapter.submitList(it)
})
Our TeamNameAdapter is a PagedListAdapter which handles working with our PagedList object for us. We only need to provide implementations for onCreateViewHolder()
, onBindViewHolder()
, and a DiffUtil.ItemCallback.
That's all there is to using the PositionalDataSource object with Room. All the hard work and heavy lifting is done for you.
Let's move on and take a look at the PageKeyedDataSource, we'll have a little more to do in this example since Room won't be generating the data source for us but everything else will be the same. We'll have a data source factory that we combine with a config object to produce a LiveData object backed by a PagedList which we feed to a PagedListAdapter. (Say that 3 times fast)
Let's start with the 'hard' part and create our PageKeyedDataSource.
Implementing our PagedDataSource simply a matter of implementing loadInitial()
, loadAfter()
and loadBefore()
.
Let's look at loadInitial()
first. This will be called once when we load our first page of data. You'll be provided with a params object and a callback. The params will tell your datasource about the requested load size and whether placeholders are enabled. Our datasource is going to be hitting a custom made "search" api that gives us data in set pages so we will simply ignore the params. We'll make our retrofit call and then once we have our result we let the callback know by executing it and passing the data along with information about how to get the next page of data. We don't have a previous page url for the first page of results so we will pass null.
When the user nears the edge of the initial page our data source will receive a call to loadAfter()
and that's when we'll want to get the next page of results. Once again we'll be given some params and a callback. This time we'll make use of the params because it will have the url that we need to call to get the "next" page of data. Once we have the data from retrofit we'll once again execute the callback passing in our new data and giving it the nextPageUrl so that the next loadAfter()
call will be able to execute. loadBefore()
will follow the same pattern.
Now that we have our data source we'll just create a factory for it so that we can pass the factory to our LivePagedListBuilder in the same way we did for our PositionalDataSource example. Once we've created the factory we're done implementing our PageKeyedDataSource. All the heavy lifting will be done once again by our PagedList and PagedListAdapter. In our example we now have a smoothly scrolling list of "products" that page in from a remote api.
Finally, let's take a look at how we can implement an ItemKeyedDataSource. This example like our previous one will once again rely on a remote api to provide its data. The api backing this data source will be an alphabetized list of names that let's you specify a name and a limit that will proceed to give you the next x number of names in the list.
You can see that this looks very familiar to our PageKeyedDataSource object. We still need to implement loadInitial()
, loadAfter()
, and loadBefore()
. The only new thing we need to implement is the getKey()
method. It will be called to determine what you should use as the key associated with a given item. In a more real world example this might be an id, but for our example its just the item itself since we are using the name as the key. You'll also notice that in our loadInitial()
and loadAfter()
methods that our callback does not take a second argument specifiying how to get the next set of data. That is because this datasource will use the last item loaded to determine how to get the next page of data, hence the name ItemKeyedDataSource. Other than that minor difference though our methods are very much the same. We use retrofit to get our data and then we pass it to our callback.
We also need to implement a factory for our data source. This again is done because we will use the factory in conjunction with a config object to create our LivePagedList data in the view model.
That conludes our look at the paginging library. The entire example project can be found here. I've included some additional links below to help you learn more and I would strongly encourage watching the Google I/O talk about the paging library to get a really good idea about how everything works.
Additional Links
https://github.com/bltuckerdevblog/paging-library-testdrive
https://developer.android.com/topic/libraries/architecture/paging/
https://github.com/googlesamples/android-architecture-components/tree/master/PagingSample
https://github.com/googlesamples/android-architecture-components/tree/master/PagingWithNetworkSample