96SEO 2026-08-08 14:02 22
只从网络分页,断网后页面就会失去内容;只从数据库读取,又需要自己处理拉取、缓存和翻页。当 UI 同时依赖网络与本地时会出现闪烁、缓存不一致甚至错误提示频繁弹出的情况,使使用者体验大打折扣。怎么说呢, 解决思路: 将界面始终观察 Room 的 PagingSource。由 RemoteMediator 在后台决定何时拉取并写入数据库。这使得“显示”完全由本地驱动,“同步”完全由后台完成。不过,下面以资讯列表为例,逐步实现可刷新、可续页、可离线浏览的完整方案。并解释最容易出错的一致性问题。

此方式避免了多源冲突,使离线与在线走同一方法。
/// 存放真正展示的数据 @Entity data class ArticleEntity( @PrimaryKey val id : Long。val title : String,val summary : String,val publishedAt : Long ) **Remote key table**/// 用来记录每条记录相邻页的位置,/// 它不是展示字段,但决定了翻页边界。@Entity data class ArticleRemoteKey( @PrimaryKeyval articleId : Long,var previousPage : Int?,var nextPage : Int?) **⚙️ 使用者痛点②** 如果没有正确维护 remote key,就可能出现:
@Dao interface ArticleDao {
// 排序一定要稳定,否则翻页期间顺序抖动
@Query(
\"\"\"
SELECT * FROM articles
ORDER BY publishedAt DESC。id DESC
\"\"\")
fun pagingSource : PagingSource
@Insert
suspend fun upsertAll
@Query
suspend fun clearAll
}
@Dao interface ArticleRemoteKeyDao {
@Query
suspend fun remoteKeyById : ArticleRemoteKey?
@Insert
suspend fun insertAll
@Query
suspend fun clearAll
}
📦 数据库层须提供事务支持
@Database
abstract class AppDatabase : RoomDatabase{
abstract fun articleDao : ArticleDao
abstract fun keyDao : ArticleRemoteKeyDao
}
负载类型说明:
-
{@link LoadType#REFRESH}: 首次加载或手动下拉刷新的时候触发;
-
{@link LoadType#PREPEND}: 尝试向头部追加(此接口一般无前置翻页,可直接结束);
-
{@link LoadType#APPEND}: 向尾部追加,根据上一次成功返回的 nextPage 来确定下一批请求。
APPEND 时若当前列表为空却立即标记 endOfPaginationReached=true,将永远不再尝试抓取新内容!请返回 false 并等待实际查询完成再做判断。
@OptIn
class ArticlesRemoteMediator(
private val api : ArticlesApi,private val database : AppDatabase ) :
RemoteMediator
{
private val dao = database.articleDao
private val keyDao = database.keyDao
override suspend fun load(loadType: LoadType。state:PagingState
) =
try {
// ---------- 确定请求哪一页 -----------------
var page:Int?= when{
LoadType.REFRESH -> STARTING_PAGE
LoadType.PREPEND -> return MediatorResult.Success
else -> { // APPEND
state.lastItemOrNull?老实说,.let {last->
keyDao.remoteKeyById?.nextPage
},说起来,的观点是。return MediatorResult.Success
}
}
// ---------- 拉取 ---------------------------------
var response = api.getArticles
// ---------- 写入 ---------------------------------
database.withTransaction{
if{keyDao.clearAll;dao.clearAll}
// 每条记录都插入对应键值对
response.items.map{dto->dto.toEntity}.let{entities->
dao.upsertAll
keyRepoFromEntities.let{keys->
keyRepo.insertAll
}
}
}
MediatorResult.Success
}catch{MediatoResult.Error}catch{MediatoResult.Error}
}
fun ArticlesDto.toEntity:ArticleEntity={..}
private const STARTING_PAGE=1
fun keyRepoFromEntities:List{..}
-
刷新时需要清理旧键+旧数据+插入新键+插入新实体,一旦任意一步失败就会留下“有键无值”或“有值无键”的脏态。
-
若把写操作拆成多步。
即便最终成功也可能在半途中被挂起,从而造成临时空白屏幕或部分更新。
-
常用方法先完成 API 调用 + 本地 DTO 转换。接下来在同一次
database.withTransaction{} 内执行全部 DB 操作,只要整个事务成功才算一次完整更新。
class ArticlesRepository{
companion object{const const PAGE_SIZE=20}
@OptIn
internal operator fun invoke: Flow =
Pager(
config=PagingConfig(pageSize=PAGE_SIZE。prefetchDistance=,enablePlaceholders=false),remoteMediator=ArticlesRemoter,pagingSourceFactory={dbAppDb.article.pagingSrc}
).flow
-
pageSize 必须与服务器分页大小保持一致,否则每次触发都会多余一次请求或者漏掉部分内容。
-
prefetchDistance 太大容易提前消耗流量。也太小则可能滑到底部还未触发,再也没机会抓到下一批。
-
placeholders 若服务端能提供总数,可开启占位符;否则建议关闭简化逻辑,
使用 CombinedLoadStates 可以分别查看 local source 与 mediator 状态,以便做出更细粒度 UI 展示:
lifecycleScope.launchWhenStarted{
viewModel.items.collectLatest
}
lifecycleScope.launchWhenStarted{
adapter.loadStateFlow.collectLatest {states->
// 当 mediator 正在 loading 且无任何 item 时显示全屏进度条
binding.progress.isVisible =
states.mediator?.refresh is LoadState.Loading && adapter.itemCount==0
binding.swipeRefresh.isRefreshing =
states.mediator?.refresh is LoadState.Loading && adapter.itemCount>0
binding.errorGroup.isVisible =
states.mediator?.refresh is LoadState.Error && adapter.itemCount==0
binding.offlineHint.isVisible =
states.mediator?.refresh is LoadState.Error && adapter.itemCount>0
}
}
adapter.setFooter)
binding.recyclerView.adapter=adapter.withLoadStateFooter
注意:* 当有缓存但 network 错误发生。只保留已有列表并轻量提示即可,不必切换至全屏错误页;只有完全无缓存且 refresh 错误才弹全屏报错。*
默认每次 new Pager 都会执行 REFRESH。如果想短时间内复用已存在缓存,可覆盖 initialize
html
// …override suspend fun initialize: InitializeAction {
return cacheMeta.lastUpdated?.let{ last->
if-last
// 将更新时间写进同一次 transaction 中,以保证 “成功写入 + 更新时间” 原子一致。cacheMeta.updateTime)
根据业务类型调整超时时间。例如价格信息可以短于一分钟,而新闻文章可以长达数小时。
步骤
检查项
常见原因
服务端排序
是否使用固定字段
新增/删除影响位置
主键信息
是否真唯一?其实,
重复 ID 会覆盖
nextPage 写法
是否末尾正确设为 null?怎么说呢,
没设置 → 无限递归
空响应判定
是否识别为空数组为结束?
忽略 →
请求
刷新流程
是否同时清理 keys + entity?
留下脏 Key 或 Data
DAO 排序
与服务器是否保持一致?
翻页顺序错乱
若支持分类/关键词,请为 query 参数生成独立 query_key 并作为 composite primary key:
kotlin
@Entity
data class QueryAwareLocal
@Entity
data class QueryAwareRemotekey
否则切换筛选条件可能拿到上一种条件下遗留的 paging 状态。
使用内存 Room + mock API 可以覆盖以支:
1️⃣ 首次 REFRESH 写入 business & keys
- 验证两张表都有记录且数量匹配
- 验证 order 与预期相符
2️⃣ REFRESH 替换旧缓存
- 确认 old keys/data 已被删除
3️⃣ APPEND 正确获取 nextPage 并写回 keys
4️⃣ 接口返回空数组 → endOfPaginationReached=true
5️⃣ 网络异常 → 保留旧缓存且未提交 transaction
6️⃣ 同 ID 重复调用 -> UPDATE 而非 INSERT
7️⃣ 多个查询参数互不污染
测试不仅检查 MediatorResult,还应直接查询 DB 验证最终状态。按理说,
🚦🛑🟢🟣🟧🟥🟨🟪⬛⬜ ➜ ⬆️ ⬇️ ➜ ❗️❗️❗️⚙️🏁📱🚀✈️🤖🔒🔐🔥🌐💡📊🔍🎯🔎⛔️🏅🍎🍓🍇🥭🥕🌽🍚🍱🥗🍔🥪☕🍺🍸🥂🎉👨💻🚧💬🙌🛒🏆💸🎉📦✉️📞😱🙃😂👍🏾👏🏼🤝🏿❓😕💭👀🙏💭↩️➕➖✖️➗∑⌛⌚⏰⏱〰〱〴〵⊂⊃⊄⊅⊇⊈⊉≦≧≤≥∈∋∩∪⋂⋃∀∃≈≡≅≤≥⇔⇔↔↕↨↩↪⇧⇩⌈⌉⌊⌋⎮⎴⎸⎹┐╭╯▹◀▶◤◥▿│█▢▣▤▥▦▋⑴⑵⑶⑷⑸⑹⑺⓵⓶⓷⓸⓹⓺ⒶⒷⒸⒹⒺⒻⒼⒽ⓫㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉①②③④⑤⑥⑦⑧⑨⑩㈱㈲㈳㈴㈵㈶㈷㈸㈹ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹᴀʙᴄᴅᴇꜰɢʜɪʤᴋʟᴍɴᴏᴘǫɾˢʈᴜᴠʏᴢᴀʙabcdefghijKMNOPQRSTUVWXYZABCDEFGHIJKLMNOPABCDEFGSTUVWSXYYZZ𐌀𐌀𐌀𐌀𐌀𐌀𐌀𐌀𐌀𐌀》《▼▲◆◇○◎△▽☆★♯♭♪♬♩♪♫♭♮♯♫♪﹄﹄﹄﹄﹄﹄﹚﹛﹜﹝﹞︰︱︲︳︴︵︶︷︸︹︺︻︼〉〈※‼‼‼‼‼‼‼‼‽‾〜〓〘〖〝〞〟‥『』〖』「」『」』「」《》〈〉《》『』「」『」『』「」 《》《〉〈〉《》〈〈〈〈〈 』』
小结 🚀
通过让 Room 成为唯一事实来源 并利用 RemoteMediator 在后台统一管理同步与分页逻辑。可以把“显示”和“同步”彻底拆解开来实现真正意义上的离线优先。说到关键步骤包括,
1️⃣ 建立业务表 + remote‑key 表。并保证它们永远保持原子一致性。2️⃣ 在 load 中精准计算下一页索引,同时避免空列表误判结束。3️⃣ 配置合适的 Pager 参数及 Cache 超时时间,让使用者体验平稳且可靠。4️⃣ 用 CombineLoadStates 区分 network vs local 状态。
在 UI 上做细粒度展示,而不是一次性错误页面。
5️⃣ 对每个分支编写单元测试,以防止难以排查的重复/漏掉/死循环问题。
这样即使设备断网、进程重建或频繁手动刷新的情况下也能保证列表始终完整且连贯 —— 真正做到 “永不失联”,同时满足高质量离线体验需求。按理说,
标签:
离线
-
上一篇:
ArkUI 声明式 UI,实战从何开始?
-
下一篇:
Rust异步编程,如何实现并发控制与超时?
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback