64

Android Architecture Components: Using the Paging Library With Room

 5 years ago
source link: https://www.tuicool.com/articles/hit/M3ieqeJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 andRoom 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'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!

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

RNFNJvi.jpg!web

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:

id
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: 

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

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 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 for LiveData<PagedList> , given a DataSource.Factory and 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>

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

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

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 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()
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:

N3Ab2qI.png!web

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK