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