Paging 3 与 RemoteMediator:列表分页工程
文章目录第 1 章 全量加载的代价为什么需要 Paging 3原理补充踩坑动手练第 2 章 PagingSource单数据源分页踩坑动手练第 3 章 Pager、Flow 与 ViewModel原理补充踩坑动手练第 4 章 UI 集成RecyclerView 与 Compose踩坑动手练第 5 章 RemoteMediator网络 Room 离线缓存原理补充踩坑动手练第 6 章 工程策略占位、头部与测试动手练推荐插图选图理由TYPE / MODEDiagram DSL v4Illustration Prompt面试速查 · 追问链追问链 #1Paging 3 核心组件 追问链 #2RemoteMediator 解决什么 追问链 #3getRefreshKey 干什么 ⭐追问链 #4LoadState 几种怎么 UX ⭐追问链 #5筛选变化怎么重建分页 完整链路一句通相关推荐第 1 章 全量加载的代价为什么需要 Paging 3订单列表、信息流、评论页如果一次性getOrders()拉全量内存万级项List常驻 ViewModel低端机 OOM。首屏用户只看前 20 条却等全量 JSON 解析。滚动体验notifyDataSetChanged全刷Diff 成本高。Paging 3 以分页窗口向 UI 提供数据PagingData随滚动增量加载内置LoadState刷新/追加/前置。与Kotlin Flow、Room、Compose LazyColumn原生集成。新列表默认 Paging 3Paging 2 / 手写分页仅遗留对照。// 反模式全量suspendfunloadAll():ListOrderapi.fetchAllOrders()原理补充Pager协调PagingSource数据源与PagingConfig页大小、预取距离。PagingDataAdapter/LazyPagingItems通过DiffUtil提交分页快照。踩坑把PagingData转成List存 ViewModel → 失去分页意义。pageSize过大等于伪全量。忽略LoadState→ 用户看不到重试入口。动手练APaging 3 相对「ViewModel 持全量 List」解决哪两个问题D找所在工程列表页是否一次拉全量 API。第 2 章 PagingSource单数据源分页纯网络或纯本地均可实现PagingSourceKey, ValueclassOrderPagingSource(privatevalapi:OrderApi,):PagingSourceInt,Order(){overridesuspendfunload(params:LoadParamsInt):LoadResultInt,Order{valpageparams.key?:START_PAGEreturntry{valresponseapi.getOrders(pagepage,sizeparams.loadSize)LoadResult.Page(dataresponse.items,prevKeyif(pageSTART_PAGE)nullelsepage-1,nextKeyif(response.items.isEmpty())nullelsepage1,)}catch(e:Exception){LoadResult.Error(e)}}overridefungetRefreshKey(state:PagingStateInt,Order):Int?state.anchorPosition?.let{pos-state.closestPageToPosition(pos)?.prevKey?.plus(1)?:state.closestPageToPosition(pos)?.nextKey?.minus(1)}companionobject{constvalSTART_PAGE0}}LoadResult.Error会反映到 UILoadStatenextKey null表示没有更多。踩坑prevKey/nextKey算错 → 重复页或死循环。loadSize忽略params.loadSize→ 预取失效。未实现getRefreshKey→ 下拉刷新后滚动位置恢复异常。动手练B起点修复nextKey始终page 1导致空页仍请求的问题应用items.isEmpty()判断。第 3 章 Pager、Flow 与 ViewModel用Pager构建FlowPagingDataOrderclassOrderListViewModel(privatevalapi:OrderApi,):ViewModel(){valorders:FlowPagingDataOrderPager(configPagingConfig(pageSize20,enablePlaceholdersfalse,prefetchDistance10,),pagingSourceFactory{OrderPagingSource(api)},).flow.cachedIn(viewModelScope)}cachedIn(viewModelScope)保证旋转后同一分页流存活且离开屏幕后按策略取消上游。筛选变化时用flatMapLatest重建Pagerprivateval_filterMutableStateFlowOrderStatus?(null)valorders:FlowPagingDataOrder_filter.flatMapLatest{status-Pager(configPagingConfig(pageSize20),pagingSourceFactory{OrderPagingSource(api,status)},).flow}.cachedIn(viewModelScope)原理补充PagingData是不可变快照流收集者在 Main 提交给 Adapter。cachedIn使用 ViewModel 作用域共享MulticastedPagingData。踩坑忘记cachedIn→ 每次collect重新从第一页加载。flatMapLatest未用 → 切换筛选旧请求覆盖新结果。pageSize与后端约定不一致 → 重复或缺项。动手练C为status筛选写flatMapLatestPager骨架≤12 行。第 4 章 UI 集成RecyclerView 与 ComposeRecyclerView PagingDataAdapterclassOrderAdapter:PagingDataAdapterOrder,OrderViewHolder(DIFF){companionobject{privatevalDIFFobject:DiffUtil.ItemCallbackOrder(){overridefunareItemsTheSame(a:Order,b:Order)a.idb.idoverridefunareContentsTheSame(a:Order,b:Order)ab}}// onCreateViewHolder / onBindViewHolder}// FragmentlifecycleScope.launch{repeatOnLifecycle(Lifecycle.State.STARTED){viewModel.orders.collect{pagingData-adapter.submitData(pagingData)}}}加载态与重试adapter.addLoadStateListener{state-binding.progress.isVisiblestate.refreshisLoadState.Loadingvalerrorstate.refreshas?LoadState.Error binding.retry.isVisibleerror!nullbinding.retry.setOnClickListener{adapter.retry()}}ComposevalordersviewModel.orders.collectAsLazyPagingItems()LazyColumn{items(orders.itemCount){index-orders[index]?.let{OrderRow(it)}}when(orders.loadState.refresh){isLoadState.Loading-item{CircularProgressIndicator()}isLoadState.Error-item{RetryButton{orders.retry()}}else-Unit}}踩坑DiffUtil错误 → 闪烁或更新失效。submitData不在主线程 → 崩溃collect在 Main。忽略append错误 → 底部加载失败无提示。动手练ALoadState.refresh和LoadState.append分别对应什么用户动作第 5 章 RemoteMediator网络 Room 离线缓存生产常见Room 为真相源网络分页写入本地UI 从 RoomPagingSource读。RemoteMediator协调REFRESH/APPEND/PREPENDOptIn(ExperimentalPagingApi::class)classOrderRemoteMediator(privatevalapi:OrderApi,privatevaldb:AppDatabase,):RemoteMediatorInt,OrderEntity(){privatevaldaodb.orderDao()overridesuspendfuninitialize():InitializeActionInitializeAction.LAUNCH_INITIAL_REFRESHoverridesuspendfunload(loadType:LoadType,state:PagingStateInt,OrderEntity,):MediatorResult{valpagewhen(loadType){LoadType.REFRESH-0LoadType.PREPEND-returnMediatorResult.Success(endOfPaginationReachedtrue)LoadType.APPEND-{vallastdao.getLastRemoteKey()?:returnMediatorResult.Success(endOfPaginationReachedtrue,)last.nextPage}}returntry{valresponseapi.getOrders(page,state.config.pageSize)db.withTransaction{if(loadTypeLoadType.REFRESH){dao.clearRemoteKeys()dao.clearAll()}dao.insertAll(response.items.map{it.toEntity()})dao.insertRemoteKeys(response.items.map{keyEntity(page,it)})}MediatorResult.Success(endOfPaginationReachedresponse.items.isEmpty())}catch(e:Exception){MediatorResult.Error(e)}}}Pager配置Pager(configPagingConfig(pageSize20),remoteMediatorOrderRemoteMediator(api,db),pagingSourceFactory{dao.pagingSource()},// Room 提供).flow离线时 Room 仍有数据网络恢复后refresh()同步。原理补充RemoteMediator在网络层加载RoomPagingSource读本地。RemoteKeys表记录各页 next/prev供 APPEND 算页码。withTransaction保证键与数据一致。踩坑REFRESH 未清表 → 脏数据残留。无RemoteKeys→ APPEND 不知下一页。网络错误未映射MediatorResult.Error→ 无法触发重试。动手练D画 REFRESH 时 DAO 调用顺序clear → insert三步。第 6 章 工程策略占位、头部与测试分隔线 / 头部用separatedBy或多种ViewTypeConcatAdapter/ Composeitem分支。测试PagingSource单测 mock API 返回页链TestPagerasPagingSourceData收集项。RemoteMediator用内存 Room 或Room.inMemoryDatabaseBuilder。场景选型纯网络列表PagingSourceonly离线优先 feedRemoteMediator Room本地纯 DB 大表RoomPagingSourceonly搜索实时变flatMapLatest 新 Pager反模式在load里GlobalScope把 UI 分页和业务分页混用两个不同 pageSize忽略endOfPaginationReached仍请求。动手练D 验收飞行模式进列表看缓存开网swipeRefresh滚到底触发 append断网 append 显示错误并可retry()。B起点为OrderRemoteMediator的REFRESH分支补clearAll与insertAll事务≤15 行。推荐插图选图理由RemoteMediator 写 Room ← 网络Room PagingSource → Pager → UI双路径在数据库汇合体现离线优先分页。TYPE / MODEHYBRID_PIPELINEHYBRID_MODEDiagram DSL v4VERSION: 4.0 TYPE: HYBRID_PIPELINE MODE: HYBRID_MODE TITLE: Paging 3 与 RemoteMediator NODES: - UiList - ViewModel - Pager - RoomPagingSource - RemoteMediator - NetworkApi - RoomDb EDGES: - UiList - ViewModel : PRIMARY - ViewModel - Pager : PRIMARY - Pager - RoomPagingSource : PRIMARY - RoomPagingSource - RoomDb : PRIMARY - Pager - RemoteMediator : CALL - RemoteMediator - NetworkApi : PRIMARY - RemoteMediator - RoomDb : PRIMARY - RoomDb - RoomPagingSource : CALLIllustration Prompt Android Architecture Illustration Prompt TITLE: Paging 3 与 RemoteMediator SUBTITLE: 网络分页写入 RoomUI 只读本地 PagingSource NODES: 列表 UI · ViewModel · Pager · Room PagingSource · RemoteMediator · API · Room PRIMARY: UI→Pager→Room 读RemoteMediator→API→写 Room END 面试速查 · 追问链追问链 #1Paging 3 核心组件 参考回答PagingSource加载单页Pager配PagingConfig生成FlowPagingDataUI 用PagingDataAdapter或LazyPagingItems。LoadState管刷新/追加错误。cachedIn(viewModelScope)跨旋转保流。追问 1PagingData能转 List 长期持有吗答不应失去窗口化内存优势应用 Flow 直接交 UI。追问 2prefetchDistance作用答距列表底部还有多少项时预取下一页平衡流畅度与请求次数。追问链 #2RemoteMediator 解决什么 参考回答网络分页 本地 DB 缓存。Mediator 按LoadType拉远程页写 RoomUI 的PagingSource只读 DB。离线可看缓存刷新清表重拉。需RemoteKeys追踪页码。追问 1和双PagingSource叠加以区别答Mediator 是官方离线优先模式双源易状态不一致。追问 2InitializeAction何时SKIP答本地已有数据且不必启动即刷新时减少无效首请求按产品定。追问链 #3getRefreshKey 干什么 ⭐参考回答下拉刷新或数据源失效后根据当前PagingState锚点位置算从哪页重启尽量恢复滚动附近数据。实现不好会导致刷新回第一页或跳页。追问 1纯网络源如何实现答常用closestPageToPosition取prevKey1或nextKey-1。追问链 #4LoadState 几种怎么 UX ⭐参考回答refresh全量刷新prepend/append前后追加。各分 Loading/Error/NotLoading。首屏看refresh底部看append。Error 提供retry()。勿只盯 refresh 忽略 append 失败。追问 1CombinedLoadStates在 Compose 怎么用答lazyPagingItems.loadState解构 refresh/append 渲染 footer。追问链 #5筛选变化怎么重建分页 参考回答筛选条件进StateFlowflatMapLatest里新建Pager/PagingSource。旧流取消避免竞态。记得cachedIn包在外层或内层按是否共享决定。追问 1同一 ViewModel 多列表答多个FlowPagingData或flatMapLatest切换勿共用一个 Adapter 不换源。完整链路一句通PagingSource 算页键→Pager PagingConfig→cachedIn进 ViewModel→Adapter/LazyPagingItems LoadState→离线用 RemoteMediator 写 Room→筛选flatMapLatest重建流。相关推荐Kotlin 语法与空安全Android 开发第一课Kotlin 作用域函数let/apply 工程选型