1. 项目拆解:我对这件事的定位理解
“Flutter 框架跨平台鸿蒙开发 - 药品服用记录器开发教程”这个标题,看起来信息量很大,但拆开揉碎其实就三层意思:你手里有一个想跨平台复用的 Flutter 技术栈,你要让它跑在鸿蒙生态里,并且你要用它做个真正能用的药品服用记录器。
先说为什么选这个项目作为切入点。药品服用记录器这个产品形态,功能边界刚好踩在 Flutter 跨平台能力的中线上——它需要 UI 交互、本地数据存储、系统通知能力、文件导出能力,这几样 Flutter 的生态里都有成熟方案;同时在鸿蒙端它又能触及元服务卡片、后台代理提醒这类国产系统特色能力,非常适合用来验证“同一套代码在不同平台的差异化落地”。换句话说,做完这个项目,你掌握的不只是“怎么做一个药盒提醒 App”,而是“一套 Flutter 代码怎么在鸿蒙上体面地活着”。
这个教程适合谁?我建议三类人直接对照:第一类是有 Flutter 基础、想把应用扩展到鸿蒙的开发者;第二类是带着真实需求来的医疗健康类小团队,想快速搭建内部原型;第三类是刚接触跨端开发、想通过一个完整案例理解“工程化适配”思路的新人。如果你纯粹是刷 API 熟练度,这个项目也能用,但你会少掉大半味道——味道都在适配层的取舍里。
药品服用记录器这个品类的核心矛盾从来不是 UI 好不好看,而是“用药安全”四个字。漏服、重复服用、剂量记错,随便一个失误都涉及实际风险,这让项目的技术选型天然倾向保守:数据要稳、提醒要准、操作要能容错。我会在下面每个关键节点都把“为什么这么设计”讲透,而不是只贴代码。
2. 技术选型与鸿蒙化思路
2.1 为什么必须关心 Flutter 的鸿蒙运行时
先说一个很多人忽略的事实:Flutter 在主流移动平台上是直接编译成机器码运行的,但在鸿蒙上目前不是同一条路。鸿蒙对 Flutter 的适配,本质是让 Dart 代码跑在一个特殊的运行时环境里,UI 层通过自绘引擎完成渲染,同时将鸿蒙的原生能力通过一套桥接层暴露给 Flutter 层调用。
这就带来了第一个关键问题:你不能想当然地以为“能跑 Android 的 Flutter 代码改几行就能跑鸿蒙”。因为平台通道(Platform Channel)对应的原生实现、系统服务的调用方式、权限管理模型,在鸿蒙端和 Android 端是两套逻辑。你代码里写的MethodChannel默认是去 Android 侧找实现,到了鸿蒙设备上,缺少对应原生侧注册,调用就会静默失败。
开发前我建议你先确认你的 Flutter 版本和鸿蒙 SDK 版本是否在社区维护的适配范围内。这个适配分支的版本节奏比官方 Flutter 主线通常要滞后一些,所以别一上来就升级到最新 Flutter 版本,否则可能找不到对应的鸿蒙适配产物。我自己习惯的做法是:先锁定 Flutter 稳定分支的某个中间版本,再找该版本对应的鸿蒙适配构建,两者绑定之后再开项目。
2.2 运行时适配与 UI 渲染的边界在哪里
对 Flutter 开发者来说,自适应这块不用过度担忧。鸿蒙环境下 Flutter 的自绘引擎依然是 Skia 那一套,所以你在 Android 上写好的布局、字体渲染、动画曲线,绝大多数能原样呈现。真正要留意的差异点集中在下面几个层面,我列个表方便你对照自查:
| 差异维度 | Android 端习惯 | 鸿蒙端实际表现 | 影响程度 |
|---|---|---|---|
| 页面路由返回手势 | 系统返回键 + 手势 | 返回手势触发逻辑存在差异,部分场景需手动处理 | 中 |
| 系统字体缩放 | 跟随字体缩放设置 | 读取逻辑不同,可能导致文本溢出 | 中 |
| 状态栏高度获取 | 通过 MediaQuery 获取 | 实现方式不同,需在平台层透传 | 低 |
| 权限弹窗样式 | 系统原生弹窗 | 鸿蒙样式,文字表述不同 | 低 |
| 原生控件混合 | 支持 PlatformView | 部分场景支持,性能开销偏大 | 高 |
关于最后一行的 PlatformView,我要多说一句。药品记录器这种工具类应用,理论上你不太需要混合原生控件,这反而是个好消息——纯 Flutter 页面在鸿蒙上的渲染一致性远高于混合视图场景。如果后续你需要在应用里嵌入地图或者原生扫描控件,我建议先把卡片级的场景跑通再做大集成,否则很容易被孤立问题拖住进度。
2.3 项目结构怎么摆才利于跨平台扩展
很多 Flutter 项目到最后撑不住,不是功能写崩了,而是模块边界烂掉了。药品服用记录器虽然体量不大,但我强烈建议你按下面的目录来组织工程:
lib/ ├── core/ # 核心能力:网络、存储、工具函数 │ ├── database/ # 数据库封装 │ ├── notification/ # 通知服务抽象 │ └── utils/ # 日期处理、校验工具 ├── features/ # 页面模块 │ ├── add_medicine/ # 新增药品 │ ├── dashboard/ # 今日总览 │ ├── history/ # 历史记录 │ └── settings/ # 设置页 ├── models/ # 数据模型 ├── services/ # 服务层:服药计划调度、记录写入 └── app.dart # 根组件这套结构的核心思路是“按功能域切分,而不是按类型切分”。开发一段时间后你会发现,和药品相关的数据模型、数据库操作、页面逻辑天然内聚,放在同一个域下面,改动时只需要看这一个目录,排查问题的心智负担会小很多。
还有个容易被忽略的点:服务的抽象层一定要独立出来。比如通知服务,在 Android 上你用系统通知,在鸿蒙上你可能会改用代理提醒,但如果 UI 层直接依赖具体实现,更换时改动面就会很大。预先在core/notification里定好接口,页面只管调用“提醒我吃药”这个方法,具体底层是谁实现,不去关心。这就是跨平台适配的基本功。
3. 核心功能模块设计与实现
3.1 药品数据模型:替用户把抽象信息结构化
药品服用记录器再简单,本质上也是在管理结构化数据。一个最小可用的模型应该包含这些字段:
class Medicine { final String id; final String name; // 药品名称 final String spec; // 规格,如 0.25g/片 final int total; // 总数量 final int remaining; // 剩余数量 final List<DosePlan> plans; // 每日服药计划 final DateTime createdAt; }每一个字段都值得推敲。spec的重要性很多人一开始理解不到——它直接关系到“数量”的换算。比如一盒药 48 片,每次吃 0.5 片,你到底是按“片”扣库存还是按“剂量”扣库存?我建议统一按最小使用单位记账:用户在录入时指定规格,系统自动换算成“每次消耗几片”,后续记录、统计、库存预警都基于这个换算后的片数,否则很容易出现“吃了 3 次药库存还是没变”的尴尬局面。
再比如id字段,千万别用自增整数。这种应用未来大概率要支持多设备同步,或者导出数据后在另一个端重建,字符串唯一标识符(生成随机 ID)在合并场景下远优于自增序号。这个细节你现在看起来不重要,等真要做同步就会感谢当初这个决定。
3.2 服药计划调度:别写死,用配置驱动
服药计划是整个应用逻辑最重的一部分。你要支持一天一次、一天多次、隔天一次、按周期服药等不同模式,如果每种模式都堆条件分支,代码会迅速腐烂。
我设计时采用的方案是“时间点 + 周期类型”二元结构:
class DosePlan { final List<String> timesOfDay; // 例如 ["08:00", "14:00", "20:00"] final RepeatType repeatType; // daily, everyOtherDay, fixedDays final List<int> repeatDays; // 周期类型为 fixedDays 时生效 }这个结构能覆盖绝大部分用药场景。timesOfDay是基础框架,无论什么周期,每天的提醒时间点是由它决定的;repeatType决定“今天到底要不要提醒”。隔天一次的本质就是“以 48 小时为周期循环”,你也可以把它理解为repeatDays为[true, false]的固定周期。
调度逻辑的核心是一个每天触发一次的“日滚动任务”:应用启动时检查今天是否有需要生成的服药计划,有则创建当天提醒,没有就跳过。这个检查不是用定时器每秒轮询,而是基于“设备时区凌晨重置”的监听来实现。在 Flutter 里,TimeZone变化和系统日期变化都有对应监听,你要做的是把它们收敛到同一个“日期变更”事件处理器里。
3.3 服药打卡与容错记录
打卡是用户每天的主动行为,设计上要尽量降低操作成本。我的做法是在今日总览页把“当天所有待服药时段”以卡片列表呈现,每张卡片上有一个大大的按钮,点击即完成打卡。打卡的瞬间只做两件事:写一条记录、更新剩余库存。
但真正考验设计的是“补录”和“误操作”两个场景。
补录:用户上午忘了点,中午想起,希望把上午那次也标记上。这时候如果只提供一个“现在服药”按钮,数据就不准确了。所以我做了“历史时段补记”功能,允许用户选择一个过去的时段进行标记,并在记录里带上实际记录时间和目标服药时间两个时间戳。这样统计报表里就能区分“准时服用了”和“补记的”,不会污染服药依从性分析。
误操作:用户手滑多点了。我的方案是提供一个很短的撤销窗口——打卡完成后显示一条可撤销的提示,允许在 10 秒内撤销操作并回滚库存。这个功能看似小,但用户信任感提升明显,因为老人和照护者都怕“记错了”,给了后悔药大家才愿意放心点。
3.4 通知提醒的两套实现策略
通知是药品记录器的灵魂,也是最体现鸿蒙差异的地方。生态上,Android 端你用flutter_local_notifications这类库就能搞定;但在鸿蒙端,社区适配的可靠性参差,直接依赖插件可能踩到通知不弹的坑。
我建议两条腿走路:
一条是 Flutter 层用本地通知插件,这是默认路径,适合快速验证功能。另一条是鸿蒙原生侧的“代理提醒”能力,通过平台通道方式让 Flutter 调用鸿蒙系统的后台提醒接口。后者有几个优势:系统级调度更省电、应用进程被杀后依然可靠、支持动态生成卡片展示用药信息。
两条策略并存的好处是:即便某台鸿蒙设备对系统提醒支持不完整,你还能回退到 Flutter 本地通知方案,不至于让提醒功能完全失效。这个“能力降级”的思路,在跨平台开发里非常实用——你不应该假定每个平台的能力天花板是一致的,而是要给每个能力准备一个 B 计划。
4. 鸿蒙适配的实操细节
4.1 权限声明与平台通道改造
鸿蒙对权限的管理模型和 Android 有很多相似之处,但声明方式完全不同。Android 在AndroidManifest.xml里声明权限,鸿蒙则需要在其对应的配置文件中声明。这一步如果漏掉,运行时会静默拒绝,你不会收到任何报错,只是功能不工作,排查起来非常费时。
以通知权限为例,你需要做两件事:第一,在配置文件中声明通知相关权限;第二,在应用启动时通过平台通道发起权限请求。这个“声明 + 运行时请求”的双重机制是鸿蒙的规范,和 Android 的动态权限申请思路一致。
平台通道的改造,以“获取应用版本号”为例,代码层面是这样的:
static const _channel = MethodChannel('com.example.medtracker/system'); Future<String?> getVersion() async { return await _channel.invokeMethod('getVersion'); }鸿蒙原生侧注册的是MethodChannel(name: 'com.example.medtracker/system')对应的处理器,能在收到getVersion方法调用时返回版本字符串。这样一套机制并不复杂,但你要记住一个核心原则:业务逻辑不要直接散落平台通道调用,最好统一收敛到一个SystemInfoService里,方便后续维护和 mock 测试。
4.2 数据存储方案:本地优先还是云同步
药品记录器的数据不是海量级,但对“可靠性”要求很高。我认为本地数据库是主存储,云同步是增强能力,这个先后顺序不能颠倒。
本地存储我选用轻量级数据库方案,单表结构,按日期索引。每天产生的记录量大概几十条,性能完全没有压力。关键是把数据库操作封装成仓储接口,比如MedicineRepository、DoseRecordRepository,这样未来无论更换数据库引擎还是引入云同步,都只动仓储层的实现。
云同步是这个项目的进阶话题。如果做,我强烈建议用“本地写优先 + 冲突合并规则”的设计:每次打卡先写本地,再进入同步队列。同步冲突的合并规则可以直接用“按时间戳后写优先”——对服药记录这种弱一致场景足够用,不需要复杂的 CRDT 算法。很多团队在这个阶段过度设计,引入复杂同步方案,反而把系统稳定性拖垮了。
4.3 构建与打包的工程化细节
鸿蒙应用的构建流程和 Android 相似,但有两个核心差异需要提前熟悉。
第一个是签名配置。鸿蒙应用包需要专门的签名文件,开发阶段可以使用调试签名,但发布阶段必须使用正式签名。这个衔接如果没做好,到上架阶段会被打回来重新配置。建议在项目初始化阶段就把签名相关的配置文件纳入版本管理,并让团队成员共享一套签名规范。
第二个是多产物输出。一套 Flutter 代码要同时产出鸿蒙包和 Android 包,我用统一的Makefile脚本管理构建命令,避免“本地能出包、CI 出不来”的经典问题。脚本里把鸿蒙构建工具链的路径、签名文件路径、构建参数全部显式传入,保证在任何环境都能复现同一套构建结果。
5. 常见问题与排查实录
5.1 通知不弹的常见诱因
我做这个项目时遇到最多的问题就是“测试机上通知根本没反应”。排查下来无非三类原因:
一是权限没拿到却已静默失败。这种情况需要你在通知服务里做一个“当前权限状态”的查询方法,并在页面上以状态条形式展示,方便用户自查。
二是应用进程被系统回收。HarmonyOS 对后台进程的管理比 Android 更激进,普通应用默认很难在锁屏后保住进程。解决办法就是走系统级代理提醒通道,让系统来接管提醒调度。
三是时间点格式错误。有些鸿蒙设备对时间字符串的解析比较严苛,"8:00"和"08:00"会被当成不同值,导致提醒计划生成失败。我建议在数据层统一规范化时间格式,存库前先做一次格式化校验。
5.2 Flutter 页面在鸿蒙上的渲染问题
我在测试时还遇到过页面局部闪烁和字体错位,后来定位到原因,是鸿蒙对部分字体的 fallback 规则和 Android 不同。解决方式很简单:显式指定全局字体族,或者在关键文本组件上设置fontFamilyFallback,避免系统猜测你的渲染意图。
另一个坑是横竖屏切换时的布局重建。鸿蒙在部分平板上默认允许自由旋转,如果你的页面没有做自适应,横屏后会出现大面积空白。建议在MaterialApp层通过builder注入一个统一的MediaQuery处理逻辑,把安全区域问题一次性解决,而不是在每个页面单独处理。
5.3 数据库锁死问题
本地数据库偶尔出现database is locked的报错,多数原因是多线程同时写入。Flutter 是单线程事件循环模型,按理说不会有并发写,但你引入 isolate 处理数据导入导出时,就要特别小心了。我的规避方案是:所有数据库操作统一走一个串行队列服务,保证任意时刻只有一个写入者。听起来简单,但很多项目忽略了这个约束,上线后偶发崩溃就找不到原因。
6. 给后来者的心得体会
6.1 从零到一最值得投入的三件事
这个项目从头到尾做下来,如果要我总结最值得投入精力的三个方向,第一是数据模型的抽象能力,第二是平台能力适配的“降级思维”,第三是构建流程的自动化。数据模型决定了你的功能天花板,适配思维决定了你在多平台上的心态——不要总想着把平台差异填平,而是想清楚每个平台的核心竞争力是什么,然后让你的应用去拥抱它。
鸿蒙的元服务卡片就是个很好的例子。药品记录器完全可以做一张桌面卡片,直接展示今日用药进度。你要做的不是把 Flutter 页面硬塞进卡片,而是通过鸿蒙提供的卡片框架,把你已有的数据渲染到桌面上。这种能力在 Android 上做起来成本极高,在鸿蒙上却有天然红利,跨平台开发的价值就在这里体现——同一套业务逻辑,每个平台都能用最适合的方式表达。
6.2 这套项目还能延伸到哪里
药品记录器做完之后,我最大的体会是“健康管理类应用的通用底盘”已经出来了。你换个数据模型,就变成健身打卡;换个业务逻辑,就变成宠物喂食记录;再往深走一步,接入智能药盒的蓝牙数据,就变成物联网应用。底层的本地存储、通知调度、跨平台适配这些骨架都是复用的。
这种复用价值在跨平台开发里常被低估。很多人只盯着“一个功能能不能在两个平台跑”,却忽略了一个更重要的能力:你沉淀出来的工程结构、异常处理模式、构建脚本,才是团队最宝贵的资产。这些抽象的东西不依赖任何具体业务,却能在下一个项目里帮你少踩至少一半的坑。
我自己在做这个项目的过程中,收获最多的并不是熟悉了几个 API 或插件,而是理解了如何站在“平台能力差异”的角度重新审视业务需求。健康类应用尤其如此——每个平台对后台运行、消息推送、桌面小组件的支持力度都不同,真正负责任的产品设计,是在动手编码之前就把这些边界想清楚,然后在实现时优雅地落下去。