Paging 是 Jetpack 中专门解决列表分页加载的库,它按需加载数据,避免一次性查询大量数据导致的内存压力与 UI 卡顿。Paging 3 以 Kotlin Coroutines + Flow 为核心,提供了简洁的响应式分页 API。
一、核心组件
PagingSource:定义数据源以及如何从其中加载数据。每个 PagingSource 需实现 load() 方法,接收 LoadParams,返回 LoadResult。
class ArticlePagingSource( private val api: ApiService ) : PagingSource<Int, Article>() {
override fun getRefreshKey(state: PagingState<Int, Article>): Int? { return state.anchorPosition?.let { anchor -> state.closestPageToPosition(anchor)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchor)?.nextKey?.minus(1) } }
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> { return try { val page = params.key ?: 1 val response = api.getArticles(page, params.loadSize) LoadResult.Page( data = response.items, prevKey = if (page > 1) page - 1 else null, nextKey = if (response.hasNext) page + 1 else null ) } catch (e: Exception) { LoadResult.Error(e) } } }
|
PagingData:分页数据的容器,通过 Pager 创建,内部以 Flow 形式暴露给 UI 层。
PagingDataAdapter:继承自 RecyclerView.Adapter,配合 DiffUtil.ItemCallback 高效对比数据差异,驱动列表增量更新。
当需要”先展示本地缓存,再加载网络数据并回写本地”的双重数据源模式时,使用 RemoteMediator:
@OptIn(ExperimentalPagingApi::class) class ArticleRemoteMediator( private val db: AppDatabase, private val api: ApiService ) : 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 -> { val lastItem = state.lastItemOrNull() ?: return MediatorResult.Success(true) lastItem.page + 1 } } return try { val response = api.getArticles(page, state.config.pageSize) db.withTransaction { if (loadType == LoadType.REFRESH) { db.articleDao().clearAll() } db.articleDao().insertAll(response.items) } MediatorResult.Success(endOfPaginationReached = !response.hasNext) } catch (e: Exception) { MediatorResult.Error(e) } } }
|
Paging 与 Room 自动集成 —— 只需返回 PagingSource 或使用 @Query 直接返回 PagingSource,Room 会在数据变更时自动触发页面刷新。
三、UI 层集成
@AndroidEntryPoint class ArticleListFragment : Fragment() { private val viewModel: ArticleViewModel by viewModels() private val adapter = ArticlePagingAdapter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) lifecycleScope.launch { viewModel.pagingDataFlow.collectLatest { pagingData -> adapter.submitData(pagingData) } } } }
|
面试常考问题
Q1:PagingSource 的 getRefreshKey() 作用?
当 PagingData 失效(如数据库更新)需要刷新时,getRefreshKey() 返回用于重新加载的 key,确保刷新后列表位置不错乱。通常取当前可见 item 所在页的前一页或后一页 key。
Q2:Paging 2 vs Paging 3 主要区别?
Paging 3 全面拥抱 Kotlin Coroutines/Flow,去除了 PagedList/DataSource 等旧 API。加载状态通过 loadStateFlow 统一暴露,支持 retry 操作。Paging 2 基于 RxJava/LiveData,API 较为分散。
Q3:RemoteMediator 中三种 LoadType 分别何时触发?
REFRESH 在 PagingData 初始化或手动调用 refresh() 时触发;PREPEND 在列表顶部上拉加载更早数据时触发;APPEND 在列表底部滚动触发加载更多时触发。源码定义在 androidx.paging.LoadType。