
1. 项目背景与核心设计思路1.1 为什么要在 OpenHarmony 上做 Flutter 开发先聊点实际的。我前阵子接了个小项目要在某国产设备上做一个家具购买记录 App需求倒不复杂把家里买过的家具记下来品牌、型号、购买日期、价格、保修状态定期提醒哪些东西快过保了。客户指定的平台不是安卓也不是 iOS而是 OpenHarmony——因为设备本身跑的就是这套系统而且日后可能要上应用商店。这套系统的好处是什么它在 API 设计上有意识地做了兼容层凡是 OpenHarmony 上标记为 supported 的 Flutter 插件写法和标准 Flutter 几乎一致。也就是说我之前积累的 Flutter 经验可以直接平移过来不用为了一个简单的工具类 App 去学两套原生开发。坏处也很明显生态不如安卓成熟一些组件、插件、构建工具链需要踩坑尤其是涉及系统能力和原生交互的部分。这个项目用的是 Flutter for OpenHarmony 的适配分支。平时开发流程和标准 Flutter 一模一样用 Dart 写业务逻辑用 Widget 搭界面只是最后构建目标平台变成 OpenHarmony。理解这一点很重要因为后面的所有代码示例除了平台相关配置你会发现和普通 Flutter 项目没有区别。1.2 App 的功能定位与保修模块的价值如果再往深看一层这个 App 解决的需求其实是很普遍的消费记录问题。我们买家具的时候保修卡、发票、说明书经常随手一放等真出了问题翻箱倒柜找不着保修期过了才想起来。所以一个家具购买记录 App 的核心价值不是记录我买了什么而是记录这些东西什么时候需要维护、什么时候过保、出了问题找谁。所以在这个 App 里保修模块不是锦上添花是刚需。一张双人床一两千一组定制衣柜可能上万保修期内一个电话就能免费修过了保修期上门费都得自己掏。保修模块要解决的问题有四个第一记录保修起始日和保修时长自动计算出到期日第二状态可视化一眼看出哪些在保、哪些临期、哪些已过保第三维护记录联动每次维修都追加到该家具的保修时间线上第四提醒机制临期前一个月开始提示用户联系厂商续保或安排检查。这个模块做得好不好直接决定了这个 App 是花架子还是真能帮上忙的工具。1.3 技术选型的取舍选型阶段其实纠结过要不要用原生开发。OpenHarmony 的 ArkTS 确实有官方支持UI 组件也够用但问题在于团队里没人写过 ArkTS而我们对 Flutter 的熟悉程度已经形成了肌肉记忆。与其花两周学一门新语言不如用熟悉的 Flutter 快速把业务跑通。Flutter 在这类场景下的优势在于声明式 UI 写起来非常顺手尤其是表单类界面状态管理清晰自带 Material 组件库输入框、日期选择器、对话框开箱即用不需要额外引入 UI 库热重载让调试效率大幅提升这在开发初期调整界面细节时尤其重要一套代码多端复用以后如果客户要求出安卓版本改动成本几乎为零。代价也有。OpenHarmony 的 Flutter 适配层目前对部分插件支持不完整比如我习惯用的某个本地数据库插件在适配版里就不能直接用被迫换了方案。这个后面在实操部分细说。1.4 项目结构总览整个项目采用标准的 feature-first 架构。核心模块分为三层数据层本地数据库表结构、仓库类、模型定义业务层保修状态计算逻辑、提醒处理、数据校验UI 层列表页、详情页、添加/编辑表单、设置页。添加保修功能落在数据层和 UI 层之间核心是一个保修记录的增删改查流程外加一个状态自动更新的计算引擎。下面的内容我会从数据模型设计开始逐步带你走完整条实现链路。2. 数据模型设计与状态机2.1 家具记录的主模型在设计数据库表结构之前我先把业务对象梳理了一遍。家具记录是主实体保修信息是依附于主实体的从属性信息但考虑到后期可能有跨实体查询的需求——比如所有今年过保的家具——我决定把保修字段直接嵌进主表而不是单独建一张关联表。这个决策基于一个简单的事实保修信息不会出现一对多的情况。一件家具只有一个保修策略要么有保要么没保不存在一个家具对应多条保修记录的情况。既然是一对一嵌进主表可以减少一次联表查询代码也更直接。主模型定义大致如下class FurnitureItem { final String id; // 唯一标识 final String name; // 家具名称 final String brand; // 品牌 final String model; // 型号 final DateTime purchaseDate;// 购买日期 final double price; // 购买价格 final String? storeName; // 购买渠道 final String? receiptPath; // 发票照片路径 final WarrantyInfo? warranty; // 保修信息可空 final DateTime createdAt; // 记录创建时间 final DateTime updatedAt; // 记录最后更新时间 }design 上多花一分钟后面少写十行补丁代码。把warranty设计为可空对象意味着用户可以只录入基础信息不填保修内容后面想补随时可以编辑。2.2 保修字段的设计与可扩展性考量接下来是重点——WarrantyInfo模型。这块是整个功能的核心数据载体设计时我反复推敲了几轮。class WarrantyInfo { final DateTime startDate; // 保修起始日 final int durationMonths; // 保修时长月 final String? warrantyCardNo;// 保修卡编号 final String? providerPhone; // 售后电话 final String? providerName; // 售后方名称 final String? remarks; // 备注 final ListMaintenanceRecord maintenanceRecords; // 维修记录列表 }几个细节值得展开startDate默认取购买日期但允许手动调整。因为实际业务中有些厂商的保修起始日不是购买日而是安装日或者送货日尤其定制家具经常出现款付了半年才送货安装的情况。这个灵活性必须给用户。durationMonths用月做单位而不是年。绝大多数家具厂商的保修期表述是3年5年但也有部分小品牌按36个月来标。统一换算成月存库计算到期日时就变成纯日期运算不需要考虑闰年、跨年这些边界问题。maintenanceRecords是 List 类型意味着一个保修期内可以追加多次维修记录。这个字段我用了一个简单的内嵌列表数据库层面用 JSON 字段存储。数据量不会很大——一个家庭用得上的东西通常就是一个衣柜一张床——没必要为了这个列表单独建表。2.3 保修状态的计算逻辑数据记下来不是目的目的是随时能知道这个东西现在是个什么状态。所以我定义了一个枚举类型来表示保修状态enum WarrantyStatus { active, // 在保 expiringSoon, // 即将到期30天内 expired, // 已过保 noWarranty, // 未登记保修信息 }状态不是存储在数据库里的而是每次查询时根据日期动态计算。这么做的好处很实在数据库里存的是一个绝对的业务事实——startDate和durationMonths——而状态是一个相对的判断结果。如果用户修改了 startDate或者系统当前时间变化了状态不需要额外更新只在计算时得出正确结果即可。具体判断逻辑WarrantyStatus getWarrantyStatus(WarrantyInfo? warranty, DateTime now) { if (warranty null) return WarrantyStatus.noWarranty; final endDate warranty.startDate.add(Duration(days: warranty.durationMonths * 30)); final daysLeft endDate.difference(now).inDays; if (daysLeft 0) return WarrantyStatus.expired; if (daysLeft 30) return WarrantyStatus.expiringSoon; return WarrantyStatus.active; }注意这里用durationMonths * 30来估算天数是因为月本身不是固定长度。日常精度足够不差那两三天真到了提醒功能阶段再按自然月精确计算也来得及。2.4 对象关系与模块边界数据模型设计完模块边界也自然清晰了。FurnitureItem 是聚合根WarrantyInfo 是值对象MaintenanceRecord 是子实体。聚合根通过仓储接口暴露操作能力值对象不单独暴露数据访问能力。这样做的好处是添加保修这个需求本质上就是在修改 FurnitureItem 的warranty字段。无论 UI 层怎么做表单最终落到数据层的就是一个save(FurnitureItem item)调用。UI 层不用关心数据怎么存数据层不用关心表单长什么样。边界清楚了接下来做 UI 就顺理成章。3. 添加保修表单的实现细节3.1 表单的字段编排与默认值策略进入 UI 实现环节。添加保修表单我拆成了