Paging:分代分页数据流 · AndroidX 源码指南
AAndroidX 源码指南
数据层
数据层 · androidx.room3

Paging:分代分页数据流Room3 3.0.0-rc01

PagingSource、PagingConfig 参数、RemoteMediator 网络+数据库,与 Compose 三态处理。

最后更新 2026-08-01

最小链路

val users = Pager(
    config = PagingConfig(pageSize = 30),
    pagingSourceFactory = { repository.usersPagingSource() },
).flow.cachedIn(viewModelScope)

Compose 侧:

val items = viewModel.users.collectAsLazyPagingItems()

LazyColumn {
    items(
        count = items.itemCount,
        key = items.itemKey { it.id },
    ) { index ->
        items[index]?.let { UserRow(it) }
    }
}

PagingConfig 参数表

参数默认值说明
pageSize必填每页条数,也是初始加载的基数
prefetchDistancepageSize距末尾多远处预取下一页
initialLoadSizepageSize * 3首次加载条数(通常是 3 页)
enablePlaceholderstrue未知位置是否占位(影响滚动条与 loading 表现)
maxSize无界分页数据缓存上限(LRU 驱逐)
jumpThreshold无界触发”跳页”(跳过中间页)的距离阈值

maxSize 必须至少是 2 * prefetchDistance + pageSize;设置合理上限可控制内存。

写一个 PagingSource

class UsersPagingSource(
    private val api: UserApi,
) : PagingSource<Int, User>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        return try {
            val page = params.key ?: 1
            val response = api.getUsers(page, params.loadSize)
            LoadResult.Page(
                data = response.items,
                prevKey = if (page > 1) page - 1 else null,
                nextKey = if (response.items.isNotEmpty()) page + 1 else null,
            )
        } catch (e: IOException) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, User>): Int? {
        // 刷新后尽量回到当前视口附近
        return state.anchorPosition?.let { anchor ->
            state.closestPageToPosition(anchor)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
        }
    }
}

getRefreshKey 决定刷新后回到哪个页——不实现会导致刷新后回到第一页。

RemoteMediator:网络 + 数据库单一事实源

只靠内存分页,刷新后历史页要重新请求。要让”数据库是单一事实源、网络是增量补充”,用 RemoteMediator:

class UsersRemoteMediator(
    private val api: UserApi,
    private val db: AppDatabase,
) : RemoteMediator<Int, User>() {

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, User>,
    ): MediatorResult {
        return try {
            val page = when (loadType) {
                LoadType.REFRESH -> 1
                LoadType.APPEND -> {
                    val last = state.lastItemOrNull()
                        ?: return MediatorResult.Success(endOfPaginationReached = true)
                    last.page + 1
                }
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
            }
            val response = api.getUsers(page, state.config.pageSize)

            // 写入数据库(触发 PagingSource.invalidate 自动重查)
            db.withTransaction {
                if (loadType == LoadType.REFRESH) db.userDao().clearAll()
                db.userDao().insertAll(response.items.map { it.toEntity(page) })
            }

            MediatorResult.Success(endOfPaginationReached = response.items.isEmpty())
        } catch (e: IOException) {
            MediatorResult.Error(e)
        }
    }

    override suspend fun initialize(): InitializeAction = LAUNCH_INITIAL_REFRESH
}
val users = Pager(
    config = PagingConfig(pageSize = 30),
    remoteMediator = UsersRemoteMediator(api, db),
    pagingSourceFactory = { db.userDao().pagingSource() },
).flow.cachedIn(viewModelScope)

要点:

  • load(loadType, state)REFRESH/APPEND/PREPEND 分支处理。
  • 写入数据库后要触发 PagingSource.invalidate()(数据库 Flow 自动触发,手动数据源需自己调)。
  • MediatorResult.Success(endOfPaginationReached) 告知是否还有下一页。
  • initialize() 返回 LAUNCH_INITIAL_REFRESH(默认)或 SKIP_INITIAL_REFRESH

LoadStates:三态处理

when (val refresh = items.loadState.refresh) {
    is LoadState.Loading -> LoadingView()
    is LoadState.Error -> ErrorView(refresh.error) { items.retry() }
    is LoadState.NotLoading -> ListContent()
}

// 追加加载(列表尾部)
when (items.loadState.append) {
    is LoadState.Loading -> FooterLoading()
    is LoadState.Error -> FooterError { items.retry() }
    else -> {}
}

refresh 与 append 必须分开处理:整屏刷新错误是”页面挂了”,尾部加载错误是”还能重试”——不是同一种体验。

常见陷阱

  • cachedIn 生命周期:放在 ViewModel 的 viewModelScope;Activity 里乱放会重复生成分页流。
  • 没有 getRefreshKey:刷新后跳回第一页,用户体验差。
  • RemoteMediator 忘了 invalidate:数据库写了但 UI 不更新。
  • enablePlaceholders=trueitems[index] 可能是 null(占位),必须判空。
  • key 类型PagingSource<Key, Value> 的 Key 要稳定;页码用 Int 最简单。

复制即用

// 依赖:androidx.paging:paging-runtime + paging-compose

class UserRepo(private val api: UserApi) {
    fun pagingSource() = UsersPagingSource(api)
}

class UserViewModel(private val repo: UserRepo) : ViewModel() {
    val users = Pager(
        config = PagingConfig(
            pageSize = 30,
            enablePlaceholders = false,
            maxSize = 200,
        ),
        pagingSourceFactory = { repo.pagingSource() },
    ).flow.cachedIn(viewModelScope)
}

要点

  • 每次刷新创建新一代 PagingSource;getRefreshKey 控制刷新落点。
  • PagingConfigprefetchDistance/maxSize/enablePlaceholders 影响预取与内存。
  • RemoteMediator 让数据库成为单一事实源,网络只做增量同步。
  • Compose 用 collectAsLazyPagingItems,三态(refresh/append/prepend)分开处理。

相关页面