在本教程中,我将向您展示如何将Android Architecture组件中的Paging库与Android应用中由Room支持的数据库一起使用。
您将学习如何使用分页库从Room支持的数据库中有效加载大型数据集,从而在滚动RecyclerView时为用户提供更流畅的体验。
先决条件
要遵循本教程,您需要:
- Android Studio 3.1.3或更高版本
- Kotlin插件 1.2.51或更高版本
- 对Android体系结构组件(尤其是
LiveData
和Room数据库 )有基本的了解
您可以在我们的GitHub存储库中找到本教程的示例项目,因此您可以轻松地进行后续操作。
什么是分页库?
Paging库是另一个添加到Architecture Components的库。 该库有助于有效地管理RecyclerView
大数据集的加载和显示。 根据官方文档:
分页库使您可以更轻松地在应用程序的RecyclerView
逐渐优雅地加载数据。
如果您的Android应用程序的任何部分将显示来自本地或远程数据源的大型数据集,但一次仅显示其中一部分,那么您应该考虑使用Paging库。 这将有助于提高您的应用程序的性能!
那么为什么要使用分页库?
既然您已经了解了Paging库的介绍,您可能会问,为什么要使用它? 有一些原因使您考虑在RecyclerView
中加载大型数据集时使用它。
- 它不请求不需要的数据。 当用户滚动列表时,该库仅请求用户可见的数据。
- 节省用户的电池并消耗更少的带宽。 因为它仅请求所需的数据,所以可以节省一些设备资源。
当处理大量数据时,这将是无效的,因为底层数据源将检索所有数据,即使仅将一部分数据显示给用户。 在这种情况下,我们应该考虑分页数据。
1.创建一个Android Studio项目
启动您的Android Studio 3并创建一个名为MainActivity
的空活动的新项目。 确保选中包括Kotlin支持 。
2.添加架构组件
创建新项目后,在build.gradle中添加以下依赖项 。 在本教程中,我们使用最新的Paging库版本1.0.1,而Room是1.1.1(在撰写本文时)。
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"
}
这些工件可以在Google的Maven存储库中找到。
allprojects {
repositories {
google()
jcenter()
}
}
通过添加依赖关系,我们教会了Gradle如何查找该库。 确保记住添加项目后同步项目。
3.创建实体
创建一个新的Kotlin数据类Person
。 为简单起见,我们的Person
实体只有两个字段:
- 唯一ID(
id
) - 人名(
name
)
另外,包括一个toString(
方法,该方法仅返回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.创建DAO
如您所知,要让我们通过Room库访问应用程序的数据,我们需要数据访问对象(DAO)。 对于我们自己,我们创建了一个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)
}
在PersonDao
类中,我们有两个@Query
方法。 其中之一是getAll()
,它返回一个LiveData
,其中包含一个Person
对象列表。 另一个是getAllPaged()
,它返回一个DataSource.Factory
。
根据官方文档, DataSource
类是:
用于将快照数据页面加载到PagedList
基类。
PagedList
是一种特殊的List
用于显示Android中的分页数据:
PagedList
是一个List,它从DataSource
以块(页面)的形式加载其数据。 可以使用get(int)
访问项目,并可以使用loadAround(int)
触发进一步的加载。
我们所谓的Factory
在静态方法DataSource
类,作为一个工厂(而无需指定确切类将要创建的对象的创建对象)的DataSource
。 此静态方法采用两种数据类型:
- 标识
DataSource
项目的键。 请注意,对于“房间”查询,页面已编号,因此我们将Integer
用作页面标识符类型。 可以使用Paging库来“锁定”页面,但是Room目前不提供该功能。 - 由
DataSource
加载的列表中的项目或实体(POJO)的类型。
5.创建数据库
这是我们的Room数据库类AppDatabase
外观:
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()
}
}
}
在这里,我们创建了一个数据库实例,并使用新的WorkManager API将其预填充了数据。 请注意,预先填充的数据只是1,000个名称的列表(请参阅提供的示例源代码以了解更多信息)。
6.创建ViewModel
为了使我们的UI以关注生命周期的方式存储,观察和提供数据,我们需要一个ViewModel
。 我们的PersonsViewModel
扩展了AndroidViewModel
类,它将用作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
}
在此类中,我们只有一个字段称为personsLiveData
。 该字段只是一个LiveData
,其中包含Person
对象的PagedList
。 因为这是一个LiveData
,所以我们的UI( Activity
或Fragment
)将通过调用getter方法getPersonsLiveData()
来观察此数据。
我们在init
块内初始化了personsLiveData
。 在此块内,我们通过调用PersonDao
对象的AppDatabase
单例来获取DataSource.Factory
。 当我们得到这个对象时,我们调用getAllPaged()
。
然后,我们创建一个LivePagedListBuilder
。 这是官方文档中关于LivePagedListBuilder
:
给定DataSource.Factory
和PagedList.Config
LiveData<PagedList>
生成器。
我们为其构造函数提供一个DataSource.Factory
作为第一个参数,并将页面大小作为第二个参数(在我们自己的情况下,页面大小将为50)。 通常,您应该选择一个大于您一次可以显示给用户的最大数字的大小。 最后,我们调用build()
构造并返回一个LiveData<PagedList>
。
7.创建PagedListAdapter
要在RecyclerView
显示我们的PagedList
数据,我们需要一个PagedListAdapter
。 这是来自官方文档的此类的清晰定义:
RecyclerView.Adapter
基类,用于在RecyclerView
显示来自PagedList
的分页数据。
因此,我们创建了一个扩展PagedListAdapter
的PersonAdapter
。
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
}
}
}
与RecyclerView.Adapter
任何其他子类一样,使用PagedListAdapter
。 换句话说,您必须实现onCreateViewHolder()
和onBindViewHolder()
。
要扩展PagedListAdapter
抽象类,您必须在其构造函数中提供PageLists
的类型(这应该是一个普通的旧Java类:POJO),以及一个扩展将由适配器使用的ViewHolder
的类。 在我们的例子中,我们分别给它Person
和PersonViewHolder
作为第一个和第二个参数。
需要注意的是PagedListAdapter
需要你传递一个DiffUtil.ItemCallback
到PageListAdapter
构造。 DiffUtil
是RecyclerView
实用程序类,它可以计算两个列表之间的差异,并输出将第一个列表转换为第二个列表的更新操作列表。 ItemCallback
是一个内部抽象静态类(在DiffUtil
内部),用于计算列表中两个非空项目之间的差异。
具体来说,我们将PersonDiffCallback
给我们的PagedListAdapter
构造函数。
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
}
}
因为我们正在实现DiffUtil.ItemCallback
,所以我们必须实现两个方法: areItemsTheSame()
和areContentsTheSame()
。
-
areItemsTheSame
来检查两个对象是否表示同一项目。 例如,如果您的商品具有唯一的ID,则此方法应检查其ID的相等性。 如果两个项目表示相同的对象,则此方法返回true
否则,返回false
。 -
areContentsTheSame
来检查两个项目是否具有相同的数据。 如果项目的内容相同,则此方法返回true
否则,则返回false
。
我们的PersonViewHolder
内部类只是一个典型的RecyclerView.ViewHolder
。 它负责根据需要将模型中的数据绑定到列表中一行的小部件中。
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.显示结果
在MainActivity
onCreate()
中,我们简单地执行了以下操作:
- 使用实用工具类
ViewModelProviders
初始化我们的viewModel
字段 - 创建一个
PersonAdapter
实例 - 配置我们的
RecyclerView
- 将
PersonAdapter
绑定到RecyclerView
- 观察
LiveData
并通过调用submitList()
将PagedList
对象提交给PersonAdapter
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)
})
}
}
最后,当您运行该应用程序时,结果如下:
滚动时,Room可以通过一次加载50个项目并将其提供给我们的PersonAdapter
(这是PagingListAdapter
的子类)来防止出现间隙。 但是请注意,并非所有数据源都会快速加载。 加载速度还取决于Android设备的处理能力。
9.与RxJava集成
如果您在项目中使用或希望使用RxJava,则分页库包含另一个有用的工件: RxPagedListBuilder
。 您可以使用此工件而不是LivePagedListBuilder
来获得RxJava支持。
您只需创建一个实例RxPagedListBuilder
,提供相同的参数,你会为LivePagedListBuilder
-the DataSource.Factory
和页面大小。 然后调用buildObservable()
或buildFlowable()
返回一个Observable
或Flowable
为您PagedList
分别。
要为数据加载工作显式提供Scheduler
,请调用setter方法setFetchScheduler()
。 要还提供用于传递结果的Scheduler
(例如AndroidSchedulers.mainThread()
),只需调用setNotifyScheduler()
。 默认情况下, setNotifyScheduler()
默认为UI线程,而setFetchScheduler()
默认为I / O线程池。
结论
在本教程中,您学习了如何轻松地将Room中的Android架构组件(属于Android Jetpack的一部分)中的Paging组件使用。 这有助于我们从本地数据库有效地加载大型数据集,从而在滚动浏览RecyclerView
的列表时实现更流畅的用户体验。
我强烈建议您查看官方文档,以了解有关Android中的Paging库的更多信息。