RemoteMediator 与缓存
结合网络分页和数据库分页
RemoteMediator 与缓存 是 CoddyKit 上的免费 Android Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 4 节课。
离线优先分页
普通的网络 PagingSource 可以正常工作,但连接断开后列表就会变空。要构建健壮的应用,您需要让数据库成为唯一可信数据源,并在后台由网络为其填充数据。
这正是 RemoteMediator 所实现的功能:从本地数据库进行分页,并在数据库中的数据用尽时从网络获取更多数据。
架构
使用 RemoteMediator 时,数据沿一个方向流动:
- 界面从 Room 的
PagingSource进行分页 - 当 Room 中的数据接近末尾时,
RemoteMediator获取网络中的下一页 - 网络结果被写入 Room
- Room 发出新增的行,界面随之更新
即使处于离线状态,用户也总能立即看到缓存的数据。
// Network ---> RemoteMediator ---> Room (source of truth) ---> UI
// UI never reads the network directly免费获得 Room PagingSource
Room 可以自动生成 PagingSource。您只需声明一个返回 PagingSource<Int, Entity> 的查询,Room 就会负责实现它。
界面实际就是从这个数据源进行分页的。
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Query
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles ORDER BY position ASC")
fun pagingSource(): PagingSource<Int, ArticleEntity>
}继承 RemoteMediator
RemoteMediator<Key, Value> 有一个必需的方法 load(),以及一个可选的 initialize()。
load() 接收一个 LoadType(REFRESH、PREPEND 或 APPEND)和当前的 PagingState,然后返回一个 MediatorResult。
import androidx.paging.ExperimentalPagingApi
import androidx.paging.RemoteMediator
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
private val api: ArticleApi,
private val db: AppDatabase
) : RemoteMediator<Int, ArticleEntity>() {
// override suspend fun load(loadType, state): MediatorResult
}处理 LoadType
在 load() 中,您需要根据 LoadType 决定要获取哪一页:
- REFRESH - 加载第一页(或锚点附近的页面)
- PREPEND - 通常无需执行任何操作;返回
endOfPaginationReached = true - APPEND - 加载当前已有数据之后的页面
val page: Int = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND ->
return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastKey = db.remoteKeyDao().last()?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)
lastKey
}
}远程键表
与 PagingSource 不同,RemoteMediator 不能直接返回下一页或上一页的键,这些键必须持久化。请将它们与数据一起存储在一个小型的远程键表中。
这样,应用重启后就可以继续执行 APPEND/PREPEND。
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "remote_keys")
data class RemoteKey(
@PrimaryKey val articleId: Int,
val prevKey: Int?,
val nextKey: Int?
)将网络结果写入 Room
获取一页数据后,请在事务中将数据项及其键写入 Room。执行 REFRESH 时,应先清除旧表,以保持缓存一致。
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.remoteKeyDao().clearAll()
db.articleDao().clearAll()
}
val nextKey = if (response.items.isEmpty()) null else page + 1
db.remoteKeyDao().insertAll(
response.items.map { RemoteKey(it.id, prevKey = page - 1, nextKey = nextKey) }
)
db.articleDao().insertAll(response.items.map { it.toEntity() })
}返回 MediatorResult
通过返回一个 MediatorResult 来结束 load():
- 没有更多页面时返回
Success(endOfPaginationReached = true) - 可能还有更多页面时返回
Success(endOfPaginationReached = false) - 失败时返回
Error(e),这会体现为LoadState.Error
return try {
val response = api.getArticles(page = page, size = state.config.pageSize)
db.withTransaction { /* write items + keys */ }
MediatorResult.Success(
endOfPaginationReached = response.items.isEmpty()
)
} catch (e: IOException) {
MediatorResult.Error(e)
} catch (e: HttpException) {
MediatorResult.Error(e)
}将 Pager 与 remoteMediator 连接起来
同时使用 remoteMediator 和 Room 的 pagingSourceFactory 构建 Pager。中介器负责填充 Room;界面从 Room 进行分页。
由于使用了实验性 API,请通过 @OptIn(ExperimentalPagingApi::class) opt in。
@OptIn(ExperimentalPagingApi::class)
fun articles(): Flow<PagingData<ArticleEntity>> = Pager(
config = PagingConfig(pageSize = 20),
remoteMediator = ArticleRemoteMediator(api, db),
pagingSourceFactory = { db.articleDao().pagingSource() }
).flow查找 APPEND 所需的键
执行 APPEND 加载时,您需要最后一个已加载数据项的 nextKey。请使用 PagingState 的锚点,从远程键表中找到正确的远程键。
通过辅助函数,可以让跨越 REFRESH、APPEND 和 PREPEND 的 load() 保持清晰易读。
private suspend fun lastRemoteKey(
state: PagingState<Int, ArticleEntity>
): RemoteKey? {
return state.pages.lastOrNull { it.data.isNotEmpty() }
?.data?.lastOrNull()
?.let { db.remoteKeyDao().keyFor(it.id) }
}
// In load(): for APPEND
val nextKey = lastRemoteKey(state)?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = true)缓存与新鲜度
由于数据存储在 Room 中,应用可以立即加载数据并支持离线使用。为了避免内容过时,请重写 initialize(),根据缓存存在的时间决定是否在启动时刷新。
override suspend fun initialize(): InitializeAction {
val lastUpdate = db.remoteKeyDao().lastUpdatedMillis() ?: 0L
val cacheTimeout = TimeUnit.HOURS.toMillis(1)
return if (System.currentTimeMillis() - lastUpdate >= cacheTimeout) {
InitializeAction.LAUNCH_INITIAL_REFRESH
} else {
InitializeAction.SKIP_INITIAL_REFRESH
}
}快速检查
在 RemoteMediator 架构中,界面进行分页时所依赖的唯一可信数据源是什么?
总结:RemoteMediator 与缓存
您构建了一个离线优先的分页流程:
- 界面从 Room 的
PagingSource进行分页,它是唯一可信数据源 RemoteMediator.load()处理REFRESH/PREPEND/APPEND,并在事务中将结果写入 Room- 远程键表会持久化下一页和上一页的键,使其在应用重启后仍然可用
Pager同时接收remoteMediator和pagingSourceFactoryinitialize()控制缓存的新鲜度
现在,您可以高效地加载无限列表,无论在线还是离线。恭喜您完成 Paging 3 课程。
用 AI 导师学习 Kotlin — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 36
- 课程
- 152
常见问题解答
「RemoteMediator 与缓存」课时是免费的吗?
是的 — 「RemoteMediator 与缓存」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 4 节课。
「RemoteMediator 与缓存」这节课中我会学到什么?
结合网络分页和数据库分页 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Android Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「RemoteMediator 与缓存」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Android Academy 课中编写并运行代码吗?
能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 为什么需要分页
- PagingSource 与 Pager
- 在 Compose 列表中实现分页
- RemoteMediator 与缓存