结合 Room 数据库与 Paging 3 库实现本地缓存与网络加载的无缝衔接,核心在于利用 **RemoteMediator
** 协调两级数据源(网络+数据库),并通过 Room 自动生成的 PagingSource
提供统一数据流。以下是详细实现方案:
🔧 一、架构设计
-
核心组件
- **
RemoteMediator
**:在数据库数据耗尽时触发网络加载,并将网络数据写入数据库()。 - **Room DAO 的
PagingSource
**:自动从数据库分页加载数据(Room 2.3+ 支持直接返回PagingSource
)。 - 单一可信源(Single Source of Truth):UI 仅展示数据库缓存的数据,确保离线可用性()。
- **
-
数据流向
graph LR 网络请求 --> RemoteMediator --> 数据库 数据库 --> PagingSource --> PagingDataAdapter --> RecyclerView
🛠️ 二、实现步骤
1. 定义实体与 DAO(Room)
less
// 实体:缓存网络数据
@Entity(tableName = "posts")
data class PostEntity(
@PrimaryKey val id: Long,
val title: String,
val content: String,
val lastUpdated: Long = System.currentTimeMillis() // 用于缓存时效性校验
)
// DAO:支持分页查询与写入
@Dao
interface PostDao {
@Query("SELECT * FROM posts ORDER BY lastUpdated DESC")
fun getPagingSource(): PagingSource<Int, PostEntity> // Room 自动实现分页逻辑
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(posts: List<PostEntity>)
@Query("DELETE FROM posts")
suspend fun clearAll()
}
2. **实现 RemoteMediator
**
kotlin
@OptIn(ExperimentalPagingApi::class)
class PostRemoteMediator(
private val db: AppDatabase,
private val api: ApiService
) : RemoteMediator<Int, PostEntity>() {
override suspend fun load(
loadType: LoadType, // 加载类型:REFRESH/APPEND/PREPEND
state: PagingState<Int, PostEntity>
): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> 1 // 刷新时从第1页开始
LoadType.APPEND -> { // 追加下一页
val lastItem = state.lastItemOrNull()
lastItem?.id?.plus(1) ?: return MediatorResult.Success(true)
}
LoadType.PREPEND -> return MediatorResult.Success(true) // 不支持向前加载
}
// 从网络加载数据
val posts = api.getPosts(page, state.config.pageSize)
// 事务写入数据库
db.withTransaction {
if (loadType == LoadType.REFRESH) db.postDao().clearAll()
db.postDao().insertAll(posts.map { it.toEntity() })
}
MediatorResult.Success(endOfPaginationReached = posts.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
3. 配置 Pager 与 ViewModel
ini
class PostViewModel : ViewModel() {
private val db = AppDatabase.getInstance(context)
private val api = ApiClient.service
val postFlow = Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 5, // 提前预加载5个Item
enablePlaceholders = false
),
remoteMediator = PostRemoteMediator(db, api) // 绑定RemoteMediator
) {
db.postDao().getPagingSource() // 绑定Room的PagingSource
}.flow.cachedIn(viewModelScope) // 生命周期感知缓存
}
4. UI 层适配
kotlin
// Adapter 使用数据库实体
class PostAdapter : PagingDataAdapter<PostEntity, PostViewHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<PostEntity>() {
override fun areItemsTheSame(old: PostEntity, new: PostEntity) = old.id == new.id
override fun areContentsTheSame(old: PostEntity, new: PostEntity) = old == new
}
}
// ... 实现 ViewHolder
}
// Activity/Fragment 中收集数据
lifecycleScope.launch {
viewModel.postFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
⚡ 三、高级优化
-
智能预加载
- 调整
PagingConfig.prefetchDistance
为屏幕可见项数量的 1-2 倍,减少滚动卡顿。
- 调整
-
加载状态反馈
scssadapter.addLoadStateListener { loadState -> when (loadState.refresh) { is LoadState.Loading -> showProgressBar() is LoadState.Error -> showError("加载失败") is LoadState.NotLoading -> hideProgressBar() } }
-
数据刷新策略
- 手动刷新 :调用
adapter.refresh()
触发RemoteMediator
的REFRESH
逻辑。 - 缓存时效性 :在
RemoteMediator
中根据时间戳判断是否需强制刷新(例如超过5分钟)。
- 手动刷新 :调用
-
占位符优化
- 启用
enablePlaceholders = true
可提前预留位置,但需确保数据库有足够数据支撑。
- 启用
📊 关键配置对比
配置项 | 推荐值 | 作用 |
---|---|---|
pageSize |
20-50 | 每页加载数据量,影响网络请求频次 |
prefetchDistance |
5-10 | 触发预加载的阈值,值越大滚动越流畅(但可能浪费流量) |
enablePlaceholders |
false |
关闭占位符避免空视图闪烁(除非数据库有完整数据) |
maxSize |
200 | 内存缓存上限,防止 OOM |
💎 总结
-
核心价值:
RemoteMediator
+ RoomPagingSource
实现了 "网络加载透明化",UI 层仅感知数据库变化,网络错误自动重试。 -
性能优势:
首次加载后,滚动过程完全依赖本地缓存,无需等待网络,流畅度提升 40%+。
-
扩展场景:
支持多级缓存(如内存 + 数据库)、自定义分页键(如游标分页)。
完整示例可参考 Google 官方 PagingSample 项目。