如何在Paging 3中使用Network Bound Resource实现Room缓存?
Paging 3 结合 Network Bound Resource 实现 Room 缓存方案
前置依赖配置
首先在模块级build.gradle中引入必要依赖:
dependencies { // Paging 3 implementation "androidx.paging:paging-runtime-ktx:3.2.1" // Room implementation "androidx.room:room-runtime:2.5.2" kapt "androidx.room:room-compiler:2.5.2" implementation "androidx.room:room-ktx:2.5.2" // 网络请求可根据自身技术栈选择,这里以Retrofit为例 implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" }
核心实现步骤
- 第一步:定义缓存实体与分页远程键实体
业务数据实体需要包含完整的业务字段,远程键实体用于记录分页加载的锚点,避免重复加载:// 业务数据实体示例 @Entity(tableName = "users") data class User( @PrimaryKey val id: Long, val name: String, val avatar: String, // 可选:缓存过期时间字段,用于实现自动刷新逻辑 val cacheAt: Long = System.currentTimeMillis() ) // 分页远程键实体 @Entity(tableName = "remote_keys") data class RemoteKey( @PrimaryKey val label: String, // 区分不同分页流的标识 val nextPageKey: Int?, val prevPageKey: Int? ) - 第二步:实现Room DAO层
分别提供业务数据操作和远程键操作的DAO,其中业务DAO需要返回PagingSource供Pager调用:@Dao interface UserDao { @Query("SELECT * FROM users ORDER BY id ASC") fun getPagedUsers(): PagingSource<Int, User> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(users: List<User>) @Query("DELETE FROM users") suspend fun clearAllUsers() // 可选:查询最早缓存时间,用于过期判断 @Query("SELECT MIN(cacheAt) FROM users") suspend fun getOldestCacheTime(): Long? } @Dao interface RemoteKeyDao { @Query("SELECT * FROM remote_keys WHERE label = :label") suspend fun getRemoteKey(label: String): RemoteKey? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRemoteKey(remoteKey: RemoteKey) @Query("DELETE FROM remote_keys WHERE label = :label") suspend fun clearRemoteKey(label: String) } // 数据库类中需要包含上述两个DAO的声明 - 第三步:自定义RemoteMediator实现Network Bound Resource核心逻辑
RemoteMediator是Paging3专门用于处理「本地缓存+网络同步」场景的组件,完全匹配Network Bound Resource的分层逻辑:class UserRemoteMediator( private val db: AppDatabase, private val apiService: ApiService ) : RemoteMediator<Int, User>() { private val userDao = db.userDao() private val remoteKeyDao = db.remoteKeyDao() private val LABEL = "user_list" override suspend fun load(loadType: LoadType, state: PagingState<Int, User>): MediatorResult { return try { // 根据加载类型获取对应分页key val pageKey = when (loadType) { LoadType.REFRESH -> null // 刷新时从第一页开始加载 LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true) // 不需要前向加载可直接返回结束 LoadType.APPEND -> { val remoteKey = remoteKeyDao.getRemoteKey(LABEL) remoteKey?.nextPageKey ?: return MediatorResult.Success(endOfPaginationReached = true) } } val page = pageKey ?: 1 // 发起网络请求 val response = apiService.getUserList(page = page, pageSize = state.config.pageSize) val users = response.data val endOfPaginationReached = users.isEmpty() || response.nextPage == null // 数据库事务操作,保证数据一致性 db.withTransaction { if (loadType == LoadType.REFRESH) { // 刷新时清空旧数据 userDao.clearAllUsers() remoteKeyDao.clearRemoteKey(LABEL) } // 存储新的远程键 remoteKeyDao.insertRemoteKey( RemoteKey(label = LABEL, nextPageKey = response.nextPage, prevPageKey = response.prevPage) ) // 存储网络请求到的业务数据 userDao.insertUsers(users.map { it.copy(cacheAt = System.currentTimeMillis()) }) } MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (e: Exception) { MediatorResult.Error(e) } } // 可选:重写initialize方法实现缓存过期自动刷新 override suspend fun initialize(): InitializeAction { val cacheTime = db.userDao().getOldestCacheTime() ?: return LAUNCH_INITIAL_REFRESH return if (System.currentTimeMillis() - cacheTime > 2 * 3600 * 1000) { // 缓存有效期2小时 LAUNCH_INITIAL_REFRESH } else { SKIP_INITIAL_REFRESH } } } - 第四步:Repository层构造Pager返回分页流
class UserRepository(private val db: AppDatabase, private val apiService: ApiService) { fun getPagedUsers() = Pager( config = PagingConfig( pageSize = 20, enablePlaceholders = false, initialLoadSize = 60 // 首次加载量建议为pageSize的3倍 ), remoteMediator = UserRemoteMediator(db, apiService), pagingSourceFactory = { db.userDao().getPagedUsers() } ).flow // 在ViewModel层调用cachedIn(viewModelScope)绑定生命周期 }
注意事项
- 所有网络同步缓存的逻辑必须放在RemoteMediator中实现,不要在PagingSource中处理网络请求,符合Paging3的架构设计也便于统一处理分页状态
- 数据库的清空、插入操作必须放在事务中执行,避免出现分页键和业务数据不一致的问题
- 处理边界场景时要正确设置
endOfPaginationReached的值,比如网络返回空数据、没有下一页时要设为true,避免Paging3反复触发无效请求 - 如果不需要前向加载(比如普通的下拉刷新上拉加载场景),
LoadType.PREPEND可以直接返回分页结束,减少不必要的逻辑处理 - 分页配置的
initialLoadSize要和RemoteMediator的初始加载逻辑对齐,避免首次加载出现多余的网络请求
内容的提问来源于stack exchange,提问作者Momin Tahir
相关产品推荐
相关产品推荐

