Jetpack - Paging

发布时间:2026/11/7 14:25:25

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
延伸阅读

更多相关文章

2026/11/7 14:24:46

网上在哪考电工证行业前景与薪资待遇

电工证网上在哪考值不值得考?学历不高也能上手! 你是不是也担心自己学历不高,根本报不上名?别急,电工证网上在哪考,其实并不难,而且值不值得考,还得看你怎么看这件事。别让学历拦住了你,电工证这条路,说不定就是你翻身的跳板。…

2026/11/7 14:24:46

特种作业电工证要打印别怕考不过 郑州金水区学生备考指南

特种作业电工证要打印别怕考不过 郑州金水区学生备考指南 怕考不过白交培训费?特种作业电工证要打印,这事儿真得提前规划好。在郑州金水区,像你这样的应届工程类毕业生,报名前了解清楚官方报名入口、备考重点、材料清单,才能避免交了钱却白忙一场。…

2026/11/7 14:24:46

电工证能从事什么工作怎么通过官方报名入口报名

电工证能从事什么工作怎么通过官方报名入口报名 你是不是也遇到过这种情况:手里拿着电工证,却不知道能干啥,更不知道怎么报名、怎么操作,怕被中介坑?别急,这篇文章就来帮你解决这些痛点,从【电工证能从事什么工作】到【官方报名入口】,一步步教你避开…

2026/11/7 14:24:46

辽阳保办电工证怎么复审?上岗必备证书过期别慌,手把手教你解决

辽阳保办电工证怎么复审?上岗必备证书过期别慌,手把手教你解决 证书过期了不知道怎么复审延期,这个问题让不少辽阳的电工从业者感到焦虑。很多人辛辛苦苦考下来的电工证,因为疏忽或不了解流程,结果一不小心就失效了,直接影响工作安排和职业发展。尤其在…

2026/11/7 14:24:46

南靖哪里有培训电工证多少钱 电工证报名避坑指南

南靖哪里有培训电工证多少钱 电工证报名避坑指南 你是不是正为“南靖哪里有培训电工证”发愁?怕被中介坑,又不知道多少钱,关键是连报名流程都摸不清。别急,这篇干货讲得就是你关心的点,看完你就知道怎么选机构、怎么报名、怎么拿证,还能省下不少冤枉钱…

2026/11/7 14:19:46

江苏电工作业查询不上岗必备?低学历也能考!

江苏电工作业查询不上岗必备?低学历也能考! 你是不是总觉得自己学历不高,担心江苏电工作业查询不了名?别急,今天我就从一个干了十几年特种作业培训的老手角度,给你讲清楚江苏电工作业查询的流程、报考条件、考试时间,以及这个证在你那儿挂靠行情多少?…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/6 10:49:01

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/7 0:03:08

郑州报考避坑指南:电工证和钳工证怎么选?实操考试心里没底怕挂科

郑州报考避坑指南:电工证和钳工证怎么选?实操考试心里没底怕挂科 实操考试心里没底怕挂科?你不是一个人。特别是在郑州这样的城市,很多施工企业负责人和一线作业人员在报考电工证和钳工证时都面临着同样的困惑。到底该考哪个证?哪个更适合自己?哪个证书…

2026/11/7 0:03:08

广西电工证办理流程全解析:外省证怎么转?在哪考最靠谱?

广西电工证办理流程全解析:外省证怎么转?在哪考最靠谱? 之前考的证在外省能不能转过来?这个问题我遇到太多了,今天就拿广西电工证办理流程来具体说说,别再走弯路了。广西电工证的报考、复审、继续教育这些环节,如果你不了解,真的会吃大亏,尤其对于从…

2026/11/7 0:03:08

电气电工证都有哪些保姆级教程

电气电工证都有哪些保姆级教程 你是不是正愁不知道去哪报名,怕被中介坑?别急,这篇保姆级教程专为河南初次报考人员准备,帮你搞清楚 电气电工证都有哪些 ,以及如何避开那些坑。 电气电工证都有哪些?别被五花八门的证书搞懵了…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/6 10:49:01

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