ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter跨端开发实战:用OpenHarmony打造家庭药箱与血糖记录App

Flutter跨端开发实战:用OpenHarmony打造家庭药箱与血糖记录App 上个月在整理家里药箱时发现三盒布洛芬已经过期大半年一管软膏标签全被磨掉甚至还有一瓶止咳糖浆压根儿看不出生产日期。当时我脑子里冒出来的想法其实挺朴素与其每次翻箱倒柜靠人眼排查不如直接写一个家庭药箱管理 App。恰好那段时间我在折腾 flutter_for_openharmony 这套工具链Flutter 代码写起来快OpenHarmony 生态又正缺这种本地化工具型应用于是这个项目就顺理成章地开搞了核心功能包含药品入库、过期预警、分类统计以及完整的血糖记录模块。这篇文章就是这个项目从零到一、从“新建项目跑不起来”到真机稳定运行的完整复盘适合有 Flutter 基础、想迁到 OpenHarmony 做工具型应用或者对健康记录类产品感兴趣的朋友。家庭药箱管理听起来是个小项目但真做起来会发现它同时踩中了跨端开发、原生插件适配、状态管理、数据库设计、图表可视化好几个深水区。尤其是血糖记录部分涉及单位换算、时段分组、趋势图渲染和隐私合规比单纯的 CRUD 复杂得多。我打算把整个项目的搭建过程、设计决策和踩坑日志拆开讲其中不少经验是文档里查不到的。1. 为什么把家庭药箱应用放在 Flutter OpenHarmony 组合上1.1 从一次真实药箱整理说起先说需求。一个家庭药箱管理 App 到底要解决什么问题我整理药箱时总结出的痛点是不知道家里有什么药、不知道哪些药快过期、不知道某类药还剩多少。慢性病家庭还会多一个诉求——帮老人记录血糖、血压这类指标观察趋势方便复查时给医生看。把这些需求拆开后功能清单其实很清晰药品的增删改查、有效期预警、余量提醒、分类统计加上一套独立的血糖记录模块。技术上没有太高门槛全是表单、列表、图表的组合但这恰恰是最适合用 Flutter 做的场景。为什么不选择给家人人手装一个不同的原生 App因为家里既有 Android 手机也有 OpenHarmony 设备我不想为一个工具型应用维护两套代码一套 Dart 通吃才是正解。硬件层面也有考量。OpenHarmony 设备大多定位在平板、智慧屏、国产终端这类家庭场景放一台在客厅茶几或餐边柜上打开就是一个专用药箱终端。Flutter 的 OpenHarmony 适配分支能直接产出 HAP 安装包意味着这套代码不只停留在模拟器里“自嗨”而是能落到真实家庭成员每天都会打开的设备上。1.2 OpenHarmony 上的 Flutter 分支现状很多人以为 Flutter 只支持 Android、iOS、Web 和桌面其实 OpenHarmony 也有官方社区维护的适配分支主要是 OpenHarmony-SIG 组织下的 flutter_flutter 仓库。它做的事情是把 Flutter 引擎跑在 OpenHarmony 的图形栈上让 Dart 代码可以编译成 HAP 包。绝大多数 Flutter 基础控件、路由、动画、手势系统在分支上都能用底层渲染和平台通道则做了独立的适配。但这并不意味着你可以无脑地把 Android 工程的依赖拿过来跑。OpenHarmony 分支的版本更新速度落后于 Flutter 上游官方 Flutter 已经发到 3.2x 甚至更高时分支可能还停留在 3.19 或 3.22 这个区间。所以我的第一个经验是锁版本别追新。一旦某个 Flutter 包的新版本依赖了上游引擎的新 API它在 OpenHarmony 分支上可能直接编译不过。上游 Flutter 版本OpenHarmony 适配分支我的使用建议3.16.x早期适配基础可用不建议插件生态太旧3.19.x适配较成熟社区资料多生产项目首选3.22.x跟随上游部分插件有断裂尝鲜可以稳定项目慎用3.24适配跟进中测试不充分等社区验证再说我最终选了 3.19 对应的分支原因非常实际它能跑通当前项目需要的所有插件稳定压倒一切。家庭药箱这种工具型应用用户不会因为你是最新版本就多给你一次打开的机会反而会因为三天两头闪退直接卸载。1.3 为什么不直接用 ArkTS聊 OpenHarmony 开发绕不开那个经典问题官方明明主推 ArkTS 和 ArkUI为什么还要折腾 Flutter我自己的答案分三层。第一从跨端角度。ArkTS 绑定的是 OpenHarmony 单端写出来的代码没法直接给 iOS 或 Android 用而 Flutter 一套代码可以同时覆盖 Android、iOS、OpenHarmony 甚至桌面端。我家里有 Android 手机也有 OpenHarmony 设备这需求是真实存在的不是理论探讨。第二从生态角度。Flutter 在图表、状态管理、数据库、UI 组件方面的第三方库非常丰富。fl_chart、provider、drift 这类成熟方案可以大幅缩短开发时间。ArkUI 本身不差但个人开发者能直接调用的高质量轮子相对少很多需求要自己从零搭。第三从开发效率角度。Dart 语言对前端转过来的开发者很友好热重载的开发体验在调 UI 时节省了大量时间。医疗工具类应用界面不需要夸张的视觉效果Flutter 自绘渲染的一致性反而成了优点在 Android 上显示什么样到 OpenHarmony 上基本还是什么样。对比维度ArkTS ArkUIFlutter(OpenHarmony 分支)官方支持度最高OpenHarmony 亲儿子社区维护官方认可跨端能力仅 OpenHarmonyAndroid/iOS/OpenHarmony/桌面第三方生态ArkUI 生态相对年轻Flutter 生态丰富成熟动画与渲染声明式性能不错自绘渲染跨端一致性好团队招聘难度相对小众Flutter 开发者的池子大得多结论如果你的目标只有 OpenHarmony 单端那我劝你老老实实用 ArkTS官方资料和工具链支持都最稳。但如果你像我一样需要覆盖多种设备或者想保留跨端选项Flutter 是更合理的投入。家庭药箱这种表单加图表的轻交互应用Flutter 的“写一套处处编译”价值远大于它的那一点点性能损耗。2. 环境搭建与“新建项目跑不起来”的完整排查链路2.1 工具链与版本组合OpenHarmony 上的 Flutter 开发环境和普通 Flutter 开发略有差异你需要准备以下东西DevEco StudioOpenHarmony 集成开发环境里面包含 OpenHarmony SDK 和模拟器Flutter 的 OpenHarmony 适配分支从 GitHub 拉取后切换到你计划使用的 release 分支ohpm 包管理器它是 OpenHarmony 生态自己的包管理工具和 pub、npm 职责类似hdc 命令行工具作用相当于 Android 生态里的 adb用来连接设备、安装 HAP、查看日志。环境变量这块是最容易踩坑的地方。首先要把DEVECO_SDK_HOME指向 DevEco Studio 内置的 OpenHarmony SDK 路径其次要确保PATH里排在前面的 Flutter 是你 clone 下来的 OpenHarmony 分支而不是官方 Flutter。我当时就载在这上面。电脑上原来装了官方 Flutter又 clone 了 OHOS 分支结果终端里flutter命令解析到的是官方版本flutter create根本不认识--platforms ohos这个参数。后面我把 flutter_ohos/bin 显式加到 PATH 最前面还是在多个终端窗口里反复出错。最后干脆不折腾全局环境变量了直接在项目目录里用一个alias flutter/path/to/flutter_ohos/bin/flutter每次开终端先看一眼flutter --version确认当前是这个分支这才算稳下来。2.2 第一次运行日志停在 dart_vm_initializer 的完整排查过程新建项目成功、编译成功但模拟器一启动就闪退这类问题在热词榜上挂了很久——“flutter新建项目后跑不起来”是很多人的第一道坎。我复现时看到的日志是这样E/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] unhand...如果你也停在这一行先别急着搜报错信息日志是被截断的。点开完整日志看后面跟的是什么我那次完整的报错其实是Unhandled exception: MissingPluginException(No implementation found for method getAll on channel com.example.shared_preferences)这才是关键。dart_vm_initializer.cc(41)只是 Flutter 引擎报“Dart 层有未捕获异常”的入口真正的错误原因在后面的异常详情里。这次闪退和 OpenHarmony 的引擎适配无关问题出在shared_preferences插件没有加载 OpenHarmony 平台的实现。这套排查链路我总结成了标准化流程先看完整异常而不是第一行。跳转到终端里日志的最后 30 行找 MissingPluginException 或者 Channel 名根据 Channel 名定位对应的 Flutter 插件去插件仓库看有没有ohos/目录没有就是没适配全局搜索代码里调用这个插件的地方注释掉或用替代方案重新编译确认闪退消失。后续我学到的通用经验是OpenHarmony 上的 Flutter 插件和 Android 插件不是自动兼容的。Flutter 官方插件默认实现的是 Android/iOS 平台通道OpenHarmony 分支需要独立的原生实现才不会被 MissingPluginException 炸掉。解决办法有三个换一个纯 Dart 实现的包、找社区维护的xxx_ohos适配包、自己写 MethodChannel 实现。2.3 从 flutter aar 到 HAP别把 Android 的构建习惯带过来热搜词里有一条特别有代表性you are applying flutters main gradle plugin imperatively using the apply s。这条报错看着像 OpenHarmony 相关实际上它是 Android 工程里用 Gradle 的 apply 脚本方式集成 Flutter 模块时常见的问题。为什么会在 OpenHarmony 项目里搜到这条报错因为不少人在 OpenHarmony 里也想照搬 Android 混合工程的“把 Flutter 当模块塞进去”的思路。Android 生态里Flutter 最终会打包成 AAR你需要在主工程的 Gradle 脚本里 apply Flutter 的插件脚本。OpenHarmony 完全不认识这套。它的构建系统基于 hvigor应用产物是 HAP独立模块则叫 HAR。OpenHarmony 分支的 Flutter 工具链已经提供了自己的构建插件会在编译时把 Flutter 引擎和三端共享的 Dart 产物一起打进 HAP。环节Android 生态OpenHarmony 生态应用包格式APK / AARHAP / HAR构建系统Gradlehvigor设备连接adbhdcFlutter 模块集成apply gradle 插件hvigor 插件连接设备adb install apkhdc install hap我当时因为目录结构问题误开了一个 Android 子工程构建时直接就撞上了这条报错。删掉那个子工程回到纯 OpenHarmony 的工程结构后问题自然消失。所以碰到这条报错先检查自己是不是把 Android 的混合工程结构误搬进了 OpenHarmony 项目。2.4 判断一个 Flutter 包能不能在 OpenHarmony 上用的清单入坑多了之后我总结了一套判断 Flutter 包是否可用的快速检查法在选型时能省很多时间看仓库里有没有ohos/目录。有原生 ohos 实现的包才会自动注册平台通道看 README 或 pub.dev 的描述里有没有 OpenHarmony 支持的字眼优先选纯 Dart 包。provider、fl_chart、intl、path、uuid 这些都是纯 Dart 实现不依赖平台原生代码OpenHarmony 上直接能用原生类插件要检查它是否被注册到生成插件列表 GeneratedPluginRegistrant 里没注册没适配不要盲目升级到最新版。OHOS 分支的引擎落后上游一两个大版本时新插件很可能不兼容。下表是我项目最终采用的依赖清单全部跑通值得直接抄包名用途平台依赖OpenHarmony 表现provider状态管理纯 Dart完全正常intl日期格式化纯 Dart完全正常fl_chart血糖趋势图纯 Dart完全正常sqflite_common_ffiSQLite 数据库FFI无原生插件完全正常uuid生成唯一 ID纯 Dart完全正常shared_preferences本地偏好存储原生需要找 ohos 适配包存储这块我要多说一句OpenHarmony 的 Flutter 生态里sqflite官方包的原生实现并不稳定我试过几次都有兼容问题。最后选sqflite_common_ffi是因为它走 FFI 调用 SQLite不依赖 Android/iOS 的插件注册机制跨端一致性很好。桌面端、OpenHarmony、Android 一套代码全通这对工具型应用来说是性价比极高的选择。3. 药箱管理的核心药品模型、数据库与过期预警3.1 药品表设计每个字段背后都有理由药品的字段设计直接决定了后续所有功能的实现难度。我最终的表结构如下CREATE TABLE medicines ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT NOT NULL DEFAULT default, stock INTEGER NOT NULL DEFAULT 0, expiry_date TEXT NOT NULL, dosage TEXT DEFAULT , manufacturer TEXT DEFAULT , remind_before_days INTEGER DEFAULT 7, created_at TEXT DEFAULT CURRENT_TIMESTAMP );几个字段的取舍理由值得聊一聊。expiry_date我用了 TEXT 类型存 ISO8601 格式的日期字符串而不是存时间戳。原因很实际SQLite 里WHERE expiry_date 2025-06-01这种字符串比较天然按字典序生效不需要额外的日期函数排序也自然按日期排。而且调试时直接打开数据库看到2025-06-01比看到一长串秒数直观得多。dosage用纯文本而不是结构化字段。药品规格太杂了“一日两次一次一片”、“每次 5ml一天三次”、“顿服”硬要结构化反而会把自己写死。文本录入虽然不“智能”但对用户最友好也最灵活。remind_before_days控制“提前几天预警”。不同药的重要程度不同慢性病常用药可以提前 14 天提醒补货短效感冒药提前 3 天提醒就行。这个字段给了用户灵活性也避免所有药都按同一套规则预警导致提醒疲劳。对应的 Dart 模型class Medicine { final int? id; final String name; final String category; final int stock; final DateTime expiryDate; final String dosage; final String manufacturer; final int remindBeforeDays; Medicine({ this.id, required this.name, this.category default, this.stock 0, required this.expiryDate, this.dosage , this.manufacturer , this.remindBeforeDays 7, }); }仓储层我把它拆成了MedicineRepository所有 SQL 操作都收敛在这个类里页面层不直接碰 SQL。这样做的好处是后面要换数据库引擎或者接后端同步时只需要替换仓储实现页面代码可以完全不动。3.2 过期预警这张“色卡”要放在数据库之外过期状态我没有存库而是放在代码里计算。原因很简单状态依赖“今天”这个动态日期存下来很快会过期。比如你昨天看某盒药还有 8 天过期存了个“warning”状态今天再看其实已经进入 7 天紧急区间了。存库的话必须每天跑定时任务去更新纯属自找麻烦。我的计算逻辑如下enum ExpireStatus { expired, urgent, warning, normal } ExpireStatus getExpireStatus(Medicine m) { final now DateTime.now(); final today DateTime(now.year, now.month, now.day); final expireDay DateTime(m.expiryDate.year, m.expiryDate.month, m.expiryDate.day); final diff expireDay.difference(today).inDays; if (diff 0) return ExpireStatus.expired; if (diff 7) return ExpireStatus.urgent; if (diff m.remindBeforeDays) return ExpireStatus.warning; return ExpireStatus.normal; }颜色规则已过期红色、7 天内紧急橙色、预警区间黄色、正常绿色。在列表项左边加一条 3 像素宽的状态色条用户扫一眼就知道哪些药需要处理不用点开详情。首页我做了三张统计卡药品总数、即将过期数量、已过期数量。这些数据不是独立查表的而是MedicineModel对外暴露的计算属性内部遍历列表即可。列表本身由状态管理维护任何增删改操作后重新加载统计卡自动更新。3.3 入库表单快是第一位校验不能省家庭药箱的录入场景通常是这样的买了一盒新药站在药箱前随手要记一下。这时候如果表单要填 8 个字段、每个都要手动输入用户大概率直接放弃。所以我把交互压缩成最短路径药品名称必填默认聚焦过期日期默认填当前日期加 180 天用户一般只需要在快过期时改一次分类用下拉选择加自定义输入常用分类预设“感冒、止痛、肠胃、外用、慢性病”数量用步进器默认 1。校验逻辑我用 Flutter 自带的 Form TextFormField validator不引额外依赖TextFormField( decoration: const InputDecoration(labelText: 药品名称), validator: (v) (v null || v.trim().isEmpty) ? 药品名称不能为空 : null, )保存时先做表单校验通过后调用仓储层写入数据库再调context.readMedicineModel().refresh()刷新全局药品列表最后Navigator.pop回到列表页。这个小闭环是整个药箱管理跑得顺不顺畅的关键——从打开 App 到录入一盒新药理想状态是十秒以内完成。4. 血糖记录功能的完整实现4.1 需求拆解血糖记录要记录什么血糖记录是整个项目里最有嚼头的部分。市面上血糖记录 App 一大通病是把流程做得很重要注册账号、绑定血糖仪、设置治疗方案、填写饮食日记。我的诉求很简单——家人测完血糖打开 App三秒内完成一条记录。基于这个诉求字段设计刻意做了减法class BloodSugarRecord { final int? id; final DateTime measuredAt; // 测量时间 final double value; // 血糖值统一 mmol/L final String period; // 测量时段 final double? medicatedDose; // 用药剂量可选 final String note; // 备注 }measuredAt默认当前时间也可以手动改因为有人会补记早上忘测的空腹血糖。value是核心数值界面输入时允许小数比如 5.6、7.2。period是一个枚举值转成的字符串分别是空腹、餐前、餐后、睡前、随机。medicatedDose是可选字段糖尿病人服药后记录剂量方便后续观察用药和血糖的关联。note用来记“吃了两碗面”“运动了 30 分钟”这类上下文。为什么要单独突出period这个字段因为糖尿病管理里医生最关注的往往不是单个数字而是“空腹血糖范围”和“餐后血糖控制”这两个维度的对比。有了这个字段我可以实现分组统计、分时段的平均值计算也能在趋势图里用不同颜色区分空腹和餐后数据。4.2 单位换算mmol/L 与 mg/dL 的坑国内血糖仪大多显示毫摩尔每升也就是 mmol/L但有些进口仪器的单位是毫克每分升也就是 mg/dL。如果 App 只认一种单位家里换台血糖仪或者导入外部 CSV 数据时就会出现数字完全对不上的情况。这里有一个必须谨记的换算常数1 mmol/L 18.016 mg/dL。const double kMmolToMgdl 18.016; double toMgdl(double mmol) mmol * kMmolToMgdl; double toMmol(double mgdl) mgdl / kMmolToMgdl;我见过不少简化方案直接用 18反正小数点后差异不大。但实际项目里我踩过一次坑外部导入的数据用的是 18本地存储和展示用 18.016两份数据混在一起后7.8 和 7.75 这种细微偏差在折线图上形成了一条本不存在的“锯齿”。所以我的原则是存储层统一用 mmol/L换算系数固定 18.016显示层再按用户偏好切换单位。天下数据对齐才能谈趋势分析。mmol/Lmg/dL(×18.016)常见含义4.479.3偏低参考线6.1109.9空腹血糖参考上限7.8140.5餐后血糖参考上限11.1200.0糖尿病诊断参考值这套设计也体现在设置页单位切换只是改了显示层的格式化函数数据库里的数值分毫不动。后来我给图表加参考线时才体会到这个决策的价值——无论用户用什么单位图表里的 6.1 和 7.8 参考线都能自动换算不用临时抱佛脚。4.3 记录列表、下拉刷新与分组展示血糖列表页的主要交互是按日期倒序查看记录下拉刷新重新拉取最新数据点击某条记录可以编辑或删除。列表逻辑用RefreshIndicatorListView实现OpenHarmony 分支上对 Material 组件支持良好下拉刷新的回弹动画体验和 Android 上几乎一致。RefreshIndicator( onRefresh: () context.readBloodSugarModel().load(), child: ListView.separated( itemBuilder: (context, index) { final record records[index]; return ListTile( title: Text(record.periodLabel), subtitle: Text(DateFormat(MM-dd HH:mm).format(record.measuredAt)), trailing: Text( ${record.value.toStringAsFixed(1)} mmol/L, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), ); }, ... ), )列表按天分组比纯平铺更直观。我用 intl 的DateFormat(yyyy-MM-dd)把记录按日期聚合同一天的记录合在一个二级标题下再按时间倒序排。这样一眼就能看到“今天测了三次空腹 5.8、早餐后 7.2、晚餐前 6.4”。新增和编辑共用同一个表单页。编辑时用Navigator.push把记录 id 传过去表单页加载时用context.readBloodSugarModel().findById(id)初始化。这一步虽然简单但有个重要的生命周期细节不要在initState里直接context.read后再异步加载数据而不判断状态后面第 5 节我会专门展开讲。4.4 用 fl_chart 画“血糖曲线”近 7 天趋势图记录血糖的最终目的是看趋势而不是盯着单次数值。趋势图我选了 fl_chart 的 LineChart这个包是纯 Dart 渲染在 OpenHarmony 分支上没有任何兼容问题是我测试下来最稳的图表库之一。核心图表配置LineChart( LineChartData( minY: 0, maxY: 12, lineBarsData: [ LineChartBarData( spots: points, isCurved: true, color: const Color(0xFF3B82F6), barWidth: 2.5, dotData: const FlDotData(show: true), ), ], extraLinesData: ExtraLinesData( horizontalLines: [ HorizontalLine(y: 6.1, color: const Color(0xFFF59E0B), strokeWidth: 1), HorizontalLine(y: 7.8, color: const Color(0xFFEF4444), strokeWidth: 1), ], ), ), )extraLinesData里的水平参考线是血糖图最实用的部分空腹参考上限 6.1 用橙色餐后参考上限 7.8 用红色用户一眼就能看出自己的点在参考线之上还是之下不需要懂任何医学知识。实际开发时遇到一个细节如果用户一天测 4 次近 7 天就是 28 个点全部画出来线会非常抖肉眼很难捕捉趋势。我做了两种视图用 Tab 切换记录明细近 7 天的全部原始记录点点的大小按时段区分空腹、餐后用不同颜色日均趋势把同一天的记录取平均值画一条平滑的日均线过滤日内波动突出整体走向。两个视图共用同一个数据源只是对 points 做了不同的预处理。实现下来效果很理想尤其日均趋势能清晰看到某个人一周内血糖是先升后降还是持续偏高给家人看也更容易理解。5. 组件通信与 Provider 状态管理这个项目里的实战用法5.1 为什么药箱这种“小项目”也需要状态管理很多人觉得家庭药箱 App 几十个页面顶天了用 setState 加回调完全够没必要上状态管理。我一开始也是这么做的实际写到第三个页面就后悔了。问题的根源是共享状态太多了。首页的统计卡片依赖药品列表药品列表页的增删改要实时反映到首页统计血糖页的最新一条记录要显示在首页的“最近血糖”卡片上添加页保存后又必须触发列表页刷新。如果全部用 setState 加构造函数回调数据流会从首页一路传到列表页、详情页、添加页中间任何一个页面断了连接刷新就失灵。Provider 解决的是“数据源与页面解耦”的问题。核心思路是让某个对象持有数据并在数据变化时主动通知所有关心它的页面。页面不需要知道数据是哪来的、谁更新的只需要订阅。这个思路对家庭药箱这种多处共享数据的小项目刚刚好不过度设计还足够灵活。5.2 MultiProvider 的组装与仓库拆分项目目录我分成四层模型、仓储、状态、页面各司其职lib/ main.dart models/medicine.dart models/blood_sugar_record.dart repositories/medicine_repository.dart repositories/blood_sugar_repository.dart providers/medicine_model.dart providers/blood_sugar_model.dart pages/medicine_list_page.dart pages/medicine_edit_page.dart pages/blood_sugar_list_page.dart pages/blood_sugar_edit_page.dart pages/blood_sugar_chart_page.dartmain.dart里用 MultiProvider 一次性注册两个模型void main() { WidgetsFlutterBinding.ensureInitialized(); final medicineRepo MedicineRepository(); final sugarRepo BloodSugarRepository(); runApp( MultiProvider( providers: [ ChangeNotifierProvider( create: (_) MedicineModel(medicineRepo)..load(), ), ChangeNotifierProvider( create: (_) BloodSugarModel(sugarRepo)..load(), ), ], child: const FamilyMedicineApp(), ), ); }一个值得讨论的细节是仓储对象放哪里。我选择在 Provider 外部构造再把实例传给 Model。对比在create回调内部直接MedicineModel(MedicineRepository())外部注入的方式让 Model 的构造函数可以接受 mock 的仓储对象后面写单元测试时完全不需要碰 Widget 层。代码多两行但测试友好度高一个量级。以BloodSugarModel为例class BloodSugarModel extends ChangeNotifier { BloodSugarModel(this._repository); final BloodSugarRepository _repository; ListBloodSugarRecord _records []; bool _loading false; ListBloodSugarRecord get records List.unmodifiable(_records); bool get loading _loading; Futurevoid load() async { _loading true; notifyListeners(); _records await _repository.findAll(); _loading false; notifyListeners(); } Futurevoid add(BloodSugarRecord record) async { await _repository.insert(record); await load(); } Futurevoid delete(int id) async { await _repository.delete(id); await load(); } }load()里我调用了两次notifyListeners一次在进入加载态一次在数据就绪后。这样页面可以在加载中显示 spinner而不是全程 white screen。虽然多了一次通知但对用户体验的提升是实打实的。5.3 Watch、Read 和跨页面刷新别再写错的三个用法Provider 的使用有三个高频 API用得不对最容易出问题。context.watchT()是订阅式读取。在build方法中调用它Provider 的值一变化当前 widget 就会重建。首页统计卡就是典型场景override Widget build(BuildContext context) { final model context.watchMedicineModel(); return Row( children: [ StatsCard(label: 药品总数, value: model.medicines.length), StatsCard(label: 即将过期, value: model.expiringCount), StatsCard(label: 已过期, value: model.expiredCount), ], ); }context.readT()是只读不订阅。适合在事件回调里拿数据或触发方法比如按钮点击后调add。ConsumerT是局部重建利器。它只重建自己的 child适合列表项里需要频繁刷新的场景。一个我在实际项目中踩过的坑在 initState 里不能直接使用 context.watch。initState只执行一次后面 Provider 的值再变也不会重新触发watch 没有任何意义。正确做法是在initState用context.readT()触发一次数据加载在build里用context.watchT()订阅变化。另一个坑是异步回调后的mounted判断。我在血糖记录保存成功后习惯直接Navigator.pop某次在低内存 OpenHarmony 设备上测试时保存过程稍慢用户等不及手动返回了结果异步回调执行时页面已经被销毁直接抛异常。Fix 方式Futurevoid _save() async { final model context.readBloodSugarModel(); await model.add(record); if (!context.mounted) return; Navigator.pop(context); }context.mounted是 Flutter 官方提供的安全判断。很多人觉得这是小概率事件但健康记录类 App 恰恰最容易遇到——用户可能随时锁屏、切后台、退出页面异步回调触发的概率远比想象中大。5.4 组件通信的其他方案与选择建议Provider 不是万能的也不该万事都用它。我在这个项目里还用了另外两种通信方式第一种是回调 route 返回值。比如添加药品页保存成功后除了刷新全局状态我还会让Navigator.pop(context, true)返回一个布尔值列表页接收返回值后弹一个轻量的 SnackBar“药品已添加”。这种一次性动作不适合走 Provider因为 SnackBar 只关心这一次保存的结果不关心后续数据状态的变化。第二种是 NotificationListener。我自定义了一个“分类快捷筛选”组件点击分类按钮时向父级列表页冒泡一个事件父级监听后切换列表筛选条件。这种从子到父的轻量事件用 Notification 比 Provider 更简洁因为不需要一个全局对象来承载“当前选中了哪个分类”这种局部状态。事件总线event_bus我也看过但最后没引入。它在跨模块广播场景确实方便但用多了会带来问题全局事件满天飞调试时根本不知道哪里触发了哪里。家庭药箱这种规模的项目引入 event_bus 纯属增加认知负担。我的原则是数据共享用 Provider一次性动作用回调局部事件用 Notification能不引入的库就不引入。6. 真机实测、渲染引擎与发布链路6.1 Impeller 在 OpenHarmony 分支上的实测热搜里 Flutter 相关的高频词有一条是 flutter impeller。Impeller 是 Flutter 新一代渲染引擎核心目的是解决 Skia 的 shader 编译卡顿问题。但这个渲染引擎主要针对 Android、iOS 的渲染管线做了大量优化OpenHarmony 分支的适配滞后于上游支持程度很有限。我在模拟器上强行开启 Impeller 后血糖趋势图出现了可复现的问题fl_chart 的坐标轴在横向拖动时偶发闪烁图表区域时而白块闪现。关掉 Impeller、回到默认 Skia 渲染后问题完全消失。所以我的建议是在 OpenHarmony 上跑项目先别急着开 Impeller。如果你遇到页面卡顿优先排查是不是某个高开销操作阻塞了 UI 线程或者使用了过于复杂的动画叠加而不是指望换渲染引擎一劳永逸。Skia 在 OpenHarmony 分支上的稳定性经过大量社区版本验证没那么差。6.2 摄像头扫描和 HDI二期再考虑先把核心跑通家庭药箱管理 App 经常被问到一个问题能不能扫药品条码自动识别药名这个功能确实有价值但它牵扯到的坑比想象多。OpenHarmony 的 camera 能力接口和 Android 存在差异。Flutter 社区常用的 camera 插件在 OpenHarmony 上没有官方适配真机调起摄像头的稳定性很不理想。要扫条码还得再叠一层条形码解析库整个链路太长一期硬做会拖垮主流程。我的计划是二期走“系统相机拍照 本地 OCR/条码解析”路线或者更朴素一点先让用户在搜索框里输入药名。药箱管理 App 的核心价值是“记录和提醒”不是“扫码识别”把前者做扎实比塞一堆花哨功能重要得多。至于外接血糖仪那就更是硬件层的事了。OpenHarmony 的 HDI 接口负责驱动层抽象普通应用开发者直接调用上层能力接口就行不需要碰驱动代码。如果你团队做的是血糖仪配套 App更值得投入精力的反而是三端数据模型的统一——iOS、Android、OpenHarmony 上的记录格式一致后面做同步和迁移才不会有乱账。6.3 上架前的 XTS 认证与隐私合规健康数据是雷区OpenHarmony 的应用市场认证会过一套 XTS 兼容性测试它检查的不只是“App 能不能正常跑”还包括权限声明是否合理、隐私合规是否到位。健康类应用在这个环节格外严格因为血糖数据属于敏感个人信息。我第一次提交测试时栽了一个小跟头早期为了试验摄像头功能加过 camera 权限后来功能砍掉了权限声明却留在配置文件里。XTS 测试直接报“申请的权限与实际业务场景不符”打回重改。这个教训我记到现在——权限最小化不是口号是硬性门槛。XTS 常见检查项我的处理方式权限声明合理性只保留存储、通知等真正用到的权限隐私政策缺失首次启动弹隐私说明明确“数据全部本地存储不上传”包名、应用名规范按 DevEco 规范配置避免特殊字符版本签名使用 DevEco 的统一签名配置敏感数据说明血糖、用药记录属健康数据页面底部加免责声明医疗健康类内容还有一条必须遵守的底线App 里要明确提示用户“记录仅供参考不构成诊疗建议”。我在设置页和血糖列表页底部都放了一行小字既符合合规要求也避免用户把软件记录当成医生诊断依据。6.4 坑清单整理我踩过的那些“网上查不到”最后把这轮开发里比较有代表性的问题放进一个表给后来人参考现象根本原因解决方式flutter create不认 ohos 参数PATH 里的 Flutter 是官方版不是 OHOS 分支用 alias 锁定分支先flutter --version确认启动闪退日志停在 dart_vm_initializer某个插件没有 ohos 原生实现抛 MissingPluginException看完整日志定位 channel换适配包或纯 Dart 包构建时报 apply flutter gradle plugin把 Android 混合工程结构误搬进 OpenHarmony删掉 Android 子工程用 hvigor 构建 HAP开启 Impeller 后图表闪烁OHOS 分支对 Impeller 支持不完整关闭 Impeller用默认 Skia异步保存后 Navigator.pop 崩溃页面已被销毁回调还在执行if (!context.mounted) return;XTS 报权限过多测试期间加的权限没删干净按“权限最小化”清理配置文件这个项目跑稳之后我的药箱终于从“纸箱里一堆过期待处理”变成了一目了然的列表哪些药快过期、哪些药快用完、家人最近血糖趋势如何打开 App 就能看到。个人最满意的是血糖记录的录入速度家人两三天测一次每次开 App 记录不超过五秒。下一步我打算做两件事一是把过期预警接入系统通知能力让设备在药快过期时主动提醒而不是等用户打开 App 才发现二是把血糖数据做成 CSV 导出月底可以直接发文件给医生参考。这些功能其实都围绕同一个思路——健康记录的终点是“被使用”不是“被存储”。如果你也在折腾 Flutter 和 OpenHarmony 的组合我最想传达的一个经验是这套跨端方案的实际成熟度远超多数人想象但每一个坑都得亲手踩过才能真正摸清版本边界。社区资料虽然不像 Android 那么铺天盖地但只要你掌握了“看 ohos 目录、锁版本、优先纯 Dart 包”这三板斧大部分问题都能自己解决。祝你们跑通第一个 HAP。
返回列表