Preferences 适合保存最近一次选择和轻量开关,但最近浏览要排序、去重、展示时间,就需要 RDB 明细表。两边同时存在时,最怕状态写一半。这不是页面上多写几个条件判断就能解决的事。中式美食这类应用,列表、详情、收藏、购物清单、推荐流会互相跳转,状态一乱,用户看到的就是“刚才点过的东西没了”。
| 应用名称 | 中式美食 |
|---|---|
| 本章主题 | 中式美食怎么把 Preferences 快速状态同步到 RDB 明细表 |
| 核心文件 | entry/src/main/ets/data/UserPreferenceStore.ets |
| 关键模块 | UserPreferenceStore / ViewedRecipeRepository |
| 技术关键词 | HarmonyOS,ArkTS,Preferences,RDB,中式美食 |
本章导读
这章只说一个真实工程问题:Preferences 适合保存最近一次选择和轻量开关,但最近浏览要排序、去重、展示时间,就需要 RDB 明细表。两边同时存在时,最怕状态写一半。
| 先看哪里 | 为什么要看 |
|---|---|
| 状态入口 | 先确认哪些动作会改数据 |
| Repository 边界 | 决定查询和写入放在哪里 |
| ViewModel 输出 | 页面拿到的是可直接渲染的数据 |
| 失败兜底 | 出错时不能让旧列表假装是新结果 |
当前验证环境
| 项目 | 版本或说明 |
|---|---|
| DevEco Studio | 6.x 系列 |
| HarmonyOS SDK | API 12/ArkTS 工程 |
| UI 框架 | ArkUI 声明式开发 |
| 数据层 | Preferences + RDB relationalStore |
| 验证设备 | 模拟器与真机页面流转 |
先把边界说清楚
我的处理方式是把状态先收口,再让页面渲染。页面不直接拼查询条件,组件也不偷偷改业务字段。这样写前面会慢一点,但后面加搜索、收藏、推荐、购物清单联动时,问题会少很多。
| 层级 | 应该负责 | 不要负责 |
|---|---|---|
| Page | 布局、点击、加载态展示 | 直接拼 SQL 或拼存储 key |
| ViewModel | 组合页面状态,暴露可渲染模型 | 操作底层表结构 |
| Repository | 查询、写入、事务、去重 | 关心按钮颜色和页面文案 |
| Model | 稳定字段和默认值 | 临时 UI 动画状态 |
exportinterfaceViewedRecipeSyncTask{keyword?:stringcategoryId?:stringrecipeId?:stringcheckedOnly?:booleansortBy?:'default'|'recent'|'favorite'}数据模型先定下来
我不建议一开始就把所有字段塞到一个对象里。中式美食这里会先分两类:一类是业务字段,一类是页面展示字段。业务字段要稳定,展示字段可以后面再算。
exportinterfaceViewedRecipeRecord{id:stringrecipeId:stringtitle:stringcategoryName:stringupdatedAt:numberchecked:boolean}| 字段 | 用途 | 踩坑点 |
|---|---|---|
| id | 列表稳定 key | 不能用 index 顶替 |
| recipeId | 回到菜谱来源 | 不然详情页不知道来源 |
| categoryName | 分类展示和筛选 | 不要只存展示文案 |
| updatedAt | 排序和最近记录 | 时间戳要统一单位 |
| checked | 勾选或状态标识 | 刷新后要能恢复 |
Repository 不要把脏数据交给页面
页面最怕拿到半成品。Repository 要把默认值、空数组、重复记录先处理好,再交给 ViewModel。这样页面写起来会笨一点,但后面问题少很多。
exportclassViewedRecipeSyncTaskRepository{asyncquery(input:ViewedRecipeSyncTask):Promise<ViewedRecipeRecord[]>{constrows=awaitthis.queryRows(input)returnrows.map(row=>this.toModel(row))}privatetoModel(row:Record<string,Object>):ViewedRecipeRecord{return{id:String(row.id??''),recipeId:String(row.recipeId??''),title:String(row.title??''),categoryName:String(row.categoryName??'未分类'),updatedAt:Number(row.updatedAt??0),checked:Boolean(row.checked??false),}}}为什么不让页面自己兜底?因为页面兜底多了之后,每个入口都会有一套临时补丁。搜索页补一次,收藏页补一次,推荐页再补一次,同一道菜就会在不同页面显示成不同状态。
ViewModel 只输出页面能直接用的结果
ViewModel 的目标不是炫技,而是让页面少判断。中式美食页面多,列表、详情、收藏、购物清单都在读相似数据,输出模型越稳定,后面复用越舒服。
@ObservedexportclassViewedRecipeSyncTaskViewModel{items:ViewedRecipeRecord[]=[]loading:boolean=falseerrorMessage:string=''asyncreload(query:ViewedRecipeSyncTask){this.loading=truethis.errorMessage=''try{this.items=awaitthis.repository.query(query)}catch(err){this.errorMessage='数据暂时没取到,可以稍后再试'}finally{this.loading=false}}}| 页面状态 | 用户看到什么 | 开发要检查什么 |
|---|---|---|
| loading | 骨架或加载文案 | 是否覆盖旧请求 |
| empty | 明确告诉用户没有结果 | 查询条件是否真的为空 |
| error | 允许重试 | 错误是否被吞掉 |
| ready | 稳定列表 | key 是否稳定 |
ArkUI 页面不要偷改业务状态
ArkUI 写起来很方便,但也容易在组件里顺手改数据。我的习惯是:组件只发动作,ViewModel 决定怎么改。这样后面加埋点、加缓存、加失败回滚都还有位置。
@Componentstruct ViewedRecipeRecordList{@ObjectLinkvm:ViewedRecipeSyncTaskViewModelbuild(){List(){ForEach(this.vm.items,(item:ViewedRecipeRecord)=>{ListItem(){Text(item.title).fontSize(16).fontWeight(FontWeight.Medium)}},(item:ViewedRecipeRecord)=>item.id)}}}这里的 key 一定要用业务 id。不要用 index。index 在筛选、排序、插入、删除之后都会变,列表复用就容易把上一条状态带到下一条。
常见问题复盘
| 问题 | 表现 | 处理方式 |
|---|---|---|
| 旧请求覆盖新请求 | 搜索后又跳回旧列表 | 请求带版本号或时间戳 |
| key 不稳定 | 勾选状态串行 | ForEach 使用业务 id |
| 空态不清楚 | 用户不知道是没数据还是加载失败 | emptyReason 单独输出 |
| 查询散落在页面 | 每个入口逻辑不一样 | 收回 Repository |
| 文案和真实条件不一致 | 标题显示分类 A,列表是全部 | 筛选状态只保留一份 |
letrequestVersion=0asyncfunctionsafeReload(query:ViewedRecipeSyncTask){constcurrent=++requestVersionconstnext=awaitrepository.query(query)if(current!==requestVersion){return}this.items=next}工程验收
我会按下面这几项验收,不只看页面能不能跑起来。
| 验收项 | 操作 | 通过标准 |
|---|---|---|
| 首次进入 | 打开目标页面 | loading 到 ready 不闪旧数据 |
| 条件切换 | 连续切关键词、分类、排序 | 最终列表和标题一致 |
| 空结果 | 输入不存在的菜名 | 空态文案清楚,不显示旧列表 |
| 返回页面 | 从详情页返回 | 原筛选条件还在 |
| 勾选或收藏 | 改一条状态后刷新 | 状态不串到别的条目 |
expect(vm.loading).toBe(false)expect(vm.items.every(item=>item.id.length>0)).toBeTruthy()expect(newSet(vm.items.map(item=>item.id)).size).toBe(vm.items.length)本章小结
中式美食怎么把 Preferences 快速状态同步到 RDB 明细表这类问题,不是多写几个 if 就能解决。中式美食现在的处理方式是把查询、状态和展示拆开:Repository 负责干净数据,ViewModel 负责页面模型,ArkUI 组件只负责展示和触发动作。
如果你也在做 HarmonyOS 菜谱、工具类或者内容列表页,可以先检查一件事:页面里的状态是不是只有一个可信来源。只要状态来源多了,低概率问题迟早会变成线上问题。