
分页加载的需求大家肯定都碰到过,一般我们会使用 RecycleView 添加刷新的 header 和加载更多的 footer 来处理滑动加载事件。而 Paging 组件就是 Google 推荐的,用来解决分页加载问题的一种解决方案。使用Paging 组件的优势如下:
- 大量数据集列表处理。默认情况下当用户一直滑动列表时,当前界面的数据集列表会越来越大,可能会影响性能。而
Paging组件对此做了处理,可以确保内存中只保存了合理的数据列表。 - 内置的请求去重功能
- 可配置的
RecyclerView适配器,会在用户滚动到已加载数据的末尾时自动请求数据。 - 对 Kotlin 协程和数据流以及
LiveData和RxJava的支持。 - 内置对错误处理功能的支持,包括刷新和重试功能。
如果你想使用 Paging,需要导入如下依赖:
dependencies {
implementation "androidx.paging:paging-runtime:3.3.0"
}
Paging 组件的组成
如上图所示,Paging 组件由3部分组成,分别是数据源(PagingSource或者RemoteMediator)、Pager 和 PagingDataAdapter,图中箭头所指的方向是指数据的流动方向。
- 数据源:顾名思义,数据源就是提供数据的类。它有两种分别是
PagingSource和RemoteMediator。其中PagingSource用于单一数据源的加载场景;而RemoteMediator用于多个数据源的加载场景。 Pager:处理数据源,它会根据PagingConfig每一页的配置来返回我们需要的分页数据。上图中,使用的是 Flow 返回数据。但实际上Pager也可以使用liveData来处理数据PagingDataAdapter:继承RecyclerView的Adapter,是用来展示分页数据的处理器
Paging 使用的代码示例如下:
// 定义数据源
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
// 以异步方式提取更多数据
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
// 当数据发生更改而需要重新加载界面项时,会调用该方法
}
}
// 使用 Pager 获取我们需要显示的数据,这里使用 LiveData
class ArticleViewModel: ViewModel() {
val items = Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = { ArticlePagingSource() }
).liveData
}
// 监听数据变化
val articleAdapter = ArticleAdapter()
viewModel.items.observe(this) {
articleAdapter.submitData(lifecycle, it)
}
// 在 RecyclerView 上显示
recyclerView.adapter = articleAdapter
PagingSource
从上面的代码示例可以看到,PagingSource 中有两个泛型类型,其中第一个表示页码参数的类型,一般声明为 Int 即可;第二个表示每一项数据对应的实体类,这里是 Article 表示文章的信息。
实现 PagingSource 这个抽象类需要实现两个方法 load 和 getRefreshKey 。我们先看 load 方法,作用是以异步方式提取更多数据。假设我们需要使用 load 加载文章数据,代码示例如下:
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
// LoadParams 保存有和加载相关的信息
// 当 params.key 为 null 时,表示是首次加载,这时 start = 0
val start = params.key ?: 0
// 拉取文章数据,其中 params.loadSize 是每一页拉取的数量
val articleList = loadArticleList(start, params.loadSize)
// 使用 LoadResult.Page 返回结果
return LoadResult.Page(
data = articleList, // 数据
// 如果 load() 方法需要提取先于当前页面显示的项,会使用 prevKey
prevKey = when (start) {
0 -> null
else -> (start - params.loadSize).coerceAtLeast(0)
},
// 如果 load() 方法需要提取晚于当前页面显示的项,会使用 nextKey
nextKey = start + params.loadSize + 1
)
}
可以看到 load 方法内部的逻辑其实很简单,就是使用 LoadParams 的加载信息来加载指定的数据,然后以 LoadResult 的形式返回结果。其中LoadResult有三种,分别是
LoadResult.Page如果结果成功,返回它。其中 data 表示数据;prevKey 表示加载上一页时的key;nextKey 加载下一页时的keyLoadResult.Error如果发生错误时返回LoadResult.Invalid如果PagingSource因无法再保证其结果的完整性而应失效时返回
当我们调用了 PagingAdapter 类的 refresh 方法或者 PagingSource 的 invalidate 方法时,表示数据有变化,这时就会调用 PagingSource 的 getRefreshKey 方法来获取新的key,代码示例如下:
// 当数据变化时,计算出需要重复拉取数据的 key
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
// 尝试查找离锚点最近的页面的页面键
// prevKey或nextKey;你需要处理可空性:
// prevKey==null->anchorPage是第一页。
// nextKey==null->anchorPage是最后一页。
// prevKey和nextKey都为空->anchorPage是
// 初始页面,因此返回null。
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
Pager
通过 Pager 我们可以获取到对应的数据。构造 Pager 需要三个参数,分别是 PagingConfig、initialKey、pagingSourceFactory。
PagingConfig 顾名思义是用来配置如何拉取分页数据的。其中 pageSize 表示每页的大小;maxSize 表示超过多少项数据会丢弃界面,默认没有限制的
而 initialKey 表示是起始的 key,默认是 null,一般情况下不传就可以了。pagingSourceFactory 是构造 PagingSource 的 Factory。代码示例如下:
class ArticleViewModel: ViewModel() {
val items = Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = { ArticlePagingSource() }
).liveData
}
PagingDataAdapter
获取到数据后,我们就可以使用 PagingDataAdapter 来让 RecyclerView 显示出 UI 了。代码示例如下:
object ArticleComparator : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
class ArticleAdapter(diffCallback: DiffUtil.ItemCallback<Article>) :
PagingDataAdapter<Article, ArticleViewHolder>(diffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ArticleViewHolder {
return ArticleViewHolder(parent)
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
}
class ArticleViewHolder(itemView: View): ViewHolder(itemView) {
fun bind(article: Article?) {
....
}
}
// 监听数据变化
val articleAdapter = ArticleAdapter()
viewModel.items.observe(this) {
articleAdapter.submitData(lifecycle, it)
}
// 在 RecyclerView 上显示
recyclerView.adapter = articleAdapter
这里 DiffUtil.ItemCallback 是用于计算列表中两个非空项之间差异的回调,熟悉 RecyclerView 的同学应该都清楚。其他的部分都是一般的 RecyclerView 的使用,这里就不多介绍了。
除此之外,我们还可以使用 addLoadStateListener 来监听加载的状态。代码示例如下:
adapter.addLoadStateListener { combinedLoadStates ->
// 适用于初始加载的 LoadState
when (it.refresh) {
// 正在加载
is LoadState.Loading -> {}
// 加载错误
is LoadState.Error -> {}
// 未加载,无错误
is LoadState.NotLoading -> {}
}
}
addLoadStateListener 方法会回调 combinedLoadStates 回来。它内部包含了多个 LoadStates,它们的作用分别是:
LoadStates.append:适用于在用户当前位置之后获取的项的LoadStateLoadStates.prepend:适用于在用户当前位置之前获取的项的LoadStateLoadStates.refresh:适用于初始加载的LoadState
每一个 LoadState 都有三个不同类型的字段,分别是
LoadState.Loading:正在加载项LoadState.NotLoading:未加载项LoadState.Error:加载时发生错误
如果你想自定义加载、失败等 header、footer,你可以使用 withLoadStateHeaderAndFooter 来自定义,代码示例如下:
class HeaderAndFooterAdapter(private val retry: () -> Unit) :
LoadStateAdapter<ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
...
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
// 根据不同的 loadState 创建不同的 ViewHolder
...
}
}
val headerAndFooterAdapter = HeaderAndFooterAdapter(retry = { adapter.retry() })
adapter.withLoadStateHeaderAndFooter(header = headerAndFooterAdapter,
footer = headerAndFooterAdapter)