You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 22:18:05