大概一个多月前,我接到一个需求:给家里老人做一款家庭药箱管理App,既想跑在Android手机上,又得适配国产的OpenHarmony开发板。考虑到项目节奏比较紧,我选了Flutter作为跨平台框架,配合flutter_for_openharmony这个适配方案来做UI和业务层。整体做下来,最大的感受是:Flutter跑在OpenHarmony上的路子已经能走通了,但和标准Android开发比起来,坑还是不少,尤其是插件适配和原生权限这块。这篇博文就把我这次从零搭建“家庭药箱管理App”的完整过程写出来,重点拆解设置功能模块的实现思路,给后面想踩这条路的同学一个参考。
如果你正在考虑“Flutter能不能上OpenHarmony”“设置页这种偏系统的功能该怎么做”“药品数据怎么存怎么提醒”,那这篇文章应该能帮上忙。里面没有照抄文档的东西,都是我实际跑通过的代码路径和踩过坑之后的修正方案。
1. 项目整体设计与技术选型思路
1.1 为什么选Flutter跑OpenHarmony,而不是直接上ArkUI
这个项目最开始有个硬约束:同一个App要贴在两个平台上。老人手机上跑Android,家里的智能药箱设备用的是OpenHarmony的RK3566平台。如果各写一套UI逻辑,后续维护成本直接翻倍。所以跨平台方案是刚需。
Flutter在Android、iOS上已经很成熟,但OpenHarmony并不是Flutter官方支持的构建目标。好在社区有flutter_for_openharmony这个适配项目,它做的事情简单说就是把Flutter引擎编译到OpenHarmony的NAPI体系上,让Flutter应用可以以hap包的形式运行。这意味着我可以用完全同一套Dart代码去构建Android APK和OpenHarmony的HAP包。
那为什么不选ArkUI?如果只做OpenHarmony一个平台,ArkUI是更顺的选择,毕竟是系统原生的声明式UI框架,性能和控制力都更好。但问题是时间不允许我重写两套业务逻辑。这次项目选择Flutter不是因为它比ArkUI强,而是因为它让团队可以用一套代码吃下两个平台,而且flutter_for_openharmony的社区活跃度一直不错,踩坑时能找到不少人一起讨论。
1.2 家庭药箱管理的核心需求拆解
接到需求后,我没有急着写代码,先把用户场景列了一遍。家里主要使用者是老人,操作要少、字要大、提醒要显眼。药箱里存的药要能快速定位,过期药要提前预警,降压药之类需要每天定时吃的药不能漏。
拆解下来核心功能就四块:
- 药品档案管理:药名、规格、数量、生产日期、有效期、用法用量、药品照片
- 过期/余量提醒:药品快过期时推送通知,余量小于阈值时提示补充
- 服药计划:针对每天定时服用的药品,设置吃药时间并触发提醒
- 家庭成员与设置:切换家庭成员、管理提醒方式、备份数据等
技术上的难点集中在两块。第一是数据持久化,要同时兼容Android端和OpenHarmony端,不能依赖平台相关性太强的存储接口。第二是本地通知调度,Android上可以用flutter_local_notifications,但OpenHarmony上未必有对应插件,得想别的办法。这两块在整个项目中都磨了很久,后面会详细讲。
1.3 关于设置模块的定位
设置功能在产品里非常容易被轻视,但实际做下来,它反而是连接系统能力和用户偏好的枢纽。这次家庭药箱App里的设置模块承载了几类关键任务:
- 通知开关总控:整个App的提醒中枢,关掉后所有服药提醒、过期提醒都停止
- 默认提醒时间:设置每天提醒的起始时间段,避免半夜被打扰
- 数据管理:备份药品列表到本地文件、从备份恢复数据、清空所有数据
- 关于与帮助:版本号、开源许可、操作指引
这些功能看起来不起眼,但每一条都和系统级能力绑定。比如通知开关要读写系统通知权限,备份要处理文件存储路径,版本号要读取原生包信息。在OpenHarmony上,这些系统能力API和Android不是完全一致的,所以在设置页面里会集中遇到大量平台适配问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与OpenHarmony适配痛点
2.1 flutter_for_openharmony工程怎么配
先把环境整理清楚。我是在macOS上做的开发,宿主Flutter用的是官方稳定版,同时拉了一份flutter_for_openharmony的fork分支作为OpenHarmony构建工具链。
实际步骤大致是这样的:
- 准备OpenHarmony SDK,版本选的是API 9。这个版本对flutter_for_openharmony来说兼容性较好
- 把flutter_for_openharmony的flutter仓库clone下来,用里面的flutter命令替代官方flutter
- 配置DevEco Studio,用于构建hap包,需要设置好OpenHarmony SDK路径
- 创建Flutter工程后,在工程目录下执行flutter create --platforms ohos .来生成ohos平台目录
- 用DevEco Studio打开ohos目录,配置签名后直接构建hap
关键点在于:如果你已经在用官方Flutter写了业务代码,切换到flutter_for_openharmony构建时,Dart层代码基本不用动,但依赖Flutter SDK内部能力的部分需要重新编译。比如我用了image_picker插件,发现它在OpenHarmony上并没有直接对应的实现,还需要找支持OpenHarmony的插件分支。
注意:flutter_for_openharmony并不是Flutter官方仓库,安装SDK版本、插件版本都有一定滞后性。进入项目前最好先去他们GitHub的release页面确认版本兼容关系,避免开发到一半引擎API变了。
2.2 Mac上RK3566设备调试怎么连
我这次的目标设备是RK3566的OpenHarmony开发板,跑的是OpenHarmony 3.2 release版本。这东西不像手机插上USB就能adb devices,需要做几步特殊配置。
首先,开发板要开启USB调试模式。OpenHarmony的开发者选项里有一个“USB调试”开关,但如果你的系统镜像里没有内置开发者设置,可能需要通过修改系统参数方式打开。我翻了不少资料最后是通过执行param set persist.hdc.mode 1这样的操作来启用的。
其次,hdc(OpenHarmony Device Connector)是替代adb的工具。路径一般在DevEco Studio的SDK目录下,比如/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc。连接开发板后要先用hdc tconn 192.168.x.x:5555做网络连接,而不是直接用USB。这个和Android的开发习惯差异很大,刚开始我在这上面卡了小半天。
最后是hot reload。在flutter_for_openharmony环境下,flutter run -d ohos可以做到热重载,但速度明显比Android端慢。而且如果改了原生代码,必须重新构建hap包安装,不能指望增量同步。这和标准的Flutter体验还是有差距的,建议开发时把UI调试放到Android模拟器上做,OpenHarmony真机只跑集成验证。
2.3 插件兼容性排查思路
这个项目里我依赖了几个常用插件:sqflite、shared_preferences、flutter_local_notifications、image_picker。在Android上这些都是标配,但在OpenHarmony上,情况变成了这样:
- shared_preferences:有对应支持,flutter_for_openharmony社区的插件仓库里能拉到一个shared_preferences_ohos实现,基础读写没问题
- sqflite:没有直接可用的版本。OpenHarmony的数据库体系是关系型数据库(RDB),API风格和SQLite不同。好在RDB底层是SQLite,我后来是直接通过数据库操作封装类来做转换,原生侧用RDB实现,Dart侧暴露统一的接口给业务层调用
- image_picker:社区有适配版,但版本较老,部分接口参数不兼容,需要二次封装
- flutter_local_notifications:这是最麻烦的一个。OpenHarmony的通知服务API和Android差异非常大,社区没有成熟适配,最后只能自己写平台通道调原生接口
所以这个项目我做了很多“Dart侧统一接口 + 各平台原生实现”的适配工作。本质上和开发一个插件的思路是一样的:在Dart层定义好抽象接口,在ohos目录下用ArkTS/Java写平台实现,再通过MethodChannel传递数据。后面的设置模块里,通知开关就是完全基于这套机制做的。
3. 家庭药箱核心业务功能实现
3.1 数据模型与本地存储方案
我先说说药品数据的整体设计。药品对象的核心字段如下:
- id:唯一标识,自增主键
- name:药品名称
- spec:规格,比如“5mg*28片”
- dosage:每次用量,比如“1片/次”
- frequency:服用频率,比如“每天早饭后”
- stock:当前库存
- expireDate:有效期截止日期
- notifyEnabled:是否参与提醒
- notifyTime:提醒时间,只在有定时服药需求时才非空
存储方案上,Android端用了sqflite,OpenHarmony端用了系统RDB。为了让业务层不感知差异,我做了一个DatabaseHelper抽象类,暴露init、insertMedicine、queryMedicines、updateMedicine、deleteMedicine这几个方法。每个平台写对应的实现,再通过工厂方法返回实例。
实际遇到的坑是OpenHarmony RDB的并发能力限制比较大。多线程同时写时会遇到database is locked,而且RDB的默认线程池不会帮你排队处理。我的解决方式是做一个全局的单写者队列,所有写操作串行执行,读操作独立走只读实例。这样虽然损失了一点并发性能,但家庭药箱这种低频数据量场景完全够用,而且稳定可靠。
3.2 药品录入与过期计算逻辑
药品录入页的UI没什么特别,就是表单加相机拍照。真正花心思的是过期时间的计算逻辑。按照需求,药品在过期前7天和过期当天都要提醒,而且如果药品已经过期,列表里要用红色标签醒目展示。
我在Dart层写了一个MedicineStatusHelper,核心就两个静态方法:
dart复制static int daysUntilExpiry(DateTime expireDate) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final expire = DateTime(expireDate.year, expireDate.month, expireDate.day);
return expire.difference(today).inDays;
}
static MedicineStatus getStatus(int daysUntilExpiry) {
if (daysUntilExpiry < 0) return MedicineStatus.expired;
if (daysUntilExpiry <= 7) return MedicineStatus.expiringSoon;
return MedicineStatus.normal;
}
这里有个容易忽略的点:如果用DateTime.now()直接和expireDate做difference计算,会因为时分秒的差异导致结果差一天。必须先都规整到当天零点再算。这个问题我一开始没注意,导致测试时明明离过期还有7天却显示已过期。
另外药品照片的存储路径也要考虑跨平台。Android上我统一存到应用私有目录下的images文件夹,用药品id做文件名。OpenHarmony上要考虑到应用沙箱路径不同,但通过path_provider获取到的目录在flutter_for_openharmony环境下也能正常使用,只需要验证一下路径拼接逻辑。
3.3 服药提醒的本地通知方案
这是整个项目里最折腾的一块。Android端我可以直接集成flutter_local_notifications,用zonedSchedule实现每天定点通知。但OpenHarmony上这个插件没有对应实现,只能走原生通道。
OpenHarmony的通知API是@ohos.notificationManager,使用方式大概是这样的:
typescript复制import notificationManager from '@ohos.notificationManager';
let notificationRequest = {
id: 101,
content: {
contentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
text: '该吃降压药了',
title: '服药提醒'
}
};
notificationManager.publish(notificationRequest, (err) => {
if (err) {
console.error('publish notification failed: ' + JSON.stringify(err));
}
});
但这里有个关键限制:OpenHarmony的通知要弹出来,应用必须申请通知授权。API 9上需要用户手动在系统设置里打开通知权限,应用侧能做的只是跳转到通知设置页。而且系统版本不同,判断权限的方法也不同,有的用notificationManager.isNotificationEnabled,有的需要配合requestEnableNotification。这块我花了不少时间调兼容。
定时触发任务这块,我用的是AlarmAbility,注册好之后在服务端通过回调发送通知。但这套逻辑写出来比较长,而且OpenHarmony对后台任务的限制比Android更严格——息屏一段时间后,定时任务可能被挂起。所以我的方案是:在应用打开时用Timer做提醒检查,同时把下一个提醒时间注册给系统AlarmAbility,作为兜底。双保险虽然代码复杂度高了点,但可靠性明显提升了。
4. 设置功能模块的深度解析
4.1 设置模块的整体界面设计
和许多工具类App一样,我把设置功能做成了列表页。但和普通App设置不同,家庭药箱的受众是老人,所以我特别调整了交互细节:
- 列表高度加大到56dp以上,整体字号统一放大
- 开关状态用文字+颜色双重表达,不只靠开关本身的颜色变化
- 提供“初始化向导”入口,方便家人为老人首次配置时快速走完流程
实际页面结构拆成四组:
- 提醒设置组:全局通知开关、每日提醒开始/结束时间、过期提前天数
- 数据管理组:备份数据、从备份恢复、清空药品数据
- 账户与同步组:家庭成员姓名、家庭成员头像
- 通用组:版本号、开源许可、用户指引
4.2 通知总开关与权限联动
这是设置模块里最核心也最容易被用户骂“App没声音”的功能。我的实现思路是:设置页里的通知开关显示的是当前系统通知授权状态,而不是App内部自定义的状态。用户点击开关关闭通知时,实际上是引导到系统设置页去关闭权限。
Dart层的代码很简单,通过MethodChannel调用原生判断:
dart复制static Future<bool> isNotificationEnabled() async {
const channel = MethodChannel('family_medicine/notification');
final bool enabled = await channel.invokeMethod('isNotificationEnabled');
return enabled;
}
static Future<void> openNotificationSettings() async {
const channel = MethodChannel('family_medicine/notification');
await channel.invokeMethod('openNotificationSettings');
}
Android端实现是读取NotificationManagerCompat.areNotificationsEnabled(),然后用Intent跳转到应用通知设置页。OpenHarmony端则是用notificationManager.isEnabled()或者isNotificationEnabled()来判断。
一个非常值得注意的点:在Android 13以上,通知权限弹窗是高危权限,而且不同手机厂商管理页面位置不一样。我测试时发现小米手机的系统设置页面和其他手机不一样,如果直接跳ACTION_APP_NOTIFICATION_SETTINGS可能只到应用详情页而不是通知页。所以如果只是想提高成功率,可以直接跳应用详情页,至少用户知道从哪里进。
4.3 每日提醒时间段的策略实现
需求里有一条:“提醒时间要能设置一个时间段,只在时间段内提醒”。这个设计很聪明,避免老人晚上休息时被吃药提醒打扰。
我的实现策略是:提醒开关只在这两个时间点之间有效,每次提醒触发时都要检查当前时间是否在允许范围内。过期药品提醒则不受这个时间段约束,因为过期状态是一个紧急事件,需要随时提醒。
具体代码是这样的:
dart复制bool shouldNotifyNow({required DateTime now, required int startHour, required int endHour}) {
int currentMinutes = now.hour * 60 + now.minute;
int startMinutes = startHour * 60;
int endMinutes = endHour * 60;
if (startMinutes == endMinutes) return true; // 全时段
if (startMinutes > endMinutes) {
// 跨天场景,比如晚上九点到第二天早上七点
return currentMinutes >= startMinutes || currentMinutes <= endMinutes;
}
return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
}
跨天场景容易忽略。如果用户设置了22:00到06:00这个时间段,凌晨1点也算有效提醒时间。虽然家庭药箱场景下很少会这样设置,但作为通用设置功能,还是要考虑到。
4.4 数据备份与恢复的设计
家庭药箱的药品数据不算复杂,所以备份方案我采用了最直接的方式:把数据库导出成JSON文件,存到用户指定目录;恢复时读取JSON,重建数据库。
备份的JSON格式大概是这样的:
json复制{
"app": "family_medicine",
"version": 1,
"exportTime": "2025-01-15T10:30:00",
"medicines": [
{
"id": 1,
"name": "阿莫西林",
"spec": "0.25g*24粒",
"stock": 12,
"expireDate": "2026-03-01"
}
]
}
导出文件时我用了一个比较稳妥的方式:先写到应用缓存目录,再通过FilePicker让用户选择最终保存位置。不能直接把路径写死,因为Android 10以后对公共目录写文件需要申请存储权限,而让用户选择位置可以绕过这个限制。
恢复过程的容错也很重要。如果用户选了一个损坏的JSON文件,不能直接把数据库清掉重来。我先做格式校验,确认必填字段都存在后,再把原数据库备份一份,最后才执行恢复导入。这样就算中途崩溃,用户也能用那份备份恢复原样。
4.5 版本号与关于页面
版本号这个看起来简单的事情,在两个平台上也折腾了一下。Android端可以用package_info_plus拿到版本号;OpenHarmony端没有现成的插件实现,我是手动从原生侧读取bundleName和versionName参数,通过MethodChannel传给Dart层。
关于页面里有一项“开源许可”,这个对Flutter项目很关键。因为Flutter引擎和插件都有自己的License,常规做法是集成flutter_showlicensepage。但这个插件在flutter_for_openharmony上也是不支持的,得另想办法。我最后是直接写死了一个简版的许可证列表页,列出主要依赖和对应的开源协议,没有做动态生成。
注意:如果你面向的OpenHarmony设备要过应用上架审核,开源许可页面不能省。很多开发者忽视这个细节,最终被驳回要求补充。
5. 遇到的高频问题与解决实录
5.1 Flutter包体积在OpenHarmony上暴增
第一个让我意外的是HAP包的体积。同样的Flutter代码,Android APK大约是28MB,但OpenHarmony的HAP包直接冲到了82MB。查下来发现是因为flutter_for_openharmony把引擎相关的so文件、依赖库都塞进了包里,而且没有做按ABI裁剪。
解决方案是在DevEco Studio的build-profile.json5里配置abiFilters,只保留arm64-v8a。因为RK3566是64位架构,不需要armeabi-v7a。配完之后HAP包缩到了52MB,还是有浪费,但已经能接受了。
5.2 setting页面在OpenHarmony上文本显示模糊
真机调试时发现设置页面的文本在RK3566上有明显的模糊感,检查后确认是把Flutter UI跑在OpenHarmony的离屏渲染路径下,分辨率适配出了问题。
解决方式是在Flutter引擎初始化时强制指定逻辑分辨率,通过设置Device Pixel Ratio来对齐OpenHarmony的密度。大致是在MainAbility里创建FlutterSurfaceView后,调用setScaleType并配置合理的DPR。这个问题印象很深,因为不解决的话整个设置页根本没法看。
5.3 通知权限判断版本兼容问题
OpenHarmony的API 9和API 10在通知权限的API名称上发生了变化。API 9里是isNotificationEnabled(),API 10里增加了isEnabled()实例方法,并且推荐用notificationManager.isEnabled()判断当前应用的授权状态。如果直接照着API文档写,很容易出现编译报错。
我的做法是在原生代码里做版本判断,通过canIUse接口检测当前系统是否支持新的API,然后走不同的分支。这种方法虽丑但兼容性最好。
5.4 设置页开关状态和系统权限不一致
这是用户反馈最直接的问题:“通知开关明明是开的,可就是没有提醒”。排查后发现是因为设置页开关显示的是App内部保存的偏好值,而系统权限可能已经被用户在系统设置里关掉了,两边不同步。
所以最后我把通知开关改成了只读展示系统权限状态的模式。进入设置页时每次重新读取系统通知权限状态,如果用户点击开关,要么跳转系统设置,要么通过弹窗引导。这样彻底消除了“开关开了但没提醒”的认知偏差。
6. 测试与上架注意点
6.1 OpenHarmony设备兼容性测试
我做测试时手上只有RK3566开发板,但OpenHarmony还有RK3588、Hi3516等不同硬件平台。不同芯片对应的系统镜像可能不同,API版本也可能有细微差异。我的建议是:
- 优先测试API 9和API 10两个版本
- 分辨率和DPR适配要做多种设备验证
- 通知权限逻辑在所有目标设备上都要人工确认一遍
另外OpenHarmony的屏幕旋转行为在某些设备上是锁定的,导致Flutter设置页横竖屏切换时会出现布局闪动。最后我在配置文件里锁定了竖屏,避免这个问题的干扰。
6.2 上架前注意审核规范
OpenHarmony应用上架前一般要补齐这些材料:隐私政策、权限声明、开源许可、用户体验计划。其中权限声明最容易漏。家庭药箱App需要申请的通知权限、存储权限都必须在隐私说明里明确告知用户用途,并且在首次启动时弹窗申请。
我还遇到一个审核问题:App被要求提供“注销账号”入口。因为家庭药箱的账户体系只是本地家庭成员名称,不涉及服务端账号,所以这个需求我通过一份“本地数据删除指南”来应对,在数据管理页提供了一键清空数据按钮并说明这是等效的注销操作。
6.3 三方库Licenses自查
既然用了第三方插件,就要把License文件收集全。我在项目里新建了一个licenses目录,手动整理了几份关键依赖的License,包括sqflite、shared_preferences、image_picker,还在关于页面里放了完整列表入口。别看这个工作不起眼,等真正要交付时就会庆幸自己提前做了。
7. 个人实际开发感受与进一步可做的事
这次项目做下来,我对flutter_for_openharmony的成熟度有了一个比较清晰的判断。作为社区适配方案,它的Dart层兼容性做得很好,常规的UI、状态管理、数据逻辑代码基本不需要改动。但凡是牵涉到系统能力的,比如通知、权限、文件存储、后台任务,都要做好自己写平台通道的心理准备。这个项目的周期要留足,不能按标准Flutter跨平台开发的时间来估算。
设置功能虽然是“辅助功能”,但这个项目里它反而成了最考验平台适配能力的模块。每一条设置项背后都链接着一个系统API,而这些API在Android和OpenHarmony上的差异,比UI层明显得多。如果你也在做类似的跨平台应用,建议把设置模块放到中期去做,不要留在最后才发现某个关键能力在目标平台上根本不支持。
最后分享一个小经验:项目里所有平台相关的调用,无论是通知、备份还是权限判断,我都统一封装到了Dart层的PlatformBridge类型里,每一个方法都同时提供Android和OHOS实现。业务层永远只面向抽象接口编程。这套做法让平台切换时不需要改业务代码,后面如果OpenHarmony生态继续完善,直接把原生实现换成官方插件,整个项目的替换成本也极低。
如果你想在这个项目基础上继续扩展,我建议下一步可以尝试接入账号同步和云端备份,把家庭成员的药箱数据做多端同步。这块的存储方案就要重新规划了,但前面搭好的Dart侧抽象层还能继续复用,不会白做。
