Android 学习笔记 D13|离线列表:刷新文章时,别把用户收藏一起覆盖
摘要:把文章元数据与收藏分开存储,让数据库成为首页读取来源,设计缓存启动、刷新失败、事务替换和缓存过期行为。
标签:Android、离线缓存、Room、Repository、数据一致性
本文对应 28 天课程 D13,连接《第一行代码》第 3 版第 7、11、13 章。今天的目标是在断网时仍能浏览已经保存的首页文章元数据。缓存标题、作者和 URL 不等于缓存网页全文,文章详情中的外部页面仍可能需要网络;这一点应在界面和博客中讲清楚。
1. 首页只保留一条显示链路
最初的网络页面会把响应直接交给 UI,后来增加缓存,又在启动时从数据库读取另一份列表。如果两条链路独立写状态,容易出现网络成功显示新数据、缓存稍后返回又覆盖新数据的问题。本文选择数据库作为首页读取来源:页面观察本地查询,网络刷新成功后只更新数据库。
这意味着“刷新成功”应包括本地写入成功。HTTP 收到内容但数据库写入失败,页面仍没有新的可靠状态,不能一边显示成功一边静默丢弃。离线架构需要明确读取来源和写入责任,官方指南也把本地来源视为上层读取的重要依据。Android 离线优先指南
本课缓存首页列表,搜索结果暂不持久化;因此离线时可以浏览已有首页,但不承诺任意关键词都能搜索。功能边界越具体,实验越容易判断,而不是把所有失败都藏在“缓存机制”四个字里。
2. 用三类记录区分三种事实
文章内容表保存 ID、标题、作者、链接与更新时间。首页成员表保存文章 ID 和位置,表示某篇文章当前是否属于这份首页快照。收藏表保存用户主动选择及收藏时间。内容、列表归属、用户选择分别变化,分开之后就不必靠一张大表里的多个布尔值互相猜测。
刷新首页时可以更新文章内容并重建首页成员,不删除收藏。某篇文章从新首页消失,也不意味着它不再可收藏或详情必须立即失效。初学版本暂时保留已获取内容,之后再根据引用和期限清理;不能简单在刷新前执行“删除所有文章”,否则可能破坏收藏详情。
下面是 SQL 结构与事务的教学伪代码,旨在说明提交边界,不是可直接复制编译的 Room API。Room 2 和 Room 3 的事务调用应使用所选版本对应文档,字段及 DAO 需在工程中实现。
CREATETABLEarticle_cache(articleIdINTEGERPRIMARYKEY,titleTEXTNOTNULL,authorTEXTNOTNULL,urlTEXTNOTNULL,updatedAtINTEGERNOTNULL);CREATETABLEhome_entry(articleIdINTEGERPRIMARYKEY,positionINTEGERNOTNULL);-- 一次成功网络响应已经转换成合法模型之后:BEGINTRANSACTION;-- 逐条插入或更新 article_cache,保留 bookmark 不动DELETEFROMhome_entry;-- 按响应顺序插入新的 home_entryCOMMIT;-- 首页读取:SELECTa.*,(b.articleIdISNOTNULL)ASisBookmarkedFROMhome_entry hJOINarticle_cache aONa.articleId=h.articleIdLEFTJOINbookmark bONb.articleId=a.articleIdORDERBYh.position;真正实现还应对网络结果按 ID 去重,并为顺序赋值,避免相同 ID 两次插入冲突。首页成员与内容更新需要在同一事务中完成,确保读者不会看到“成员已替换而文章尚未写入”的中间状态。网络请求本身应在数据库事务外进行,不能让慢网络占据长事务。
3. 缓存年龄与失败提示是不同状态
有缓存时启动,先显示本地结果,再尝试刷新;刷新失败保留内容,并告诉用户当前显示的是已有数据。无缓存时失败,则显示失败重试,不把它写成“没有文章”。成功响应为空时,本例清空首页成员并展示真正空态,但不会清空收藏或所有文章资料。
给缓存记录更新时间,可以说明数据来自什么时候。超过期限并不必然必须隐藏:新闻、配置、价格等业务对陈旧数据的容忍不同。本学习项目允许陈旧文章元数据继续展示并提示更新失败。缓存淘汰、请求节流和展示过期提示属于不同策略,不应只放一个expired就试图解决全部问题。
HTTP 缓存也不能替代这里的业务数据库。前者按 HTTP 语义复用响应,后者支持按文章 ID 查找、关联本地收藏和稳定展示。它们可以同时存在,但发生问题时必须分辨数据是网络响应缓存命中,还是本地文章表查询得到。
4. 四组离线实验
先联网加载 A、B 两篇文章,收藏 B,然后断网并重启。预期首页仍显示 A、B,B 的收藏标记仍在,刷新提示失败但内容不消失。随后清理测试数据,用无缓存状态断网进入,预期出现可重试错误而非成功空列表。
第三组让 Fake 返回相同 ID 的新标题。刷新后预期标题改变,收藏时间与收藏归属保持不变。第四组在替换首页事务中人为抛出测试异常,预期整个数据库变更回滚,旧首页仍可读取;不得只剩空成员或半份结果。故障注入仅在测试实现里进行,记录前后 ID 集合和收藏行数。
再额外观察启动顺序:数据库首次发出结果与网络刷新可以快慢不同,但页面只接受同一查询链路的输出。若仍然会新旧闪回,应检查是否还残留一个直接把网络列表写进 UI 的入口。这里的描述是验证方法,尚非你设备上的测试结论。
5. 原创面试问答与追问
问一:数据库作为读取来源有什么价值?在线与离线显示经过同一查询,减少多套列表竞争。追问:网络成功本地写入失败算刷新成功吗?本设计不算,页面的数据更新未完成提交。
问二:为什么收藏不能放进每次覆盖的网络文章里?收藏是本地用户意图,网络刷新不应重置它。追问:必须分表吗?不绝对,但更新语句必须明确保护收藏字段,分表能把边界表达得更清楚。
问三:缓存过期后一定要删吗?由业务容忍度决定,文章元数据可以带提示继续展示。追问:保存了 URL 能离线读网页吗?不能,需要另外设计全文或资源保存方案。
以上是原创自测,不代表面试鸭题库原题或官方答案。
6. D13 验收
画出“网络刷新→事务写入→数据库查询→UI”的数据流;说明收藏为何不会被刷新覆盖;完成有缓存断网、无缓存断网、同 ID 更新及写入失败回滚四组实验。书本第 7 章建立持久化基础,第 11 章提供网络来源,第 13 章连接 Room。下一课将通过取消全部收藏检验这些边界是否真的成立。