数据层
数据层 · 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 | 必填 | 每页条数,也是初始加载的基数 |
prefetchDistance | pageSize | 距末尾多远处预取下一页 |
initialLoadSize | pageSize * 3 | 首次加载条数(通常是 3 页) |
enablePlaceholders | true | 未知位置是否占位(影响滚动条与 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=true时:items[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控制刷新落点。 PagingConfig的prefetchDistance/maxSize/enablePlaceholders影响预取与内存。- RemoteMediator 让数据库成为单一事实源,网络只做增量同步。
- Compose 用
collectAsLazyPagingItems,三态(refresh/append/prepend)分开处理。