PART 1 - Understanding the Paging Library, PagedList

Tan Jun Rong avatar

Tan Jun Rong

Background

Almost every application that is used to show some sort of data needs to have pagination functionality.

Previously, I used to implement my pagination by manually calculating the page number. The problem by doing this is having to copy this logic over and over again into any screens that require pagination.

happy paging!
happy paging!

Now that Google has provided us with the Paging Library, we have a nice interface for pagination. I find the library pretty neat.

Here's a few features that I find useful:

  • bi-directional loading
    • usually we only need to load more data in one direction, for example, load page 1, then page 2, and so on. However, in cases like comments or chat list, we need to be able to load from the middle, and the loading can go in both direction
  • works well with PagedListAdapter
    • if we use PagedListAdapter, we don't have to manually handle which data has changed or removed by using notifyItemRangeInserted, notifyItemRangeRemoved(), etc. We just need to call submitList() and it will do the magic for us.
  • works with RxJava
  • able to handle empty state, load more, network state
  • handle load more detection for us

What's in this post

In this post, I will discuss how to use the Paging Library from Google. I find it a bit hard to get started with his library at the beginning, so I want to write down what I learned.

This library is recommended to be used along side with the PagedListAdapter, but I find it a little too magical for me to understand. So in this post, I want to explore using PagedList directly before using it with PagedListAdapter.

Pre-requisite

Some basic understanding of Kotlin and RxJava to understand the examples in this post.

Basic Concept: PagedList

Let's begin by understanding the main class of the Paging library, the PagedList class.

The documentation says that PagedList is:

Lazy loading list that pages in immutable content from a DataSource.
A PagedList is a List which loads its data in chunks (pages) from a DataSource. Items can be accessed with get(int), and further loading can be triggered with loadAround(int). To display a PagedList, see PagedListAdapter, which enables the binding of a PagedList to a RecyclerView.

In my own words, PagedList is something that provides paginated data, where the data can be anything, it can be a Post, it can be Feed, depending on your data model.

Documentation: PagedList

PagedList
PagedList

Before we can use PagedList, we need to configure it with DataSource.

PagedList depends on DataSource
PagedList depends on DataSource

Documentation: Data Source

So if we look at the responsibility of each of them.

  • PagedList provides pagination functionality
  • DataSource provides the data.

To put it simply, whoever holding PagedList is able to provide paginated data. 👇

PagedList depends on DataSource, and provides paginated data
PagedList depends on DataSource, and provides paginated data

Usually we pass PagedList into RecyclerView's PagedListAdapter and the adapter will help us control PagedList and get more data whenever it's running out of data.

Next, we'll try to use PagedList directly to understand how it works.


Let's build something with PagedList

It's easier to look at an example to see how it works. Let's build this trivial app of loading more dogs when we hit the button Load More.

Paginating dogs
Paginating dogs

DataSource

Now that we understand the concept and know what to build. Let's see some code already! 💻🖱

The documentation says that there are 3 different types of DataSource:

  • PageKeyedDataSource
    • this is based on page, for example page 1, page 2, etc
  • ItemKeyedDataSource
    • this is based on the next item, say you have a comment X, you can use it to load the next comment, comment Y.
  • PositionalDataSource

Each of them serve a different purpose, we will use PageKeyedDataSource for this post, since it's the easiest.

Let's say our data comes from an array:

val animalList = listOf(
            Animal("dog-1"),
            Animal("dog-2"),
            Animal("dog-3"),
            Animal("dog-4"),
            ...)

To use PageKeyedDataSource, we need to override 3 methods.

class AnimalDataSource: PageKeyedDataSource(){
    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
        TODO("not implemented")
    }
    override fun loadAfter(params: LoadParams, callback: LoadCallback) {
        TODO("not implemented")
    }
    override fun loadBefore(params: LoadParams, callback: LoadCallback) {
        TODO("not implemented")
    }
}

As their names indicate, they are responsible to load the initial, before and after set of data.

The 3 methods
The 3 methods

First, we override loadInitial(). We are given callback as the argument, and we need to use it to provide the initial set of data, the next page, and the previous page:

