HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十八):【数据持久化】收藏与浏览历史——让数据在 App 重启后依然“活着”
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(二十八):【数据持久化】收藏与浏览历史——让数据在 App 重启后依然“活着”
摘要:收藏一道菜谱、回顾之前看过什么菜——这些功能在前 27 篇中只能活在内存里。App 重启后,所有收藏和历史全部消失。本篇利用已有的
RelationalStoreHelper(完整 CRUD 封装),新增三张持久化表,让收藏和历史在 App 重启后依然存在。你还会学到:为什么收藏用主键约束而历史用追加写入?为什么浏览历史的写入失败不阻塞页面跳转?以及,如何用约 45 行代码完成从建表到 UI 联动的完整持久化闭环。
一、引言:内存的“失忆症”
一个有趣的测试:在第 27 篇的基础上,收藏一道“番茄牛腩煲”,然后关掉 App,重新打开。
收藏按钮恢复成了空心——App 完全忘记了刚才的操作。
这不是 Bug,这是内存的失忆症。前 27 篇中,所有用户数据——收藏的菜谱、浏览过的记录、个人偏好——都存储在@State或@Local变量中。这些变量的生命周期与组件绑定,组件销毁时数据也随之消失。
| 数据类型 | 存储位置 | 生命周期 | App 重启后 |
|---|---|---|---|
| 推荐结果 | HomeViewModel.recommendedRecipes | 页面级 | ❌ 消失 |
| 收藏状态 | RecipeDetailPage.heartLiked | 组件级 | ❌ 消失 |
| 浏览记录 | 无存储 | — | ❌ 从未存在过 |
🎯本篇目标:利用已有的
RelationalStoreHelper,新增收藏表、浏览历史表和社区分享表,配合 UI 层的两处微小改动,让数据在 App 重启后依然“活着”。核心代码仅约 45 行。
二、核心原理:关系型数据库的“记账本”模型
2.1 为什么是 SQLite 而非 Preferences?
HarmonyOS 提供了两种本地持久化方案:
| 方案 | 数据结构 | 查询能力 | 适用场景 |
|---|---|---|---|
| Preferences | 键值对 | 仅 get(key) | 简单配置(头像路径、昵称) |
| RelationalStore(SQLite) | 表 + SQL | SELECT/INSERT/DELETE/ORDER BY | 结构化数据(收藏、历史、订单) |
收藏和历史属于后者——你需要按时间排序查询“最近浏览的 10 道菜”,需要判断“这道菜是否已收藏”。这些需求用键值对也能实现(把所有数据序列化为 JSON 存一个 key),但查询效率低、代码丑陋、容易出错。
关系型数据库就像一个记账本:每笔收藏是一行,每笔浏览也是一行。你可以随时翻阅(SELECT)、追加(INSERT)、划掉(DELETE),不需要关心这本账怎么保存——SQLite 替你管。
2.2 已有的基础设施
前 27 篇中,我们已经在RelationalStoreHelper中建立了完整的 CRUD 封装——initDatabase()、executeSql()、insert()方法。这些方法已用于用户登录注册的本地存储(local_users表)。本篇不新增任何数据库基础设施,只扩展现有实例。
三、表结构设计:每一列都有存在的理由
在已有的数据库LingxiKitchen.db中新增三张表:
-- 收藏表(recipe_id 为主键,保证同一菜谱只收藏一次)CREATETABLEIFNOTEXISTSfavorite_recipes(recipe_idINTEGERPRIMARYKEY,recipe_nameTEXTNOTNULL,saved_atINTEGERDEFAULT(strftime('%s','now')));-- 浏览历史表(每次浏览追加一条,不设主键约束)CREATETABLEIFNOTEXISTSrecipe_history(recipe_idINTEGERNOTNULL,recipe_nameTEXTNOTNULL,viewed_atINTEGERDEFAULT(strftime('%s','now')));-- 社区分享表(预留,为社区功能做准备)CREATETABLEIFNOTEXISTSshared_recipes(idINTEGERPRIMARYKEYAUTOINCREMENT,user_nameTEXTNOTNULL,recipe_nameTEXTNOTNULL,ingredientsTEXT,stepsTEXT,shared_atINTEGERDEFAULT(strftime('%s','now')));建表语句在RelationalStoreHelper.createTables()中追加,使用CREATE TABLE IF NOT EXISTS保证幂等——重复调用不会出错。
设计考量:
| 设计点 | 选择 | 理由 |
|---|---|---|
favorite_recipes主键 | recipe_id | 收藏是唯一性操作——同一菜谱只需一条记录,取消收藏时 DELETE,重新收藏时 INSERT |
recipe_history主键 | 无(追加写入) | 浏览是可重复操作——用户可能三天看五次“番茄牛腩煲”,完整时间线比最新记录更有分析价值 |
saved_at/viewed_at默认值 | strftime('%s','now') | SQLite 内置时间函数,无需在 ArkTS 侧传入时间戳,减少代码量和时钟偏差风险 |
shared_recipes步骤字段 | TEXT(JSON 序列化) | 步骤是数组结构,SQLite 不直接支持数组类型,JSON 序列化是最简单的跨语言兼容方案 |
四、收藏逻辑:INSERT 与 DELETE 的一体两面
4.1 写入时机
在RecipeDetailPage底部操作栏的收藏按钮中,在onClick中调用this.toggleFavorite():
.onClick(()=>{this.heartLiked=!this.heartLiked;this.heartScale=1.3;setTimeout(()=>{this.heartScale=1;},150);this.toggleFavorite();// ← 新增持久化,在动画播放的同时异步写入})4.2 toggleFavorite 方法
privateasynctoggleFavorite():Promise<void>{try{if(this.heartLiked){awaitstoreHelper.insert('favorite_recipes',{recipe_id:this.recipe.id,recipe_name:this.recipe.name,saved_at:Date.now()});}else{awaitstoreHelper.executeSql('DELETE FROM favorite_recipes WHERE recipe_id = ?',[this.recipe.id.toString()]);}}catch(err){console.error('[RecipeDetail] 收藏持久化失败:',JSON.stringify(err));}}图一解读:收藏和取消收藏是同一操作的两个方向——用recipe_id作为主键,INSERT 和 DELETE 对称操作。数据库不关心用户是第一次收藏还是取消后重新收藏——它只执行 SQL,由 ArkTS 侧的heartLiked状态决定方向。
4.3 设计考量:为什么不阻塞动画?
toggleFavorite()是异步的,但onClick没有await它。这意味着动画先播(150ms 弹跳),数据库写入在后台并行进行。如果数据库写入失败(比如磁盘满),用户已经看到了动画反馈——这会不会不一致?
不会。收藏功能的核心价值是再次打开 App 时还能看到收藏,而不是“点击瞬间的数据一致性”。如果写入失败,下次打开 App 时收藏会丢失——这确实是个问题,但它发生的概率远低于用户因为等待 I/O 而感知到的卡顿。用户体验的优先级是:即时反馈 > 数据持久化 > 错误处理。前两者保证了“好用”,第三者保证了“不出大问题”。
五、浏览历史:追加写入,静默失败
5.1 写入时机
在Index.ets的handleRecipeTap方法开头新增写入:
privatehandleRecipeTap(recipe:Recipe):void{// ★ 写入浏览历史(失败不阻塞跳转)try{storeHelper.insert('recipe_history',{recipe_id:recipe.id,recipe_name:recipe.name,viewed_at:Date.now()});}catch(_err){}// 原有跳转逻辑(不受历史写入影响)this.getUIContext().getRouter().pushUrl({...});}5.2 设计考量:为什么静默吞错误?
浏览历史不是关键路径。用户点击菜谱卡片时的核心诉求是看到菜谱详情,而不是“确保这次浏览被记录”。如果数据库写入失败(磁盘满、表损坏),阻塞跳转或弹出错误提示都会严重破坏体验。
图二解读:浏览历史是一条分叉路——主路径(跳转)和副路径(写入)并行。副路径失败不影响主路径。这是“非关键路径静默失败”的设计模式——适用于所有“有更好、没有也行”的增值功能。
5.3 为什么是追加而非更新?
如果用户三天内看了五次“番茄牛腩煲”,你应该存五条记录还是一条记录?
| 策略 | 存储方式 | 能回答的问题 |
|---|---|---|
| 更新(UPDATE) | 一条记录,更新viewed_at | “最近什么时候看过这道菜” |
| 追加(INSERT) | 五条记录,各自有时间戳 | + “看过多少次” + “什么时候最常看” + “看了之后收藏了吗” |
追加的成本只是多占几行磁盘空间(每条约 100 字节),但换来了完整的行为时间线。后续可以扩展“最近浏览”列表、“猜你喜欢”推荐、“看了但没收藏”提醒等功能。追加不是冗余,是未来数据分析的基础设施。
六、代码交付清单
| 文件 | 新增/修改 | 行数 | 说明 |
|---|---|---|---|
RelationalStoreHelper.ets | 修改 | +25 | createTables新增三张建表 SQL |
RecipeDetailPage.ets | 修改 | +15 | 收藏按钮加入toggleFavorite()调用 |
Index.ets | 修改 | +5 | handleRecipeTap新增浏览历史写入 |
七、设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 收藏表主键 | recipe_id | 收藏是唯一性操作,同一菜谱只需一条记录 |
| 历史表写入策略 | 追加(INSERT)而非更新 | 保留完整行为时间线,为数据分析打基础 |
| 历史写入失败处理 | 静默吞错误,不阻塞跳转 | 浏览历史是增值功能,非关键路径 |
| 动画与持久化的顺序 | 动画先播,持久化异步并行 | 用户感知的延迟来自 I/O,动画填补了这段空白 |
| 不新建 DataSource 类 | 直接复用storeHelper单例 | 已有完整 CRUD,不引入额外抽象层 |
八、本阶段总结与下篇预告
本篇用约 45 行新增代码,让《灵犀厨房》的收藏和浏览历史从“内存失忆”变为“持久记忆”:
- 三张新表:
favorite_recipes(收藏)、recipe_history(浏览历史)、shared_recipes(预留社区) - 收藏的 INSERT/DELETE 对称操作:
recipe_id主键让收藏和取消是同一操作的镜像 - 浏览历史的追加写入:静默失败不阻塞跳转,完整时间线为未来数据分析打基础
- 最小侵入:UI 层仅两处改动,数据库基础设施复用已有封装
现在重新打开 App,收藏依然在,浏览历史可追溯——App 开始有了“记忆”。
下篇预告:第 29 篇《个人中心:偏好持久化与推荐联动》。我们将把用户的口味偏好、忌口设置和健康档案持久化到本地数据库,并让推荐引擎在下次启动时自动读取这些偏好——真正做到“越用越懂你”。
📚 本系列持续更新中:下一篇将让推荐引擎与用户偏好联动,开启个性化推荐的正循环。
🔗专栏入口:[《HarmonyOS6.1全场景实战》合集]
📦 获取基线版本源码包:包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对您有所帮助,麻烦您动动发财之手点赞 👍、收藏 ⭐ 和评论 💬。谢谢大家!!
