Room结合PagingSource无法从指定分页位置初始加载数据
问题原因
- Room自动生成的
PagingSource<Int, User>属于位置型PagingSource,其Int类型key的语义是数据在全量查询结果中的位置偏移量,不是页码。你配置pageSize为20时,第6页对应的起始偏移量是5*20=100,传入initialKey=5属于非法偏移值,Paging会做兜底处理,直接回退到偏移量0(第一页)加载。 - Room 2.5.0以下版本自动生成的PagingSource存在兼容问题,不会正确读取Pager传入的initialKey参数,首次加载始终固定从偏移量0开始取数。
- 使用位置型PagingSource时如果关闭了
enablePlaceholders配置,Paging无法计算位置对应关系,也会忽略传入的initialKey,强制从第一页加载。
实现方案
根据业务需求二选一即可:
方案1:使用Room自动生成的PagingSource
适合仅需要固定初始加载位置、不需要动态跳转到任意分页的场景,需要做3处修改:
- 升级Room依赖到2.5.0及以上稳定版本,修复initialKey读取的兼容问题
- 修正initialKey取值为对应位置的偏移量,不要传页码
- 开启
enablePlaceholders = true配置,保证位置计算逻辑正常
修改后的Pager代码:
const val PAGE_SIZE = 20 // 要加载第6页,对应页码索引为5(从0开始计数) const val TARGET_INIT_PAGE_INDEX = 5 Pager( config = PagingConfig( pageSize = PAGE_SIZE, prefetchDistance = 5, initialLoadSize = PAGE_SIZE, enablePlaceholders = true ), // 偏移量 = 页码索引 * 单页条数 initialKey = TARGET_INIT_PAGE_INDEX * PAGE_SIZE, pagingSourceFactory = { userDao.usersPagingSource() } ).flow.flowOn(Dispatchers.IO).cachedIn(viewModelScope)
方案2:自定义PagingSource
适合需要灵活跳转到任意分页、不想依赖Room版本兼容、或者需要优化大表分页性能的场景,可控性更高:
- 首先修改UserDao,增加明确的分页查询和总条数查询方法:
@Dao interface UserDao { @Query("SELECT * FROM users ORDER BY id LIMIT :limit OFFSET :offset") suspend fun queryUsersByPage(limit: Int, offset: Int): List<User> @Query("SELECT COUNT(id) FROM users") suspend fun queryUserTotalCount(): Int }
- 自定义PagingSource,可直接用页码作为key,不需要计算偏移量:
class CustomUserPagingSource( private val userDao: UserDao, private val pageSize: Int, private val initPageIndex: Int = 5 // 默认初始加载第6页,可动态传入 ) : PagingSource<Int, User>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> { return runCatching { val currentPage = params.key ?: initPageIndex val offset = currentPage * pageSize val pageData = userDao.queryUsersByPage( limit = params.loadSize, offset = offset ) val totalCount = userDao.queryUserTotalCount() val prevKey = if (currentPage <= 0) null else currentPage - 1 val nextKey = if (offset + pageData.size >= totalCount) null else currentPage + 1 LoadResult.Page( data = pageData, prevKey = prevKey, nextKey = nextKey ) }.getOrElse { LoadResult.Error(it) } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { return state.anchorPosition?.let { anchorPos -> state.closestPageToPosition(anchorPos)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPos)?.nextKey?.minus(1) } } }
- 修改Pager初始化逻辑,不需要再传initialKey:
Pager( config = PagingConfig( pageSize = 20, prefetchDistance = 5, initialLoadSize = 20, enablePlaceholders = false // 自定义实现不依赖占位符,可按需关闭 ), pagingSourceFactory = { CustomUserPagingSource(userDao, 20) } ).flow.flowOn(Dispatchers.IO).cachedIn(viewModelScope)
如果数据量超过10w条,不建议继续用OFFSET分页,可以把PagingSource的key改为上一页最后一条数据的id,用
WHERE id > :lastId LIMIT :pageSize的方式做键值分页,性能远高于位置偏移分页。
内容的提问来源于stack exchange,提问作者robert
相关产品推荐
相关产品推荐

