In this tutorial, I'll show you how to use the Paging library from the Android Architecture Components with a Room-backed database in an Android app.
You'll learn how to use the Paging library to efficiently load large data sets from a Room-backed database—giving your users a smoother experience while scrolling in a RecyclerView.
Prerequisites
To be able to follow this tutorial, you'll need:
- Android Studio 3.1.3 or higher
- Kotlin plugin 1.2.51 or higher
- a basic understanding of the Android Architecture Components (especially the
LiveData
and Room database)
If you haven't learnt about the architecture components, you are strongly advised to check out our awesome series all about Android Architecture Components by Tin Megali. Make sure you go dive in!
A sample project for this tutorial can be found on our GitHub repo so you can easily follow along.
What Is the Paging Library?
The Paging library is another library added to the Architecture Components. The library helps efficiently manage the loading and display of a large data set in the RecyclerView
. According to the official docs:
The Paging Library makes it easier for you to load data gradually and gracefully within your app'sRecyclerView
.
If any part of your Android app is going to display a large dataset from either a local or remote data source but displays only part of it at a time, then you should consider using the Paging library. This will help improve the performance of your app!
So Why Use the Paging Library?
Now that you've seen an introduction to the Paging library, you might ask, why use it? Here are some reasons why you should consider using it in loading large data sets in a RecyclerView
.
- It doesn't request data that aren't needed. This library only requests data that are visible to the user—as the user scrolls through the list.
- Saves the user's battery and consumes less bandwidth. Because it only requests data that are needed, this saves some device resources.
It won't be efficient when working with a large amount of data, as the underlying data source retrieves all the data, even though only a subset of that data is going to be displayed to the user. In such a situation, we should consider paging the data instead.
1. Create an Android Studio Project
Fire up your Android Studio 3 and create a new project with an empty activity called MainActivity
. Make sure to check Include Kotlin support.
2. Add the Architecture Components
After creating a new project, add the following dependencies in your build.gradle. In this tutorial, we are using the latest Paging library version 1.0.1, while Room is 1.1.1 (as of this writing).
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "android.arch.persistence.room:runtime:1.1.1" kapt "android.arch.persistence.room:compiler:1.1.1" implementation "android.arch.paging:runtime:1.0.1" implementation "com.android.support:recyclerview-v7:27.1.1" }
These artifacts are available at Google’s Maven repository.
allprojects { repositories { google() jcenter() } }
By adding the dependencies, we have taught Gradle how to find the library. Make sure you remember to sync your project after adding them.
3. Create the Entity
Create a new Kotlin data class Person
. For simplicity's sake, our Person
entity has just two fields:
- a unique ID (
id
) - the name of the person (
name
)
In addition, include a toString(
method that simply returns the name
.
import android.arch.persistence.room.Entity import android.arch.persistence.room.PrimaryKey @Entity(tableName = "persons") data class Person( @PrimaryKey val id: String, val name: String ) { override fun toString() = name }
4. Create the DAO
As you know, for us to access our app's data with the Room library, we need data access objects (DAOs). In our own case, we have created a PersonDao
.
import android.arch.lifecycle.LiveData import android.arch.paging.DataSource import android.arch.persistence.room.Dao import android.arch.persistence.room.Delete import android.arch.persistence.room.Insert import android.arch.persistence.room.Query @Dao interface PersonDao { @Query("SELECT * FROM persons") fun getAll(): LiveData<List<Person>> @Query("SELECT * FROM persons") fun getAllPaged(): DataSource.Factory<Int, Person> @Insert fun insertAll(persons: List<Person>) @Delete fun delete(person: Person) }
In our PersonDao
class, we have two @Query
methods. One of them is getAll()
, which returns a LiveData
that holds a list of Person
objects. The other one is getAllPaged()
, which returns a DataSource.Factory
.
According to the official docs, the DataSource
class is the:
Base class for loading pages of snapshot data into a PagedList
.
A PagedList
is a special kind of List
for showing paged data in Android:
APagedList
is a List which loads its data in chunks (pages) from aDataSource
. Items can be accessed withget(int)
, and further loading can be triggered withloadAround(int)
.
We called the Factory
static method in the DataSource
class, which serves as a factory (creating objects without having to specify the exact class of the object that will be created) for the DataSource
. This static method takes in two data types:
- The key that identifies items in
DataSource
. Note that for a Room query, pages are numbered—so we useInteger
as the page identifier type. It is possible to have "keyed" pages using the Paging library, but Room doesn't offer that at present. - The type of items or entities (POJOs) in the list loaded by the
DataSource
s.
5. Create the Database
Here's is what our Room database class AppDatabase
looks like:
import android.arch.persistence.db.SupportSQLiteDatabase import android.arch.persistence.room.Database import android.arch.persistence.room.Room import android.arch.persistence.room.RoomDatabase import android.content.Context import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.chikeandroid.pagingtutsplus.utils.DATABASE_NAME import com.chikeandroid.pagingtutsplus.workers.SeedDatabaseWorker @Database(entities = [Person::class], version = 1, exportSchema = false) abstract class AppDatabase : RoomDatabase() { abstract fun personDao(): PersonDao companion object { // For Singleton instantiation @Volatile private var instance: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } } } private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .addCallback(object : RoomDatabase.Callback() { override fun onCreate(db: SupportSQLiteDatabase) { super.onCreate(db) val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build() WorkManager.getInstance()?.enqueue(request) } }) .build() } } }
Here we have created a single instance of our database and pre-populated it with data using the new WorkManager API. Note that the data pre-populated is just a list of 1,000 names (dive into the sample source code provided to learn more).
6. Creating the ViewModel
For our UI to store, observe, and serve data in a lifecycle-conscious way, we need a ViewModel
. Our PersonsViewModel
, which extends the AndroidViewModel
class, is going to function as our ViewModel
.
import android.app.Application import android.arch.lifecycle.AndroidViewModel import android.arch.lifecycle.LiveData import android.arch.paging.DataSource import android.arch.paging.LivePagedListBuilder import android.arch.paging.PagedList import com.chikeandroid.pagingtutsplus.data.AppDatabase import com.chikeandroid.pagingtutsplus.data.Person class PersonsViewModel constructor(application: Application) : AndroidViewModel(application) { private var personsLiveData: LiveData<PagedList<Person>> init { val factory: DataSource.Factory<Int, Person> = AppDatabase.getInstance(getApplication()).personDao().getAllPaged() val pagedListBuilder: LivePagedListBuilder<Int, Person> = LivePagedListBuilder<Int, Person>(factory, 50) personsLiveData = pagedListBuilder.build() } fun getPersonsLiveData() = personsLiveData }
In this class, we have a single field called personsLiveData
. This field is simply a LiveData
that holds a PagedList
of Person
objects. Because this is a LiveData
, our UI (the Activity
or Fragment
) is going to observe this data by calling the getter method getPersonsLiveData()
.
We initialized personsLiveData
inside the init
block. Inside this block, we get the DataSource.Factory
by calling the AppDatabase
singleton for the PersonDao
object. When we get this object, we call getAllPaged()
.
We then create a LivePagedListBuilder
. Here's what the official documentation says about a LivePagedListBuilder
:
Builder forLiveData<PagedList>
, given aDataSource.Factory
and aPagedList.Config
.
We supply its constructor a DataSource.Factory
as the first argument and the page size as the second argument (in our own case, the page size will be 50). Typically, you should choose a size that's higher than the maximum number that you might display at once to the user. In the end, we call build()
to construct and return to us a LiveData<PagedList>
.
7. Creating the PagedListAdapter
To show our PagedList
data in a RecyclerView
, we need a PagedListAdapter
. Here's a clear definition of this class from the official docs:
RecyclerView.Adapter
base class for presenting paged data fromPagedList
s in aRecyclerView
.
So we create a PersonAdapter
that extends PagedListAdapter
.
import android.arch.paging.PagedListAdapter import android.content.Context import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import com.chikeandroid.pagingtutsplus.R import com.chikeandroid.pagingtutsplus.data.Person import kotlinx.android.synthetic.main.item_person.view.* class PersonAdapter(val context: Context) : PagedListAdapter<Person, PersonAdapter.PersonViewHolder>(PersonDiffCallback()) { override fun onBindViewHolder(holderPerson: PersonViewHolder, position: Int) { var person = getItem(position) if (person == null) { holderPerson.clear() } else { holderPerson.bind(person) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder { return PersonViewHolder(LayoutInflater.from(context).inflate(R.layout.item_person, parent, false)) } class PersonViewHolder (view: View) : RecyclerView.ViewHolder(view) { var tvName: TextView = view.name fun bind(person: Person) { tvName.text = person.name } fun clear() { tvName.text = null } } }
PagedListAdapter
is used just like any other subclass of RecyclerView.Adapter
. In other words, you have to implement the methods onCreateViewHolder()
and onBindViewHolder()
.
To extend the PagedListAdapter
abstract class, you will have to supply—in its constructor—the type of PageLists
(this should be a plain old Java class: a POJO) and also a class that extends the ViewHolder
that will be used by the adapter. In our case, we gave it Person
and PersonViewHolder
as the first and second argument respectively.
Note that PagedListAdapter
requires you pass it a DiffUtil.ItemCallback
to the PageListAdapter
constructor. DiffUtil
is a RecyclerView
utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one. ItemCallback
is an inner abstract static class (inside DiffUtil
) used for calculating the diff between two non-null items in a list.
Specifically, we supply PersonDiffCallback
to our PagedListAdapter
constructor.
import android.support.v7.util.DiffUtil import com.chikeandroid.pagingtutsplus.data.Person class PersonDiffCallback : DiffUtil.ItemCallback<Person>() { override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Person?, newItem: Person?): Boolean { return oldItem == newItem } }
Because we are implementing DiffUtil.ItemCallback
, we have to implement two methods: areItemsTheSame()
and areContentsTheSame()
.
areItemsTheSame
is called to check whether two objects represent the same item. For example, if your items have unique ids, this method should check their id equality. This method returnstrue
if the two items represent the same object orfalse
if they are different.areContentsTheSame
is called to check whether two items have the same data. This method returnstrue
if the contents of the items are the same orfalse
if they are different.
Our PersonViewHolder
inner class is just a typical RecyclerView.ViewHolder
. It's responsible for binding data as needed from our model into the widgets for a row in our list.
class PersonAdapter(val context: Context) : PagedListAdapter<Person, PersonAdapter.PersonViewHolder>(PersonDiffCallback()) { // ... class PersonViewHolder (view: View) : RecyclerView.ViewHolder(view) { var tvName: TextView = view.name fun bind(person: Person) { tvName.text = person.name } fun clear() { tvName.text = null } } }
8. Showing the Result
In our onCreate()
of our MainActivity
, we simply did the following:
- initialize our
viewModel
field using the utility classViewModelProviders
- create an instance of
PersonAdapter
- configure our
RecyclerView
- bind the
PersonAdapter
to theRecyclerView
- observe the
LiveData
and submit thePagedList
objects over to thePersonAdapter
by invokingsubmitList()
import android.arch.lifecycle.Observer import android.arch.lifecycle.ViewModelProviders import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.support.v7.widget.RecyclerView import com.chikeandroid.pagingtutsplus.adapter.PersonAdapter import com.chikeandroid.pagingtutsplus.viewmodels.PersonsViewModel class MainActivity : AppCompatActivity() { private lateinit var viewModel: PersonsViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProviders.of(this).get(PersonsViewModel::class.java) val adapter = PersonAdapter(this) findViewById<RecyclerView>(R.id.name_list).adapter = adapter subscribeUi(adapter) } private fun subscribeUi(adapter: PersonAdapter) { viewModel.getPersonLiveData().observe(this, Observer { names -> if (names != null) adapter.submitList(names) }) } }
Finally, when you run the app, here's the result:
While scrolling, Room is able to prevent gaps by loading 50 items at a time and making them available to our PersonAdapter
, which is a subclass of PagingListAdapter
. But note that not all data sources will be loaded quickly. The loading speed also depends on the processing power of the Android device.
9. Integration With RxJava
If you're using or want to use RxJava in your project, the paging library includes another useful artifact: RxPagedListBuilder
. You use this artifact instead of LivePagedListBuilder
for RxJava support.
You simply create an instance of RxPagedListBuilder
, supplying the same arguments as you would for LivePagedListBuilder
—the DataSource.Factory
and the page size. You then call buildObservable()
or buildFlowable()
to return an Observable
or Flowable
for your PagedList
respectively.
To explicitly provide the Scheduler
for the data loading work, you call the setter method setFetchScheduler()
. To also provide the Scheduler
for delivering the result (e.g. AndroidSchedulers.mainThread()
), simply call setNotifyScheduler()
. By default, setNotifyScheduler()
defaults to the UI thread, while setFetchScheduler()
defaults to the I/O thread pool.
Conclusion
In this tutorial, you learned how to easily use the Paging component from the Android Architecture Components (which are part of Android Jetpack) with Room. This helps us efficiently load large data sets from the local database to enable a smoother user experience while scrolling through a list in the RecyclerView
.
I highly recommend checking out the official documentation to learn more about the Paging library in Android.