2026/10/18 15:41:39

Flutter跨端实战:OpenHarmony家庭药箱与体温记录应用开发

Flutter跨端实战:OpenHarmony家庭药箱与体温记录应用开发 家里老人常吃的降压药、孩子上个月发烧剩的退烧药、柜子深处翻出来已经过期的感冒冲剂……这些家庭药品的管理说难不难但真正做起来很多人家里都是一笔糊涂账。再加上日常体温自测的数据今天记在备忘录里明天发到家庭群里时间一长根本没法复盘趋势。我一直在关注Flutter在OpenHarmony生态上的适配进展索性选了一个“小而实用”的场景练手花两个多星期把家庭药箱管理体温记录这个项目从架构做到真机运行完整走了一遍。这个项目具体能解决什么问题最核心的三件事药品有效期能不能在看一眼的界面里就知道哪些快过期了每个家庭成员今天吃没吃药、吃的什么药有没有连续记录体温数据能不能生成一眼能看懂的趋势曲线家人发烧的时候能快速判断指标在好转还是恶化。它适合两类人参考一类是正在折腾Flutter跑OpenHarmony的开发者另一类是打算做既有跨端需求又带本地数据存储的小工具的人。下面按项目推进顺序来写重点放在选型逻辑、核心模块的实现拆解和调试过程中踩过的坑。1. 项目整体设计与技术选型1.1 为什么是Flutter OpenHarmony先说一个结论家庭药箱体温记录本质上是轻量工具型应用页面不多、数据量小、UI偏表单和列表。这类应用的技术选型最怕的不是性能不够而是后续维护成本失控。我最初评估过两条路。一条是用OpenHarmony原生的ArkTS/ArkUI去写好处是跟系统贴合度高通知、权限、后台任务都能直接用系统能力但问题是代码复用性只局限在OpenHarmony这一套生态内部之后想覆盖手机端用户等于要重写一遍。另一条就是用Flutter靠社区维护的OpenHarmony适配分支把Flutter应用编译成hap包跑在OpenHarmony设备上。我最后选了Flutter核心原因是它的渲染引擎自绘UI在跨端时一致性非常高。Dart语言做这类结构化数据管理也顺手async/await处理数据库和通知逻辑很流畅再加上数据层和状态管理的生态成熟直接拿来用就行。更关键的是家庭药箱这种工具类应用对系统原生能力的依赖很浅顶多用个本地通知、本地存储大部分逻辑纯Dart代码就能搞定跨端红利能最大化。当然这不等于说ArkTS方案不行。OpenHarmony上跑Flutter目前还处于不断完善的阶段如果你要做一个深度依赖系统能力、需要大量底层交互的应用原生方案仍然是更稳妥的选择。只是对这个项目而言我的诉求很明确一套代码以后能同时覆盖手机端和OpenHarmony设备省维护成本。1.2 功能架构与数据流设计整体功能可以拆成六个模块优先级从高到低我列在下面模块核心职责优先级成员档案维护家庭成员信息用于区分药品归属和体温记录归属高药品库药品增删改查批次、效期、库存管理高用药记录登记每次服药流水联动扣减库存高效期预警计算剩余天数通过本地通知提醒高体温记录记录体温、生成趋势曲线、异常标色中设置温度单位、通知开关、数据备份入口低数据流非常简单所有操作从UI层触发进入业务层业务层调用数据层的Repository接口最终由数据库引擎落地到SQLite。整个过程不经过网络应用打开即读本地库状态同步靠状态管理框架的Provider机制。消息提醒则由本地通知服务在后台触发。存储方案我直接选了drift这个SQLite ORM没有用sqflite也没用SharedPreferences。原因很简单药箱管理是多表关联的关系型数据药品表、用药记录表、体温记录表、成员表之间都存在外键关系SharedPreferences这种KV结构根本撑不起来。而drift相比sqflite的优势是类型安全、自带迁移机制而且对OpenHarmony的平台适配比sqflite成熟——这一点在我后面真机调试时感受特别深sqflite在OpenHarmony上默认是跑不起来的需要额外适配但drift调整配置后就能正常工作。1.3 状态管理与页面组织状态管理我选了Riverpod理由很实际编译期安全、依赖注入方便、异步状态处理顺手。药箱应用里有大量异步数据库操作Riverpod的FutureProvider可以很自然地管理加载态和错误态不用自己写一堆状态枚举。页面组织上主框架用了三个底部Tab药箱、体温、我的。药箱Tab内是药品列表、分类筛选和效期预警入口体温Tab内是成员切换、最近记录和趋势图我的Tab是成员管理、设置项。所有入口点进详情页和编辑页导航结构不超过两层避免工具类App在页面路径上绕弯子。这里想多说一句工具类应用的状态管理不必过度设计。很多人一上来就搞多个全局Store、复杂的中间件对于一个本地单机应用来说完全是负担。我实际只做了三个核心Provider药品列表、成员列表、体温记录列表临时UI状态比如表单输入、弹窗开关直接用Widget内部状态管理就够了。架构是服务业务的不是用来炫技的。2. 家庭药箱管理核心功能拆解2.1 药品数据模型与表单设计药品数据模型是整个药箱模块的地基我定义的核心字段如下class Medicine { final int id; final String name; // 药品名称 final String? genericName; // 通用名 final String batchNo; // 批次号 final DateTime expireDate; // 有效期 final int stock; // 库存数量 final String unit; // 单位盒/板/片 final String category; // 分类处方药/非处方药/外用 final String? belongTo; // 归属成员 final String? remark; }这里最容易被忽略但特别重要的是批次号。同一种药不同时间买的两盒有效期可能完全不一样如果你把它们合并成一条库存记录就会导致效期预警不准确——一盒还有半年另一盒下个月过期合并后系统只会看到那个更晚的日期风险极大。所以项目里每一批药品都是独立记录库存数量只代表同批次的数量。表单校验逻辑我做了三层药品名称不能为空有效期不能早于今天库存必须是非负数。第三点看着基础但实际使用中非常有用能挡住很多误触操作。条码扫描功能我最后放弃了。原因是很多非处方药盒上的条码信息在本地库查不到有效数据联网识别又需要额外SDK家庭场景下药品数量本身不大手动录入的成本可以接受。与其做一个半吊子的扫码功能不如把手动录入体验做顺。2.2 有效期预警与提醒逻辑效期计算的核心逻辑不复杂但边界处理必须仔细。直接上代码int daysUntilExpiry(DateTime expireDate) { final today DateTime.now(); final expiry DateTime(expireDate.year, expireDate.month, expireDate.day); final now DateTime(today.year, today.month, today.day); return expiry.difference(now).inDays; }这里有个细节要注意difference计算的是毫秒差再转天数如果不把时分秒归零同一天内的不同时间会导致结果差一天出现“明明还没过期却显示已过期”的问题。所以我先把日期归一化到零点再计算保证结果稳定。分级规则我分成四档剩余天数状态UI表现小于 0已过期红色高亮置顶展示0~7 天紧急橙色标签每日提醒8~30 天临近黄色标签列表可见大于 30 天正常默认样式提醒机制用了本地通知每天上午固定时间扫描一次全库把未来7天内到期和已过期的药品汇总成一条通知发出来。同时应用每次启动时也会重新扫描一遍因为用户可能过了好几天才打开App需要保证界面上的预警列表始终是最新的而不是依赖上次通知发完就完事了。2.3 用药记录与库存联动一次用药操作背后其实要做两件事记一条用药流水谁、什么药、几点、剂量同时把对应批次的库存减掉。这两件事必须在一个数据库事务里完成否则就会出现“流水有了库存没扣”的脏数据——这在家庭场景里虽然不至于造成重大事故但这种数据错乱一旦发生后面排查起来非常痛苦。drift的transaction方法在这里派上了用场await db.transaction(() async { await db.insertLog(MedicineLog(...)); await db.decrementStock(medicineId, count); });库存扣到负数时不允许登记并给出明确提示。如果家庭成员不小心登记错了我做了撤销操作撤销时会把库存加回来同时标记原流水作废而不是物理删除——这样保留操作痕迹方便之后查记录时判断到底吃了没吃。用药记录支持按成员和按日期维度查询。老人连续服药的时候打开某个成员的页面就能看到最近一周的服药时间线避免“今天到底吃没吃药”的家庭争吵。3. 体温记录模块的实现细节3.1 体温数据表与录入交互体温记录的表结构比药品简单但设计时也要考虑多成员的场景class TemperatureRecord { final int id; final int memberId; // 关联成员表 final double temperature; // 体温值 final DateTime measuredAt; // 测量时间 final String method; // 耳温/额温/腋下/口腔 final String? remark; }录入交互我花了比较多心思。体温记录这个动作用户往往是单手拿着体温计操作的有些还是在照顾发烧的家人所以录入必须尽量少步骤。体温Tab里直接放了一个大号的快捷录入按钮点开后弹出底部面板默认时间是当前时间只需要输入体温数字一键保存全程三秒以内完成。体温值做了范围校验限制在34.0~42.0摄氏度之间超出直接拦截。这里主要是防误触比如手机放在口袋里不小心点到数字键盘如果没这个范围限制就会产生一条离谱的记录污染趋势曲线。体温异常阈值按年龄段做了区分成人37.3°C以上预警儿童0~6岁37.5°C以上预警。这个数值是经验值不同家庭可以根据自身情况调整但代码里要预留配置入口不要写死。温度单位支持摄氏和华氏切换数据内部统一存摄氏展示时再换算避免存储混乱。3.2 体温曲线绘制实现体温趋势这块我一开始想过用第三方图表库fl_chart但后来决定放弃改用CustomPainter自绘。原因有两个第一第三方图表库在OpenHarmony上的兼容性需要大量验证图表库底层依赖各种手势识别和渲染特性指不定哪个API就失效了第二体温曲线本身需求简单数据量小自绘完全够用而且自绘的可控性更强后面想加预警线、背景渐变、周末分界都方便。绘制流程分五步准备数据取最近N天的体温记录按时间排序。计算坐标系左右留出边距下方留时间轴上方留温度范围。温度范围根据数据动态扩展比如数据里最低36.1、最高39.2就取36到39.5不要把范围撑得太满。画网格线水平线表示关键温度值36°C基线用实线37.3°C预警线用红色虚线方便一眼看出数据是否越线。画折线/平滑曲线用Path连接数据点如果想要平滑效果可以用quadraticBezierTo在相邻点之间做贝塞尔插值视觉比硬折线柔和很多。画数据点并交互正常点用主题色实心圆异常点用红色高亮。点击某个点时通过坐标反查最近的数据点ID弹出该条记录的详情。class TempChartPainter extends CustomPainter { final ListTemperatureRecord records; final double warnLine; override void paint(Canvas canvas, Size size) { // 1. 计算数据点对应的画布坐标 // 2. 画网格线和预警线 // 3. 用Path连接数据点生成折线 // 4. 高亮异常点 } override bool shouldRepaint(covariant TempChartPainter oldDelegate) { return oldDelegate.records ! records; } }实际过程中比较难处理的是点击命中检测。自绘图表不像Widget有现成的onTap事件需要自己把点击坐标换算回数据索引我直接遍历所有数据点坐标计算点击点与目标点的距离取最近的点作为命中项。数据量不大的前提下这办法简单可靠。时间跨度切换做了7天、30天、90天三档用SegmentedButton控制。跨度变大时数据点变多我选择自动隐藏圆点只保留折线避免视觉上挤成一团黑。个人经验30天视图是家庭看护最常用的跨度既能看出感冒病程的起伏又不会因为点太多显得杂乱。4. 踩坑实录与问题排查技巧4.1 环境与构建阶段的坑OpenHarmony上跑Flutter绕不开环境配置问题。我遇到的第一个坑就是SDK版本匹配。Flutter的OpenHarmony适配分支和OpenHarmony SDK之间不是任意版本都能配合版本不匹配时编译过程会报各种莫名其妙的错误有的直接链接失败。解决办法是先确认适配分支文档里声明的版本区间严格按区间装SDK不要图省事拿最新版或者习惯用的老版本充数。第二个坑是插件适配。sqflite在OpenHarmony上默认跑不起来这一点我前面提过。path_provider这类常用插件在非Android平台上也需要确认是否有对应实现。我的处理方式是数据层统一走drift数据库文件路径手动指定到应用私有目录避免依赖某个平台插件的默认行为。第三个坑是包体积。Flutter工程打包成hap包后体积比普通APK大不少Debug包更夸张。轮子哥们常说“包体大是Flutter的原罪”在OpenHarmony上体现得更加明显。实际优化思路是移除用不到的插件依赖尽量不用大而全的第三方库比如自绘图表替代图表库就是这个逻辑。4.2 真机运行与调试的坑真机调试绝对是这个项目里最耗时间的一环把我踩过的坑按顺序捋一遍日志输出。Flutter的print会进设备日志但OpenHarmony设备的日志过滤规则跟Android不太一样。直接在调试工具里看海量日志会漏掉关键信息建议在代码里封装一层带标签的日志函数比如Log.d(药箱, xxx)排查问题时按标签过滤效率高很多。权限申请。OpenHarmony的权限模型与传统Android有差异通知权限如果没有申请到位你会发现药品效期提醒完全静默不弹不响。还有存储权限如果数据库文件路径落在临时目录应用被系统回收后数据直接没。这个在开发初期最容易踩我一度以为是自己代码有问题折腾半天结果是权限没给全。中文输入。文本输入框默认弹出的可能是英文键盘家庭成员名字始终输入不了中文。需要在输入框配置里明确指定中文输入类型这个细节不跑真机根本发现不了。后台保活问题。药箱提醒依赖定时任务但OpenHarmony对后台任务限制比较严格。应用在前台运行时定时器工作正常一旦退到后台长时间不操作定时器可能被挂起到点不提醒。我的兜底方案是每次App冷启动时重新扫描效期并更新通知同时提醒功能不依赖常驻定时器而是用系统能力设置通知通知内容在启动时刷新。这个方案在家庭使用场景下够用——毕竟没人会连续一周不开这个App。模拟器与真机表现不一致。有个比较隐蔽的坑部分模拟器环境对SQLite加密特性的支持跟真机不一样。为了调试方便我这个项目没有开数据库加密真机生产环境如果涉及敏感数据加密这块还需要单独验证。4.3 后续值得继续扩展的方向这个项目做完了核心闭环但有几个方向我觉得很有价值后续可以继续深挖数据备份与导出。家庭数据虽然不需要云端同步但用户换手机或者重装应用时数据就没了。做个本地文件导出导入甚至导出成CSV表格让用户能把体温数据和用药记录拿去给医生看是非常实际的需求。多端共享。家里如果有多个OpenHarmony设备可以走局域网直连做数据同步把主设备的药箱数据推送到平板、家庭中控屏等设备上让家人在不同地方都能看到用药信息。更丰富的图表分析。目前的体温曲线只是基础趋势展示后续可以加月平均体温对比、发烧周期统计配合成员的年龄和性别做更细颗粒度的参考。不过这里要提醒一句任何分析结果都不能替代医生诊断工具类应用一定要明确这个边界。写在最后整个项目做下来我最大的体会是工具类应用千万别为了技术含量堆功能先把核心闭环跑顺比什么都重要。药箱管理加上体温记录单拆出来都是很简单的需求但真正把它们在OpenHarmony的Flutter生态里跑通工程量比想象中大不少。遇到的问题大多不是Dart语言层面的而是平台适配、插件兼容、设备权限这些“工程问题”。也正因为如此走完这一轮我对Flutter在OpenHarmony上的落地状态有了更真实的认知它已经能支撑这类轻依赖应用而且体验相当不错但距离零成本跨端还有一段路要走。最后再分享一个我在社区卫生站跟医生聊天时得到的启发很多家庭其实不缺体温计不缺药缺的是一个持续可靠的记录习惯。工具本身只是降低记录的门槛真正有价值的是坚持记录这件事本身。如果你也想折腾这个项目建议从最小的MVP开始——先能记药、能提醒、能画体温曲线再去想那些锦上添花的功能。