1. 什么是Paging3
Paging3是谷歌开发的一个针对Android系统的库。Paging3可以让应用无缝的加载并展示从本地或者服务器中获取的数据。
对比Paging2,Paging3有以下5点的更新。
PagingSource
的实现更加简单。- 支持
Flow
。 - 增加请求数据时状态的回调,
- 支持设置
Header
和Footer
。 - 支持多种方式请求数据,比如网络请求和数据库请求。
关于Paging2的教程可以看这篇:juejin.cn/post/684490…
2. Paging3的架构
上图所示的是利用Paging3的应用的架构。在Repository
层获取数据,然后传给ViewModel
层中的Pager,最后PagingDataAdaper
从ViewModel
中获取Flow
数据并展示。
PagingSource
: 是一个单一的数据源,PagingSource可以从本地或服务器中加载数据。RemoteMediator
: 也是一个单一的数据源,在PagingSource中没有数据的时候,会使用RemoteMediator的数据。如果数据既存在于数据库,又存在于服务器中,更多的情况PagingSource用于数据库请求,RemoteMediator用于服务器请求。PagingData
: 单次分页数据的容器。Pager
: 用来构建Flow的类,实现数据加载完成的回调。PagingDataAdapter
: 分页加载数据的RecyclerView的适配器。
3. Paging3的实现
3.1 引入库
根据项目的需要添加库依赖到当前项目的Gradle文件中。
def paging_version = "3.0.0-beta01"
implementation "androidx.paging:paging-runtime:$paging_version"
// alternatively - without Android dependencies for tests
testImplementation "androidx.paging:paging-common:$paging_version"
// optional - RxJava2 support
implementation "androidx.paging:paging-rxjava2:$paging_version"
// optional - RxJava3 support
implementation "androidx.paging:paging-rxjava3:$paging_version"
// optional - Guava ListenableFuture support
implementation "androidx.paging:paging-guava:$paging_version"
// Jetpack Compose Integration
implementation "androidx.paging:paging-compose:1.0.0-alpha08"
复制代码
3.2 配置DataStore
在Paging2中会有3种类型的Page Source
,这导致我们在使用之前需要好好考虑应该使用哪一种。这着实让开发者抓耳挠腮。
但是在Paging3种只有一种Page Source
,不用在为选择哪一种而苦恼了,直接闭着眼睛用就可以了。
Page Source
需要继承自PagingSource
, 同时需要重写load
和getRefreshKey
方法。
class PersonDataSource(private val repository: PersonRepository) : PagingSource<Int, Person>() {
// Load data when got data from DataSource(server or local cache).
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Person> {
val pos = params.key ?: START_INDEX
val startIndex = pos * params.loadSize + 1
val endIndex = (pos + 1) * params.loadSize
return try {
// Get data from DataSource
val personList = repository.getPersonList(startIndex, endIndex)
// Return data to RecyclerView by LoadResult
LoadResult.Page(
personList,
if (pos <= START_INDEX) null else pos - 1,
if (personList.isEmpty()) null else pos + 1
)
} catch (exception: Exception) {
// Return exception by LoadResult
LoadResult.Error(exception)
}
}
// Return the position of data when refresh RecyclerView.
override fun getRefreshKey(state: PagingState<Int, Person>): Int? {
return 0
}
companion object {
private const val START_INDEX = 0
}
}
复制代码
从上面的代码可以知道,我们可以通过LoadParams
参数可以获得Paging的loadSize
和key
(起始位置)的信息。
3.3 创建一个可观察的数据集
接下来我们需要在ViewModel
中创建一个可观察的数据集。在这里所说的可观察数据集指的是LiveData
,Flow
和RxJava
中的Observable
和Flowable
等数据类型。需要值得注意的是,如果使用的RxJava
的数据集,不要忘记引入RxJava
的库。
在我的Demo中我使用的是Flow
。
var personList =
Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 20
),
pagingSourceFactory = {
PersonDataSource(repository)
}
).flow
复制代码
把PagingConfig
和PagingSource
传给Pager
, Pager
会返回一个Flow
。
在PagingConfig
中,我们需要设置PageSize
,enablePlaceholders
和initialLoadSize
。
在PagingSourceFactor
中传入我们在上面已经写好的DataSource。
3.4 创建一个Adapter
我们需要创建一个RecyclerView的adapter,该adapter需要继承自PagingDataAdapter
。具体的写法和ListAdapter
类似。
class PersonAdapter(private val context: Context) :
PagingDataAdapter<Person, PersonAdapter.ViewHolder>(PersonDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val person = getItem(position)
holder.binding.also {
it.textViewName.text = person?.name
it.textViewAge.text = person?.age.toString()
if (position % 2 == 0) {
Glide.with(context).load(person?.photoUrl).into(it.imageView)
} else {
Glide.with(context).load(R.drawable.ic_studio_icon).into(it.imageView)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
RecyclerItemBinding.inflate(
LayoutInflater.from(context), parent, false
)
)
}
class ViewHolder(val binding: RecyclerItemBinding) :
RecyclerView.ViewHolder(binding.root)
}
class PersonDiffCallback : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem == newItem
}
}
复制代码
3.5 显示item
这里的实现方法跟一般的RecyclerView中显示Item是一样的。简而言之,需要实现以下几项。
- 创建和设置adapter的实例。
- 创建coroutines.
- 在coroutines scope中接收Flow数据。
val adapter = PersonAdapter(this)
binding.recyclerView.adapter = adapter
// launch an coroutines, and received flow data. Finally, submit data to the adapter.
lifecycleScope.launch {
viewModel.personList.collectLatest {
adapter.submitData(it)
}
}
复制代码
3.6 观察加载数据的状态
我们可以在adapter中设置一个listener来观察加载数据的状态。一共有3种状态。
LoadState.Loading
: 应用正在加载数据。LoadState.NotLoading
: 应用没有在加载数据。LoadState.Error
: 应用在加载数据的时候发生了错误。
adapter.addLoadStateListener {state ->
when(state.refresh){
is LoadState.Loading -> {
binding.swipeRefreshLayout.isRefreshing = true
}
is LoadState.NotLoading -> {
binding.swipeRefreshLayout.isRefreshing = false
}
is LoadState.Error -> {
// show an error dialog.
}
}
}
复制代码
3.7 设置Header和Footer
我们可以在数据加载中和加载更多时显示特定的View。
首先,我们要创建一个继承自LoadStateAdapter
的Adapter,
具体实现方法跟PagingDataAdapter
的实现方法相似,不同的点在onBindViewHolder
方法上。
关于具体的实现可以参考下面代码。
class PersonLoadStateAdapter(private val context: Context) :
LoadStateAdapter<PersonLoadStateAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
// change the view when LoadStat was changed.
when(loadState){
is LoadState.Loading -> {
holder.binding.progressBar.visibility = View.VISIBLE
holder.binding.errorMsg.visibility = View.INVISIBLE
holder.binding.retryButton.visibility = View.INVISIBLE
}
is LoadState.NotLoading -> {
holder.binding.progressBar.visibility = View.INVISIBLE
holder.binding.errorMsg.visibility = View.INVISIBLE
holder.binding.retryButton.visibility = View.INVISIBLE
}
is LoadState.Error -> {
holder.binding.progressBar.visibility = View.INVISIBLE
holder.binding.errorMsg.visibility = View.VISIBLE
holder.binding.retryButton.visibility = View.VISIBLE
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
return ViewHolder(
LoadStateFooterViewItemBinding.inflate(
LayoutInflater.from(context), parent, false
)
)
}
class ViewHolder(val binding: LoadStateFooterViewItemBinding) :
RecyclerView.ViewHolder(binding.root)
}
复制代码
第二步,我们可以通过withLoadStateHeaderAndFooter
方法把LoadStateAdapter
拼接到PagingDataAdapter
上。然后把拼接完后的Adapter传递给RecyclerView即可。
val adapter = PersonAdapter(this)
val concatAdapter: ConcatAdapter = adapter.withLoadStateFooter(
footer = PersonLoadStateAdapter(this)
)
binding.recyclerView.adapter = concatAdapter
复制代码
4. Others
Github: github.com/HyejeanMOON…