跳到主要内容

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 的区别?

答案

特性PagingSourceRemoteMediator
数据来源单一来源(网络或数据库)网络 + 数据库双层
离线支持✅(数据库缓存)
典型用途纯网络分页列表需离线可用

Q2: cachedIn(viewModelScope) 的作用?

答案

将 PagingData 缓存在 ViewModel 中,配置变更(旋转)后恢复数据而无需重新加载。如果不调用 cachedIn,每次订阅都会重新从首页加载。

相关链接