Paging 分页加载
问题
Paging 3 的核心架构是什么?如何实现网络 + 数据库的分页?
答案
架构概览
基本使用(纯网络)
// 1. 定义 PagingSource
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val page = params.key ?: 1
return try {
val response = api.getArticles(page, params.loadSize)
LoadResult.Page(
data = response.data,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.data.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
return state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
}
}
}
// 2. 在 ViewModel 中创建 Pager
class ArticleViewModel(private val api: ArticleApi) : ViewModel() {
val articles: Flow<PagingData<Article>> = Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 5, // 距离底部 5 个 item 时预加载
enablePlaceholders = false
),
pagingSourceFactory = { ArticlePagingSource(api) }
).flow.cachedIn(viewModelScope)
}
// 3. 在 UI 中使用 PagingDataAdapter
class ArticleAdapter : PagingDataAdapter<Article, ArticleViewHolder>(DIFF_CALLBACK) {
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
}
// 提交数据
lifecycleScope.launch {
viewModel.articles.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
RemoteMediator(网络 + 数据库)
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val api: ArticleApi,
private val db: AppDatabase
) : RemoteMediator<Int, Article>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Article>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> getNextPage(state)
}
val response = api.getArticles(page, state.config.pageSize)
db.withTransaction {
if (loadType == LoadType.REFRESH) db.articleDao().clearAll()
db.articleDao().insertAll(response.data)
}
return MediatorResult.Success(endOfPaginationReached = response.data.isEmpty())
}
}
加载状态处理
// 在 Adapter 底部添加加载指示器
val footer = ArticleLoadStateAdapter { adapter.retry() }
recyclerView.adapter = adapter.withLoadStateFooter(footer)
// 监听加载状态
adapter.addLoadStateListener { loadStates ->
binding.progressBar.isVisible = loadStates.refresh is LoadState.Loading
binding.errorView.isVisible = loadStates.refresh is LoadState.Error
}
常见面试问题
Q1: PagingSource 和 RemoteMediator 的区别?
答案:
| 特性 | PagingSource | RemoteMediator |
|---|---|---|
| 数据来源 | 单一来源(网络或数据库) | 网络 + 数据库双层 |
| 离线支持 | ❌ | ✅(数据库缓存) |
| 典型用途 | 纯网络分页 | 列表需离线可用 |
Q2: cachedIn(viewModelScope) 的作用?
答案:
将 PagingData 缓存在 ViewModel 中,配置变更(旋转)后恢复数据而无需重新加载。如果不调用 cachedIn,每次订阅都会重新从首页加载。