Android Architecture Components: Using the Paging Library With Room


by 
Difficulty:IntermediateLength:MediumLanguages:
English

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.

To be able to follow this tutorial, you’ll need:

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!

  • ANDROID SDK
    Introduction to Android Architecture Components
    Tin Megali

A sample project for this tutorial can be found on our GitHub repo so you can easily follow along.

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’s RecyclerView.

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!

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.

Fire up your Android Studio 3 and create a new project with an empty activity called MainActivity. Make sure to check Include Kotlin support.

Android Studio create project screen

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). 

1
2
3
4
5
6
7
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. 

1
2
3
4
5
6
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.

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.

01
02
03
04
05
06
07
08
09
10
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
}

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.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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.

PagedList is a special kind of List for showing paged data in Android:

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).

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 use Integer 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 DataSources.

Here’s is what our Room database class AppDatabase looks like:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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).

For our UI to store, observe, and serve data in a lifecycle-conscious way, we need a ViewModel. Our PersonsViewModel, which extends the AndroidViewModelclass, is going to function as our ViewModel.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 for LiveData<PagedList>, given a DataSource.Factoryand a PagedList.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>.

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 from PagedLists in a RecyclerView.

So we create a PersonAdapter that extends PagedListAdapter.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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.

01
02
03
04
05
06
07
08
09
10
11
12
13
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 returns true if the two items represent the same object or false if they are different.
  • areContentsTheSame is called to check whether two items have the same data. This method returns true if the contents of the items are the same or false 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.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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
        }
    }
}

In our onCreate() of our MainActivity, we simply did the following:

  • initialize our viewModel field using the utility class ViewModelProviders
  • create an instance of PersonAdapter
  • configure our RecyclerView
  • bind the PersonAdapter to the RecyclerView
  • observe the LiveData and submit the PagedList objects over to the PersonAdapter by invoking submitList()
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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:

Tutorial result screenshot

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.

Advertisement

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.

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.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s