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

📅 2026/7/8 20:32:58
Flutter/HarmonyOS 实战|中式美食买菜清单勾选状态:checkedIds、统计卡与可点击食材条
上一篇写买菜清单页时重点讲的是“从一桌方案拆出采购分组”。这篇往里再走一步用户真的开始核对食材时页面状态应该怎么跟着变。买菜清单这种页面最容易被做成静态列表。菜名、数量、备注都摆出来好像就完成了。但真实使用时用户需要一边看一边确认鱼买了没有排骨买了没有青菜能不能换调味料家里有没有。这个时候勾选状态和进度反馈就很重要。中式美食当前的做法是用checkedIds统一记录已确认食材再让统计卡、食材条和底部动作一起响应这个状态。这样用户看一眼就知道清单还差几项不用自己在脑子里数。本文验证环境项目版本或范围开发工具Flutter / Dart 工程配合 DevEco Studio 做 HarmonyOS 上架前体验验证Dart SDK^3.12.2Flutter 依赖flutter、cupertino_icons、flutter_lintsHarmonyOS 目标中式美食上架后的家庭餐桌链路HarmonyOS SDK API 20 作为后续适配目标核心文件lib/main.dart核心页面ShoppingListPage核心组件ShoppingSummaryCard、ShoppingSection、ShoppingIngredientSlip核心状态checkedIds这里讲的是当前 Flutter 工程里的真实实现。后面如果迁移到 ArkUI思路仍然一样一个集合记录已确认项列表和统计都从这个集合派生。不要给每个条目单独维护状态清单页初始化时会先根据当前餐桌方案生成分组再把默认已确认的食材放进checkedIdsclass_ShoppingListPageStateextendsStateShoppingListPage{latefinalListShoppingSectionDatasections;latefinalSetStringcheckedIds;overridevoidinitState(){super.initState();sectionsshoppingSectionsFor(widget.plan);checkedIds{for(finalsectioninsections)for(finaliteminsection.items)if(item.checked)item.id,};}}这个写法的关键是页面不直接改ShoppingItemData.checked。如果用户点一下就去改数据对象本身后面会出现两个麻烦。第一原始清单和当前确认状态混在一起第二想重置、保存、对比变化时不好做。用SetString存 id 会清爽很多原始清单还是原始清单用户当前操作是另一层状态。这类页面我更喜欢把“数据是什么”和“用户现在点了什么”分开。前者来自方案后者来自页面交互。勾选切换要足够简单toggleItem()的逻辑很直白voidtoggleItem(Stringid){setState((){if(!checkedIds.add(id)){checkedIds.remove(id);}});}Set.add()如果发现 id 已经存在会返回false。所以这里可以很自然地写成加不进去就说明已经勾过了那就移除。这个逻辑小但它决定了交互是否稳定。用户连点几次不会把状态弄乱列表刷新后统计也会一起变。统计卡不要只是装饰清单页顶部的统计卡会展示总项数和已确认项intgettotalItemssections.fold(0,(total,section)totalsection.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.checkedfalse,});finalStringid;finalStringname;finalStringamount;finalString?note;finalbool checked;}比如“小青菜可换菜心”。这个备注会影响用户是否勾选如果现场没有小青菜但有菜心用户知道可以替换就不会停在那里纠结。所以买菜清单的条目不应该只有name。数量、备注、默认状态都值得放进模型里。后面如果接家庭库存甚至可以继续加“家里已有”“常买品牌”“预计价格”。工程验收记录检查点当前结果默认勾选initState()根据ShoppingItemData.checked初始化checkedIds状态切换toggleItem()通过Set增删 id状态逻辑简单统计反馈ShoppingSummaryCard使用checkedIds.length展示已确认数量整行点击ShoppingIngredientSlip外层可点击小勾选圈也可点击数据边界原始食材数据和用户当前勾选状态分开后续扩展可以继续接家庭库存、替换建议、价格估算本文小结买菜清单的价值不只是列出食材而是帮用户把“我确认了什么、还差什么”看清楚。中式美食这里用checkedIds管住勾选状态用统计卡反馈进度再把整行食材做成可点击。这个实现不花哨但很适合家庭餐食场景用户在买菜时不想研究页面只想快速确认下一项。如果你也在做清单类页面我建议先把两个问题想清楚状态是不是只有一个来源用户是不是能不用精确点击也完成操作这两个点做好页面会比单纯堆视觉组件更耐用。最后留个具体问题如果买菜清单支持多人协作你会把checkedIds做成本地状态同步还是直接把每个食材的确认状态写进远端记录我会先本地跑顺再把状态同步抽出去。