在开发鸿蒙应用《面食菜谱大全》时,原本只想做菜谱列表和详情页,后来逐步加入收藏、常做菜、餐单、购物清单、下厨模式和厨师笔记。功能之间需要传递上下文:从餐单生成采购需求要记住餐次和份量,从下厨页面进入计时工具要保留当前菜谱信息,完成后写心得也要关联到对应菜品。本文整理开发过程中遇到的购物清单合并、来源幂等、保存时序和ArkUI菜单绑定崩溃问题,供开发生活工具类鸿蒙应用参考。
一、拆分业务状态与建立来源身份
应用把“买过了”“准备好了”“做完了”设计成独立状态。买了面粉不代表已经和好面,购物清单全部勾选也不代表完成了一次下厨。只有分清这些状态,应用才能准确记录用户进行到了哪一步。
购物清单的典型需求是合并多道菜中的相同食材,但重复点击“加入清单”不能让采购数量翻倍。同一道面周一吃一次、周三再吃一次,应该形成两次采购需求;同一个周一晚餐重复点击确认,采购数量不应增加。要区分这两种情况,每份采购需求都需要稳定的来源身份。项目中的来源ID按以下方式生成:
- source.sourceId = plan
- ? `plan:${plan.id}`
- : `direct:${recipe.recipe_id}`;
复制代码
通过餐单加入时,来源关联具体餐次;直接从菜谱加入时,来源关联菜谱。不同餐次可以分别累计,同一来源再次提交时核对并更新已有需求。直接加入同一道菜会复用同一来源,需要分别采购的多次安排可以通过不同餐次表达。界面上合并展示的食材,在数据层仍然保留各条明细的来源、类别、位置和菜谱等信息。用户看到的是一组鸡蛋,应用仍然知道这些鸡蛋分别为哪顿饭准备。
二、自然语言用量不能简单相加
菜谱里会出现“适量”“少许”“半碗”“1—2个”等非标准用量,同一种食材也可能分别使用“克”和“kg”。如果只提取文本开头的数字再相加,得到的结果看似精确,实际可能误导用户。项目采用的规则是:能算的算对,不能算的保留原文。例如“半碗”仍保留原样。对采购工具来说,明确告诉用户哪些可以计算、哪些需要自行判断,比给出一个不可靠的总数更有用。
三、份量变化与取消餐次要保留用户可见性
两人份改成四人份时,当前规则会将数量变化的需求重新标记为待购,未变化的明细保留原状态;复杂用量在份量变化后也需要重新确认。取消餐次时,应用不会自动删除关联采购。计划可以取消,食材却可能已经买回家。页面会提示这层关系,再由用户决定如何处理采购记录。这里的原则是:应用可以代替用户计算,但涉及已有记录的变化,必须让用户看得见。
四、预览不等于保存,保存成功后再更新界面
用户选择今天、未来三天、本周或下周的餐次后,应用会先生成备料预览,列出采购项的新增、更新、移除和保留情况。预览并不意味着可以直接保存,因为用户查看预览期间,餐单或购物清单仍可能发生变化。因此提交时服务层会重新检查日期、餐次、权限、份量和购物快照。旧预览失效时需要重新核对;已经保存的请求再次提交时,则通过请求ID查找回执,返回原来的结果。
真正保存时,不能先让界面显示成功再处理存储。购物服务中的保存方法采用以下顺序:
- private save(next: ShoppingSnapshotV2): void {
- this.guard();
- this.validate(next);
- next.revision = this.snapshot.revision + 1;
- try {
- this.pref.putStringDurable(KEY, JSON.stringify(next));
- } catch (e) {
- this.problem = '保存未确认,请重启应用恢复后重试';
- AppStorage.setOrCreate<string>('store_shopping_error', this.problem);
- throw new Error(this.problem);
- }
- this.snapshot = JSON.parse(JSON.stringify(next)) as ShoppingSnapshotV2;
- this.publish();
- }
复制代码
业务层准备好独立的新快照,校验并执行保存后,才替换内存快照、发布界面状态。保存抛出异常时,保留旧业务视图并提示问题,避免界面先显示成功、数据却没有得到确认。
不过,保存报错也不等于数据一定没有写入。项目的回归测试分别模拟了“写入前失败”和“数据已经落盘、确认时失败”。对于后一种情况,只恢复内存并不能让磁盘数据回到原状,需要在冷启动后读取实际快照和请求回执,判断操作是否已经完成。当前方案会在保存结果不确定时阻止继续进行关键写入,并提示重启恢复。这样会中断一次操作,但可以避免在未知状态上继续修改数据。这套实现也有明确的适用范围:采购最终校验与餐单变更共用串行队列,购物数据使用Preferences快照,餐单数据还涉及关系型数据库。
五、真机崩溃:bindMenu报$$this未定义
构建通过后,真机打开“安排一餐”时应用直接退出。日志指向MealSelect,错误为:
- ReferenceError: $$this is not defined
复制代码
沿着首页卡片、餐单页到菜单初始化的触发路径排查后,按照本地SDK对应的bindMenu签名调整了绑定方式,传入布尔状态this.open,并在菜单消失时同步关闭状态:
- .bindMenu(this.open, this.optionsMenu, {
- placement: this.above() ? Placement.TopLeft : Placement.BottomLeft,
- offset: { x: 0, y: this.above() ? -8 : 8 },
- onDisappear: (): void => { this.open = false; }
- });
复制代码
这次修复针对的是项目中这一处菜单绑定。它带来的直接提醒是:构建通过以后,仍然要在设备上走完进入页面、展开菜单、选择选项、关闭和保存的完整路径。
六、小结
《面食菜谱大全》的界面使用ArkTS和ArkUI,菜谱基础数据从端侧资源库读取。开发中花了不少时间处理采购来源、状态变化和保存恢复,因为它们直接影响用户能否放心使用清单和记录。对开发清单、旅行计划、手账等生活工具的同行来说,可以参考这几点:合并数据时保留来源,更新需求时重新核对状态,保存结果不确定时保留恢复路径,再用可重复执行的检查守住业务规则。AI也可以用于辅助整理厨师笔记、提取食材信息,但识别结果要允许用户核对和修改,并继续遵守来源追踪与保存规则。 |