Flutter/HarmonyOS 实战|中式美食买菜清单勾选状态:checkedIds、统计卡与可点击食材条

Flutter/HarmonyOS 实战|中式美食买菜清单勾选状态:checkedIds、统计卡与可点击食材条

上一篇写买菜清单页时,重点讲的是“从一桌方案拆出采购分组”。这篇往里再走一步:用户真的开始核对食材时,页面状态应该怎么跟着变。

买菜清单这种页面,最容易被做成静态列表。菜名、数量、备注都摆出来,好像就完成了。但真实使用时,用户需要一边看一边确认:鱼买了没有,排骨买了没有,青菜能不能换,调味料家里有没有。这个时候,勾选状态和进度反馈就很重要。

中式美食当前的做法是:用checkedIds统一记录已确认食材,再让统计卡、食材条和底部动作一起响应这个状态。这样用户看一眼就知道清单还差几项,不用自己在脑子里数。

本文验证环境

项目版本或范围
开发工具Flutter / Dart 工程,配合 DevEco Studio 做 HarmonyOS 上架前体验验证
Dart SDK^3.12.2
Flutter 依赖fluttercupertino_iconsflutter_lints
HarmonyOS 目标中式美食上架后的家庭餐桌链路,HarmonyOS SDK API 20 作为后续适配目标
核心文件lib/main.dart
核心页面ShoppingListPage
核心组件ShoppingSummaryCardShoppingSectionShoppingIngredientSlip
核心状态checkedIds

这里讲的是当前 Flutter 工程里的真实实现。后面如果迁移到 ArkUI,思路仍然一样:一个集合记录已确认项,列表和统计都从这个集合派生。

不要给每个条目单独维护状态

清单页初始化时,会先根据当前餐桌方案生成分组,再把默认已确认的食材放进checkedIds

class_ShoppingListPageStateextendsState<ShoppingListPage>{latefinalList<ShoppingSectionData>sections;latefinalSet<String>checkedIds;@overridevoidinitState(){super.initState();sections=shoppingSectionsFor(widget.plan);checkedIds={for(finalsectioninsections)for(finaliteminsection.items)if(item.checked)item.id,};}}

这个写法的关键是:页面不直接改ShoppingItemData.checked

如果用户点一下就去改数据对象本身,后面会出现两个麻烦。第一,原始清单和当前确认状态混在一起;第二,想重置、保存、对比变化时不好做。用Set<String>存 id 会清爽很多,原始清单还是原始清单,用户当前操作是另一层状态。

这类页面我更喜欢把“数据是什么”和“用户现在点了什么”分开。前者来自方案,后者来自页面交互。

勾选切换要足够简单

toggleItem()的逻辑很直白:

voidtoggleItem(Stringid){setState((){if(!checkedIds.add(id)){checkedIds.remove(id);}});}

Set.add()如果发现 id 已经存在,会返回false。所以这里可以很自然地写成:加不进去就说明已经勾过了,那就移除。

这个逻辑小,但它决定了交互是否稳定。用户连点几次,不会把状态弄乱;列表刷新后,统计也会一起变。

统计卡不要只是装饰

清单页顶部的统计卡会展示总项数和已确认项:

intgettotalItems=>sections.fold(0,(total,section)=>total+section.items.length);ShoppingSummaryCard(totalItems:totalItems,checkedCount:checkedIds.length,)

这里的checkedCount直接来自checkedIds.length,不是另起一个计数器。

这是一个很重要的习惯:能从状态算出来的值,就不要再存一份。否则用户勾选了食材,列表状态变了,但统计数字没变,这种 bug 很难看。

显示项来源作用
食材总类sections汇总告诉用户这次采购复杂度
已确认数量checkedIds.length告诉用户还差多少项
预计采购时间页面配置给用户一个心理预期

统计卡不是为了好看,而是为了让用户知道自己进行到哪了。

食材条要整行可点

移动端上,如果只让用户点一个小圆圈,体验会比较累。中式美食这里把整条食材都做成可点击:

classShoppingIngredientSlipextendsStatelessWidget{constShoppingIngredientSlip({requiredthis.item,requiredthis.checked,requiredthis.onToggle,});finalShoppingItemDataitem;finalbool checked;finalVoidCallbackonToggle;}

真实组件里外层用了InkWell,里面的小勾选圈也能点。这样用户在超市或者厨房里拿着手机,不需要精确点中一个很小的控件,手指点到整行就能切换状态。

这类细节看着不技术,但很影响留存。生活类工具不是给用户演示组件的,用户是在边走边看,甚至手上还拿着东西。交互热区大一点,体验会舒服很多。

备注要跟状态一起读

食材条里除了名称和数量,还有备注:

classShoppingItemData{constShoppingItemData({requiredthis.id,requiredthis.name,requiredthis.amount,this.note,this.checked=false,});finalStringid;finalStringname;finalStringamount;finalString?note;finalbool checked;}

比如“小青菜,可换菜心”。这个备注会影响用户是否勾选:如果现场没有小青菜,但有菜心,用户知道可以替换,就不会停在那里纠结。

所以买菜清单的条目不应该只有name。数量、备注、默认状态都值得放进模型里。后面如果接家庭库存,甚至可以继续加“家里已有”“常买品牌”“预计价格”。

工程验收记录

检查点当前结果
默认勾选initState()根据ShoppingItemData.checked初始化checkedIds
状态切换toggleItem()通过Set增删 id,状态逻辑简单
统计反馈ShoppingSummaryCard使用checkedIds.length展示已确认数量
整行点击ShoppingIngredientSlip外层可点击,小勾选圈也可点击
数据边界原始食材数据和用户当前勾选状态分开
后续扩展可以继续接家庭库存、替换建议、价格估算

本文小结

买菜清单的价值不只是列出食材,而是帮用户把“我确认了什么、还差什么”看清楚。

中式美食这里用checkedIds管住勾选状态,用统计卡反馈进度,再把整行食材做成可点击。这个实现不花哨,但很适合家庭餐食场景:用户在买菜时不想研究页面,只想快速确认下一项。

如果你也在做清单类页面,我建议先把两个问题想清楚:状态是不是只有一个来源?用户是不是能不用精确点击也完成操作?这两个点做好,页面会比单纯堆视觉组件更耐用。

最后留个具体问题:如果买菜清单支持多人协作,你会把checkedIds做成本地状态同步,还是直接把每个食材的确认状态写进远端记录?我会先本地跑顺,再把状态同步抽出去。