PART 1 - Understanding the Paging Library, PagedList
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.
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 usingnotifyItemRangeInserted
,notifyItemRangeRemoved()
, etc. We just need to callsubmitList()
and it will do the magic for us.
- if we use
- 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
Before we can use PagedList
, we need to configure it with 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.
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.
PagedList
Let's build something with 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
.
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
.
- this is based on the next item, say you have a
- PositionalDataSource
- I never used this, but this is a good post about this: paging-beyond-room.
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.
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
Clap to support the author, help others find it, and make your opinion count.