1. 简介
为了解决RecyclerView
的加载更多的功能,Google
推出了Jetpack
的Paging
组件。通过Paging
组件,可以更流畅无缝的加载更多的数据。
Paging
是利用DataSource
进行数据的更新。根据用途的不同分为三种。
PagaKeyedDataSource<Key, Value>
: 适用于目标数据是按照页数说去数据的场景。key
是页数, 请求的数据参数重包含next/previous
页数的信息。ItemKeyedDataSource<Key, Value>
: 适用于目标数据的加载依赖特定Item的信息,key
包含Item中信息,比如需要根据第N项信息加载第N+1信息。经常应用于评论信息请求。PositionalDataSource<T>
: 适用于目标总数固定,通过特定的位置加载数据,key是位置信息。
添加paging
库的依赖:
implementation "androidx.paging:paging-runtime:2.1.1"
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
2. Paging + Room
首先,这部分教程是大家可以熟练使用Room
和ViewModel
为前提,所以不会有对该部分知识进行详细的说明。
2.1 制作数据库
2.1.1 Entity
@Entity(tableName = "users_table")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int? = null,
@ColumnInfo(name = "first_name")
val firstName: String,
@ColumnInfo(name = "last_name")
val lastName: String,
@ColumnInfo(name = "birthday")
val birthday: String,
@ColumnInfo(name = "nationality")
val nationality: String
)
2.1.2 Dao
这里需要说明的一点是返回值不是List<User>
而是DataSource.Factory
。
其中左边是key
,右边是value
。
key
是用于内部的PositionalDataSource
, 来提示当前数据的位置也就是页数。
而value
显而易见的我们需要使用的数据。
@Dao
interface UserDao {
@Query("SELECT * FROM users_table ORDER BY id ASC")
fun getAllByLivePage(): DataSource.Factory<Int, User>
}
2.1.3 Database
@Database(version = 1, entities = [User::class])
abstract class UserDataBase : RoomDatabase() {
companion object {
private var INSTANCE: UserDataBase? = null
fun getInstance(context: Context): UserDataBase? {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context,
UserDataBase::class.java,
DATABASE_NAME
).build()
}
return INSTANCE
}
fun destroyInstance() {
INSTANCE = null
}
const val DATABASE_NAME = "user_database.db"
}
abstract fun getUserDao(): UserDao
}
2.2 制作PagedListAdapter
不同于以往继承RecyclerAdapter
或者ListAdapter
, 这里我们需要继承PagedListAdapter
。 重写的内容跟ListAdapter
是一模一样,没有什么特别之处,如下。
class UserAdapter : PagedListAdapter<User, UserAdapter.UserViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ItemUserBinding =
DataBindingUtil.inflate(inflater, R.layout.item_user, parent, false)
return UserViewHolder(binding)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
getItem(position)?.also {
holder.binding.txtBirthday.text = it.birthday
holder.binding.txtFirstName.text = it.firstName
holder.binding.txtLastName.text = it.lastName
holder.binding.txtNationality.text = it.nationality
}
}
class UserViewHolder(var binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root)
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<User>() {
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem === newItem
}
}
}
}
2.3 制作Paging的Config
我们需要在ViewModel
中设置Paging
的Config
。我尝试过在Activity
中进行过设置,不能正常运行,不知道我的写法是否哪里问题。
class ShowViewModel(application: Application) : AndroidViewModel(application) {
val dao =
UserDataBase.getInstance(
application.applicationContext
)?.getUserDao()
val allUsers = dao!!.getAllByLivePage()
.toLiveData(Config(pageSize = 10, enablePlaceholders = true, maxSize = 50))
}
从DB中获得的数据要转换成我们熟悉的LiveData
,以及同时需要设置Config
。
pageSize
: 一次从数据库加载的数据量。enablePlaceholders
: 如果加载失败的话,是否显示。默认是true
。maxSize
: 存储在内存中的最大数据量。默认时Int的最大值。prefetchDistance
: 距离最后一个数据有多远的距离时,进行提前获取数据。默认是pageSize
。initialLoadSizeHint
: 加载到pagedList
中的初始数据量。默认是pageSize
的3倍。通常是大约正常一页的数据量。
2.4 监视以及传入Adapter
最后一步是在Activity
中监视,当数据有变化时传入到Adapter中进行更新。
viewModel.allUsers.observe(this, Observer {
adapter.submitList(it)
})
2.5 github
3. Paging + Network
讲完Paging + Room
以后,我们继续尝试Paging + Network
的组合。
首先需要说明一下的是,我没有找到比较合适的Api,所以我在这里进行本地模拟。
还有PagedListAdapter
和上面是完全一样的,所以就不再重复赘述了。
3.1 制作DataSource
我们需要继承PageKeyedDataSource<Key,Value>
,然后重写三个方法。
我们首先介绍一下重写函数中传入的数据。
params: LoadInitialParams<Int>
: 该参数是初始加载的参数。包含两个变量,requestedLoadSize
: 要求的数据的量。placeholdersEnabled
是跟上面讲到的是一样的。callback: LoadInitialCallback<Key, Value>
: 是初始加载完成后的回调。params: LoadParams<Int>
: 是向前和向后翻页时传入的参数。包含两个变量,requestedLoadSize
是跟上面一样,key
是当前所在的页数。callback: LoadCallback<Key, Value>)
: 是向前和向后翻页完成后的回调。
接下来介绍一下要重写的参数。
loadInitial
: 为起始加载。callback
的作用是提醒Paging
数据已经加载完成。onResult
的第一个参数是已加载好的数据。第二参数是前一页的key,如果加载的数据没有前面的数据,则可以设置为null
。第三个参数是后一页的key,我这里是已0为起始,所以在这里传入1。loadBefore
: 是向前翻页时的加载。onResult
中需要传入两个参数,第一个也是请求获得的数据,第二个数向前加载时的页数,我这里设置的是params.key-1
。loadAfter
: 是向后翻页时的加载。onResult
中也是需要传入两个参数,第一个也是数据,第二个时向后翻页加载时的页数,这里设置的是params.key+1
class UsersDataSource : PageKeyedDataSource<Int, User>() {
// 起始加载
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, User>
) {
callback.onResult(getList(0, params.requestedLoadSize), null, 1)
}
// 向前加载
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, User>) {
callback.onResult(getList(params.key, params.requestedLoadSize), params.key - 1)
}
// 向后加载
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, User>) {
callback.onResult(getList(params.key, params.requestedLoadSize), params.key + 1)
}
}
3.2 制作DataSourceFactory
我们需用通过DataSource.Factory
来生成刚才制作的UsersDataSource
。
我们需要继承在上面Paging+Room
出现过的DataSourec.Factory<Key, Value>
。
class CustomPageDataSourceFactory() : DataSource.Factory<Int, User>() {
override fun create(): DataSource<Int, User> {
return UsersDataSource()
}
}
3.3 通过LivePagedListBuilder生成LiveData
最后我们要在Activity
中设置Paging
。LivePagedListBuilder
中需要传入两个参数。第一个是DataSource.Factory<Key,Value>
,就是上面制作的class。第二个是PagedList.Config
,因为上面有介绍所以略过。
// 通过LivePagedListBuilder生成LiveData
val data = LivePagedListBuilder(
CustomPageDataSourceFactory(),
PagedList.Config.Builder()
.setPageSize(20)
.setInitialLoadSizeHint(60)
.build()
).build()
// 监视数据, 数据有变动时传递给adapter
data.observe(this, Observer {
adapter.submitList(it)
})
3.4 github
4. 结论
通过使用Paging
库,可以使数据加载无缝进行,能有更好的用户体验。尤其对Room
有更好的支持,能减少很多的开发量。