Jetpack - Paging

发布时间:2026/9/17 12:24:55
Jetpack - Paging 一、概念相对于传统的下拉刷新上拉加载只需要告诉Paging如何加载数据不用再监听滑动事件操作何时加载下一页。PagingSource用于定义数据的来源和加载方式。开发者需要实现 PagingSource 抽象类并在其中指定如何从数据源中加载特定页的数据。PagingSource 通常用于与网络 API 或本地数据库进行交互获取分页数据。Pager配置分页参数并生成数据流。通过 Pager开发者可以将 PagingSource 与其他配置参数如分页大小、预取距离等结合起来创建用于加载和展示分页数据的 PagingData 数据流。Pager 提供了多个静态方法用于创建不同类型的 PagingData 数据流。Pager对象负责从PagingSource加载数据并将数据包装成PagingData对象发射到Flow/LiveData中。PagingData表示分页数据的类。它是一个泛型类可以容纳各种类型的分页数据。PagingDataAdapter是 RecyclerView.Adapter 的子类专门用于展示 PagingData 数据流中的分页数据。PagingDataAdapter 提供了内置的数据差异计算和局部刷新机制使得在 RecyclerView 中展示分页数据变得更加高效和简单。它还提供了加载状态和错误处理等功能。LoadStateAdapter是一个用于展示加载状态的 RecyclerView.Adapter 的子类。它可以与 PagingDataAdapter 结合使用用于展示分页数据的加载状态如加载中、加载错误等。LoadStateAdapter 可以显示自定义的加载状态布局并根据加载状态的变化自动更新 UI。RemoteMediator是用于处理远程数据加载和数据库插入的接口。当 PagingSource 加载远程数据时RemoteMediator 可以在加载完成后将数据插入本地数据库并提供信息以支持分页和数据持久化。RemoteMediator 是实现离线缓存和数据持久化的关键组件之一。1.1 数据源 PagingSourcePagingSourceKey, Value第一个参数 Key表示页的标识符。在 Paging 3 中每个页都需要一个唯一的标识符来识别它。通常情况下这个标识符可以是整数类型表示页的编号或索引。在加载数据时我们可以根据这个标识符来确定要加载的是哪一页的数据。第二个参数 Value表示加载的数据项的类型。这个类型可以是你自定义的任何数据类型根据你的需求而定。在分页加载过程中每个加载的数据项都属于这个类型。getRefreshKey()请求出错时会调用refresh方法加载 如果当前已经请求了第一页到第四页的数据 可以通过设置在refresh 后会加载第5 - 8页的数据并且前四页的数据都没了。如果getRefreshKey返回nullrefresh后 会重新加载第一到第四页的数据。load()负责加载特定页的数据。编写具体的数据加载逻辑、处理数据加载状态和错误、设置下一页或者前一页的key值。参数 LoadParams 包含了当前请求的加载信息例如 params.key当前请求的页数、params.loadSize请求的加载数量等。返回值 LoadResult 是一个包装类用于封装加载结果它可以是LoadResult.Page表示成功加载了一页数据需要提供data当前页的数据、prevKey前一页、nextKey后一页。LoadResult.Error表示加载数据时遇到了错误需要提供一个Throwable对象。1.2 分页 PagerPager 负责将 PagingSource 与界面进行绑定并提供可供界面使用的数据类型。PagerPager (config: PagingConfig, //加载配置initialKey: Key? null, //可选的初始化数据指定初始页的键值remoteMediator: RemoteMediatorKey, Value?,pagingSourceFactory: () - PagingSourceKey, Value //指定数据源)调用 .flow 或 .liveData 返回需要的可观察容器。容器再调用 .cacheIn() 可将数据缓存在ViewModelScope 中。PagingConfigpageSize指定每页加载多少项数据。prefetchDistance预取下一页数据的距离不能为0否则不会拉取下一页数据默认等于 pageSize。initialLoadSize初始加载多少项数据减少首屏翻页感默认等于 pageSize * 3。二、基本使用2.1 添加依赖查看官方最新版本implementation androidx.paging:paging-runtime:3.1.12.2 DataSource中定义数据源并配置分页下一页页码不要通过 data.size params.loadSize 进行判断API可能每次取到的数量小于 PagingConfig 中 pageSize 设置的数量造成不继续加载。getRefreshKey() 被动失效必须位置就近。离线优先架构里任何写库都可能让 PagingSource 失效、自动触发刷新刷新一般是整体刷新Paging 会废弃当前数据源、整个列表重新加载。但重新加载后从第几页开始显示用户可能处在中间页这就是 getRefreshKey() 所要决定的具体根据业务场景选用不需要就直接返回 null 会回到第一页。class MyDataSource { //每次拉取20条数据 fun dataPager() Pager(PagingConfig(20)) { MyPagingSource() } } /** * 泛型参数一通常是页码Int或游标String * 泛型参数二item的类型(注意不是表)即返回的数据类型。 */ private class MyPagingSource() : PagingSourceInt, Article() { private val startPage 0 //API的默认开始页码 //提供对应页面的数据分页逻辑 override suspend fun load(params: LoadParamsInt): LoadResultInt, Article { return try { //拿到当前页码为null就设置为默认开始页码 val currentPage params.key ?: startPage //当前页获取数据逻辑数据源在内部自己管理而不是在Adapter中了 val data getData(currentPage) //上一页页码当前页是第一页上一页就返回null val prevKey if (currentPage startPage) currentPage - 1 else null //下一页页码返回数据不为空就继续加载 val nextKey if (data.isNotEmpty()) currentPage 1 else null //返回结果 LoadResult.Page(data, prevKey, nextKey) }catch (e: CancellationException) { // 对协程取消放行 throw e } catch (e: Exception) { //返回错误 LoadResult.Error(e) } } //一般直接返回null就行 //当分页数据因刷新、失效或重新加载时通过计算 refreshKey 确定从哪个位置重新加载数据避免用户界面出现跳跃或重复内容。 override fun getRefreshKey(state: PagingStateInt, Article): Int? { //anchorPosition表示用户当前屏幕可见项的近似位置如果不存在锚点位置例如首次加载直接返回null触发初始加载。 //closestItemToPosition()查找最接近锚点位置的实际数据项Article 对象若找不到对应项例如数据为空返回 null 触发默认加载逻辑。 //尝试从 prevKey 或 nextKey 中获取最接近 anchorPosition 的页面键值此处需进行空值处理。 //prevKey 1 6 1 7或 nextKey - 1 8 - 1 7两个式子算出同一个数可见页码 //若 prevKey null → 则 anchorPage 为第一页 //若 nextKey null → 则 anchorPage 为最后一页 //若 prevKey 与 nextKey 均为 null → 则 anchorPage 为初始页面此时应返回 null return state.anchorPosition?.let { anchorPosition - val anchorPage state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } //具体获取数据的方法。这里能更细分的对异常处理否则在load()中合并返回后在UI中难区分。 //但处理后还是要抛出异常不然load()不会返回异常影响UI中对Paging状态判断 private suspend fun getData(currentPage: Int): ListArticle { var data: ListArticle emptyList() runCatching { ApiService.getData(currentPage.toString()) //获取数据 }.onSuccess { data it }.onFailure { throw it } return data } }2.3 Repository将分页转为需要的数据类型提供出去可选一般直接在 DataSource 中转换并提供。可转换为 Flow 或 LivaData。class MyRepository( private val dataSource: MyDataSource ) { fun dataFlow() dataSource.dataPager().flow }2.4 ViewModel 获取数据流并缓存Pager会调用PagingSource的load( )方法获取数据每个PagingData代表一页的数据。(冷流无法后期往里发送数据cacheIn() 缓存后每次收集都会返回包含过往数据的新 PagingData 实例)class MyViewModel( private val repository: MyRepository ) : ViewModel() { //返回值FlowPagingDataArticle //将数据流在ViewModel中缓存横竖屏切换后Paging能从缓存中读取数据而不是重新联网请求 val dataFlow repository.dataFlow().cachedIn(viewModelScope) }2.5 PagingDataAdapter//比较器DIFFCALLBACK通过伴生对象DiffUtil实现 //不需要传数据源进来不需要实现条目数量这些在PagingSource中进行 class MyPagingAdapter : PagingDataAdapterArticle, RecyclerView.ViewHolder(DIFFCALLBACK) { lateinit var binding: Drawer3ItemBinding companion object { private val DIFFCALLBACK object : DiffUtil.ItemCallbackArticle() { override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean { return oldItem.id newItem.id } override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean { return oldItem newItem } } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val myViewHolder holder as MyViewHolder val article getItem(position) //拿到bean if (article ! null) { val title Html.fromHtml(article.title).toString() myViewHolder.tvTitle.text title myViewHolder.tvTime.text article.niceDate val author article.author val shareUser article.shareUser val superChapterName article.superChapterName if (author.isEmpty()) { myViewHolder.tvUserName.text String.format(%s · %s, superChapterName, shareUser) } else { myViewHolder.tvUserName.text String.format(%s · %s, superChapterName, author) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { binding ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return MyViewHolder() } inner class MyViewHolder : RecyclerView.ViewHolder(binding.root) { var tvTitle binding.tvTitle var tvUserName binding.tvUsername var tvTime binding.tvTime } }2.6 结合数据库缓存class PostRemoteMediator( private val db: AppDatabase, private val api: ApiService ) : RemoteMediatorInt, Article() { override suspend fun load(loadType: LoadType, state: PagingStateInt, Article): MediatorResult { return try { val page when (loadType) { LoadType.REFRESH - 1 LoadType.PREPEND - return MediatorResult.Success(endOfPaginationReached true) LoadType.APPEND - db.postDao().getNextPageKey() ?: return MediatorResult.Success(true) } val posts api.getPosts(page, state.config.pageSize) db.runInTransaction { if (loadType LoadType.REFRESH) db.postDao().clearAll() db.postDao().insertAll(posts) db.postDao().updateNextPageKey(page 1) } MediatorResult.Success(endOfPaginationReached posts.isEmpty()) } catch (e: Exception) { MediatorResult.Error(e) } } }2.7 UIprivate fun initView() { val progressBar binding.progressBar val recyclerView binding.recyclerView recyclerView.layoutManager LinearLayoutManager(this) adapter MyPagingAdapter() recyclerView.adapter adapter //设置加载状态监听也可以写成adapter.loadStateFlow.collectLatest{}对it进行分类 adapter.addLoadStateListener { //it.refresh在初始化刷新的使用也就是说第二页第三页...是监听不到的 //it.append在加载更多的时候使用 //it.prepend在当前列表头部添加数据的时候使用 when (it.refresh) { //当没有加载动作并且没有错误的时候 is LoadState.NotLoading - { progressBar.visibility View.INVISIBLE recyclerView.visibility View.VISIBLE } //正在加载 is LoadState.Loading - { progressBar.visibility View.VISIBLE recyclerView.visibility View.INVISIBLE } //加载错误这里的错误是PagingSource里捕获的 is LoadState.Error - { val state it.refresh as LoadState.Error progressBar.visibility View.INVISIBLE showToast(adapter报错: ${state.error.message}) adapter.retry() //调用重试 } } } } private fun byFlow() { lifecycleScope.launch { viewModel.getData().collect { pagingData - adapter.submitData(pagingData) //提交数据后Paging就开始工作了 } } } // private fun byLiveData() { // viewModel.dataLiveData.observe(this) { pagingData - // lifecycleScope.launch { // adapter.submitData(pagingData) // } // } // }三、添加Footer、Header//通过构造传入重试的方法在UI中直接传入PagingAdapter.retry() class Drawer3FooterAdapter(val retry: () - Unit) : LoadStateAdapterRecyclerView.ViewHolder() { private lateinit var binding: Drawer3FooterBinding override fun onBindViewHolder(holder: RecyclerView.ViewHolder, loadState: LoadState) { val footViewHolder holder as FootViewHolder //根据LoadState状态来控制脚部界面显示加载/重试 footViewHolder.progressBar.isVisible loadState is LoadState.Loading footViewHolder.retryButton.isVisible loadState is LoadState.Error footViewHolder.retryButton.setOnClickListener { retry } } override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): RecyclerView.ViewHolder { binding Drawer3FooterBinding.inflate(LayoutInflater.from(parent.context), parent, false) return FootViewHolder() } inner class FootViewHolder : RecyclerView.ViewHolder(binding.root) { val progressBar binding.progressBar val retryButton binding.retryButton } }adapter MyPagingAdapter() val concatAdapter adapter.withLoadStateFooter(Drawer3FooterAdapter { adapter.retry() }) //添加脚部 recyclerView.adapter concatAdapter

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询