一文理解Jetpack——Paging3

2,133 阅读5分钟

屏幕截图 2024-05-02 102507.png

分页加载的需求大家肯定都碰到过,一般我们会使用 RecycleView 添加刷新的 header 和加载更多的 footer 来处理滑动加载事件。而 Paging 组件就是 Google 推荐的,用来解决分页加载问题的一种解决方案。使用Paging 组件的优势如下:

  • 大量数据集列表处理。默认情况下当用户一直滑动列表时,当前界面的数据集列表会越来越大,可能会影响性能。而 Paging 组件对此做了处理,可以确保内存中只保存了合理的数据列表。
  • 内置的请求去重功能
  • 可配置的 RecyclerView 适配器,会在用户滚动到已加载数据的末尾时自动请求数据。
  • 对 Kotlin 协程和数据流以及LiveDataRxJava 的支持。
  • 内置对错误处理功能的支持,包括刷新和重试功能。

如果你想使用 Paging,需要导入如下依赖:

dependencies {
  implementation "androidx.paging:paging-runtime:3.3.0"
}

Paging 组件的组成

paging3-library-architecture.svg

如上图所示,Paging 组件由3部分组成,分别是数据源(PagingSource或者RemoteMediator)PagerPagingDataAdapter,图中箭头所指的方向是指数据的流动方向。

  • 数据源:顾名思义,数据源就是提供数据的类。它有两种分别是 PagingSourceRemoteMediator。其中 PagingSource 用于单一数据源的加载场景;而 RemoteMediator 用于多个数据源的加载场景。
  • Pager:处理数据源,它会根据 PagingConfig 每一页的配置来返回我们需要的分页数据。上图中,使用的是 Flow 返回数据。但实际上 Pager 也可以使用 liveData 来处理数据
  • PagingDataAdapter:继承 RecyclerViewAdapter,是用来展示分页数据的处理器

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 这个抽象类需要实现两个方法 loadgetRefreshKey 。我们先看 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 加载下一页时的key
  • LoadResult.Error 如果发生错误时返回
  • LoadResult.Invalid 如果 PagingSource 因无法再保证其结果的完整性而应失效时返回

当我们调用了 PagingAdapter 类的 refresh 方法或者 PagingSource 的 invalidate 方法时,表示数据有变化,这时就会调用 PagingSourcegetRefreshKey 方法来获取新的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 需要三个参数,分别是 PagingConfiginitialKeypagingSourceFactory

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:适用于在用户当前位置之后获取的项的 LoadState
  • LoadStates.prepend:适用于在用户当前位置之前获取的项的 LoadState
  • LoadStates.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)

参考