override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) {
       // initialData 
        val initialData = animalList.subList(0, params.requestedLoadSize)
        val pageBefore = firstPage - 1
        val pageAfter = firstPage + 1
        callback.onResult(initialData, pageBefore, pageAfter)
    }

Next, we override loadAfter() 👇
Similarly, we are given callback to provide our data. We obtain our subset of data from afterData from animalList, then we provide using callback.onResult().

override fun loadAfter(params: LoadParams, callback: LoadCallback) {
        val start = params.key * params.requestedLoadSize
        val afterData = animalList.subList(start, start + params.requestedLoadSize)
        callback.onResult(afterData, params.key + 1)
    }

We are loading data in one direction, so we will leave loadBefore() blank:

override fun loadBefore(params: LoadParams, callback: LoadCallback) {
        // do nothing
    }

The entire file looks like this in Github: AnimalDataSource.kt

PagedList

Now, Data Source is ready, let's move on to build PagedList.

Documentation: PagedList.Builder

val config: PagedList.Config = PagedList.Config.Builder()
                .setInitialLoadSizeHint(2)
                .setPageSize(2)
                .build()
val pagedList = PagedList.Builder(AnimalDataSource(), config)
                .setFetchExecutor(Executors.newSingleThreadExecutor())
                .setNotifyExecutor(Executors.newSingleThreadExecutor())
                .build()

We need to pass in a config for PagedList.Builder. There are a bunch of settings that we can set, but over here, we just use the minimal settings.

Note: I am not good at executor, so do your own research

With setInitialLoadSizeHint(2) and setPageSize(2), it will begin by loading 2 items, and continued to load 2 more every time we make another request, as shown in the gif at the top 👆 .

🎉 Finally, we have a pagedList instance! 🎉

We can now use it to load more item. To load more items, we need to simply call pagedList.loadAround(index), where index is the location we want to load more data.

In this case, we will use the last position of our data set:

pagedList.loadAround(pagedList.size - 1)

Every time we call loadAround(), we can check pagedList.size, notice that the number has increased. Also, we can use pagedList.forEach{} to loop through the data.

PagedList Runs on Background Thread

There is a caveat of using PagedList directly: it runs on background thread.

Under Loading Data section in the doc, it says:

Loading Data
All data in a PagedList is loaded from its DataSource. Creating a PagedList loads the first chunk of 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.

That is why in my example, I wrote it using RxJava to help me handle the threadings. You can take a look how it is done the example code I wrote: AnimalActivity.kt

RxPagedListBuilder

To make our life easier, the library also provides a way to build PagedList into an observable directly. That is by using RxPagedListBuilder.

There is one extra step while using this, we need to define the DataSourceFactory that will provide DataSource for the builder.

You can compare the difference between SushiActivity which uses RxPagedListBuilder vs. AnimalActivity which uses PagedListBuilder.

private val pagedListObservable = Observable.fromCallable {
    val config: PagedList.Config = PagedList.Config.Builder()
            .setInitialLoadSizeHint(2)
            .setPageSize(2)
            .build()
    PagedList.Builder(AnimalDataSource(), config)
            .setInitialKey(0)
            .setFetchExecutor(Executors.newSingleThreadExecutor())
            .setNotifyExecutor(Executors.newSingleThreadExecutor())
            .build()
}.subscribeOn(Schedulers.io())
private val pagedListObservable by lazy {
    val config: PagedList.Config = PagedList.Config.Builder()
        .setInitialLoadSizeHint(2)
        .setPageSize(2)
        .build()
    RxPagedListBuilder(SushiDataSourceFactory(), config).buildObservable()
}

It can be seen that using RxPagedListBuilder is a little shorter. 👍

Wrapping up

The entire project is available in Github here: LearningPagingLibrary
Next I will write about some tips about using it with PagedListAdapter.
Hope you enjoy the post.

See you next time!

Tan Jun Rong avatar
Written By

Tan Jun Rong

Android Programmer who likes writing blogs, reading, coffee, snowboarding.
Published in Android
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.