文章目录
- 第 1 章 全量加载的代价
- 第 2 章 PagingSource
- 第 3 章 Pager 与 ViewModel
- 第 4 章 UI:Compose 与 RecyclerView
- 第 5 章 RemoteMediator
- 第 6 章 工程策略
-
-
- 踩坑
-
- 第 7 章 治理清单
- 面试速查 · 追问链
-
-
- 追问链 #1:PagingSource vs RemoteMediator? 🔥
- 追问链 #2:refresh 键? 🔥
- 追问链 #3:与 Flow 列表区别? ⭐
- 追问链 #4:RxJava Paging2? 💡
-
- 完整链路一句通
- 相关推荐
第 1 章 全量加载的代价
首屏等全量 JSON、内存暴涨、旋转丢失页码——Paging 3 用增量加载 + 占位解决。
PagingSource(单页) → Pager → Flow<PagingData<T>> → UI collect
RemoteMediator(可选)→ Room 作缓存
第 2 章 PagingSource
class ArticlePagingSource(
private val api: ArticleApi,
) : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val page = params.key ?: 0
return try {
val items = api.getArticles(page, params.loadSize).map { it.toDomain() }
LoadResult.Page(
data = items,
prevKey = if (page == 0) null else page – 1,
nextKey = if (items.isEmpty()) null else page + 1,
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? =
state.anchorPosition?.let { pos ->
state.closestPageToPosition(pos)?.prevKey?.plus(1)
?: state.closestPageToPosition(pos)?.nextKey?.minus(1)
}
}
第 3 章 Pager 与 ViewModel
class ArticleViewModel(api: ArticleApi) : ViewModel() {
val articles: Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { ArticlePagingSource(api) },
).flow.cachedIn(viewModelScope)
}
cachedIn(viewModelScope) 旋转后保留已加载页。
第 4 章 UI:Compose 与 RecyclerView
Compose:
val articles = viewModel.articles.collectAsLazyPagingItems()
LazyColumn {
items(count = articles.itemCount) { index ->
articles[index]?.let { ArticleRow(it) }
}
}
RecyclerView:PagingDataAdapter + submitData(lifecycle, pagingData)。
第 5 章 RemoteMediator
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val api: ArticleApi,
private val db: ArticleDao,
) : RemoteMediator<Int, ArticleEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ArticleEntity>,
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> 0
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> state.lastItemOrNull()?.page?.plus(1) ?: 0
}
return try {
val remote = api.getArticles(page, state.config.pageSize)
db.withTransaction {
if (loadType == LoadType.REFRESH) db.clearAll()
db.insertAll(remote.map { it.toEntity(page) })
}
MediatorResult.Success(endOfPaginationReached = remote.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
Pager 的 remoteMediator + Room PagingSource 读本地。
第 6 章 工程策略
- 筛选条件变化:flatMapLatest 重建 Pager。
- 头部/分隔:用 insertHeaderItem 或 UI 层组合。
- 测试:PagingSource 单测 load;Mediator 用 fake API + 内存 DB。
踩坑
- 忘记 cachedIn → 旋转重复请求。
- enablePlaceholders = true 但无 placeholder 实现 → 闪烁。
第 7 章 治理清单
| 加载 | 无手写页码全局变量 |
| 缓存 | 离线需求有 RemoteMediator + Room |
| 生命周期 | cachedIn(viewModelScope) |
| 错误 | LoadResult.Error / MediatorResult.Error 映射 UI |
| 刷新 | 下拉刷新走 PagingDataAdapter.refresh() 或 LazyPagingItems.refresh() |
面试速查 · 追问链
追问链 #1:PagingSource vs RemoteMediator? 🔥
参考回答:Source 管单数据源分页;Mediator 协调网络写库+本地 Source 读库,做离线。
追问链 #2:refresh 键? 🔥
参考回答:getRefreshKey 根据 PagingState 锚点算恢复页,避免刷新后跳顶错乱。
追问链 #3:与 Flow 列表区别? ⭐
参考回答:全量 Flow 适合小列表/库观察;大列表分页用 Paging 省内存与网络。
追问链 #4:RxJava Paging2? 💡
参考回答:遗留;新工程 Paging 3 + 协程 Flow。
完整链路一句通
PagingSource/Mediator 产页 → Pager.flow.cachedIn → ViewModel 暴露 Flow → LazyColumn/Adapter 增量渲染。

相关推荐
-
Kotlin 作用域函数:let/apply 怎么选
-
Kotlin 语法与空安全:从零的第一课



