1. 项目背景与设计思考
我一直觉得“家庭药箱”这个场景特别适合做成跨端应用。家里药箱里的药,品类多、效期乱、偶尔还会出现“药还没吃完就过期”的情况,记录需求是真实存在的。正好那段时间我在用 Flutter 做跨端项目,手头又有一块 OpenHarmony 开发板,就想着把 Flutter 应用搬到 OpenHarmony 上跑一遍,于是就有了这个 flutter_for_openharmony 家庭药箱管理 App。
这个项目的完整形态可以概括为:用 Flutter 编写业务代码和 UI,通过社区适配层打包成 OpenHarmony 应用,最后在开发板上运行,核心功能覆盖药品入库、效期预警、用药提醒,以及一个完整的设置功能模块。如果你正在做类似方向——不管是想了解 Flutter 在 OpenHarmony 上能跑成什么样,还是准备开发一款家庭健康管理类应用,这篇内容都值得看完。
1.1 家庭药箱到底需要哪些能力
先不急着写代码,把需求理清楚。家庭药箱管理如果只做“药品名称+过期日期”的表格,用 Excel 就够了,做成 App 的价值在于实时性和提醒能力。我在需求分析阶段整理出的功能点如下:
- 药品信息管理:名称、规格、剂型、用途、用法用量、总数量、剩余数量、生产日期、有效期、备注。
- 效期状态识别:区分正常、临期、已过期三种状态,并在列表页直观展示。
- 定时提醒:按药品设置用药提醒,或者按家庭成员设置每日固定提醒。
- 数据导出:把药箱数据导出成 JSON 文件,方便备份或者迁移。
- 设置模块:通知总开关、默认提醒时间、主题模式切换、缓存清理、关于页面。
这里我想多说一句:家庭药箱应用最核心的体验不是“录入”,而是“提醒”。很多用户录完一次药品之后,可能一个月都不会再打开 App,真正触达用户的点在于临期提醒和用药提醒。所以设计的时候一定要把通知能力放到优先级最高的位置,而不是把精力都花在列表动画上。
1.2 为什么选择 Flutter 对接 OpenHarmony
选技术方案的时候我对比过两条路:一条是用 ArkTS + ArkUI 原生开发 OpenHarmony 应用,另一条是走 Flutter 适配层。我当时没有直接选 ArkTS,原因很现实:
- 团队里 Flutter 的代码资产最多,现成的状态管理、路由、UI 组件体系可以直接复用。
- Flutter 是自绘引擎,UI 在 Android、iOS、OpenHarmony 上的一致性很好,界面不会因为底层系统差异走样。
- Dart 生态对本地存储、JSON 处理、文件读写场景足够成熟,开发效率比从零写 ArkTS 要高。
Flutter 对接 OpenHarmony 的适配链路,目前主要是 OpenHarmony 社区维护的 flutter_flutter 分支,里面包含了 OpenHarmony 平台侧的 Runner 工程。这套适配不是把 Flutter 当作 WebView 去套壳,而是真正把 Flutter 引擎跑在 OpenHarmony 系统上,Dart 层代码通过 Platform Channel 调用 OpenHarmony 的能力。
当然,选 Flutter 也有代价。OpenHarmony 适配层的插件生态没有 Android 那么全,部分插件无法直接使用,需要自己写平台通道,或者换成纯 Dart 实现的第三方库。这个我心里有预期,所以项目规划时给适配预留了时间。
1.3 技术选型的取舍
如果你也在犹豫要不要用 Flutter 做 OpenHarmony 应用,我建议按这个逻辑判断:
- 如果应用强依赖系统级能力,比如蓝牙、NFC、USB、传感器这类硬件接口,就需要先查一下 OpenHarmony 侧 Platform Channel 有没有现成实现。社区没覆盖的话,自研成本不低。
- 如果应用只是常规界面 + 数据存储 + 网络请求,Flutter 的适配风险很小,可以放心用。
- 如果团队已经有 Flutter 开发经验,那更不建议轻易推到 ArkTS 重写。一套代码可多端运行的价值,在家庭药箱这类工具型应用里能明显体现出来。
我这个项目的系统能力依赖主要是本地通知、文件读写、偏好设置存储,这三块在 OpenHarmony 上都有可用的适配方案,所以整体选型是成立的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与工程结构
2.1 OpenHarmony 环境与 Flutter 适配层
先讲环境配置。OpenHarmony 的 Flutter 适配目前以社区维护为主,常见的做法是直接拉 flutter_flutter 的 OpenHarmony 分支,而不是用 pub.dev 上默认的 Flutter SDK。默认 SDK 不包含 OpenHarmony 平台目标,创建工程时不会生成 ohos 目录。
我使用的版本组合可以参考:
| 组件 | 版本说明 |
|---|---|
| Flutter SDK | 社区 OpenHarmony 适配分支(基于 3.x 版本线) |
| OpenHarmony SDK | 4.0 Release,API 9 及以上 |
| DevEco Studio | 用于编译 ohos 工程并导出 HAP 包 |
| hdc 工具 | 连接开发板、查看日志、安装应用 |
编译链路大致是:Dart 代码经过 Flutter 引擎,映射到 OpenHarmony 平台层,最终由 DevEco Studio 编译成 HAP 包安装到设备上。需要特别注意的是,OpenHarmony 的 Flutter 工程最终是通过 DevEco Studio 打开 ohos 目录来构建的,不是直接跑 flutter run 就能在开发板上看到效果。
2.2 工程初始化
我的初始化步骤记录如下,方便直接照着操作:
bash复制# 拉取 OpenHarmony 适配版 Flutter SDK
git clone -b dev_oh_flutter https://gitee.com/openharmony-sig/flutter_flutter.git
# 配置 PATH
export PATH=$PWD/flutter_flutter/bin:$PATH
# 检查环境
flutter doctor
flutter doctor 会输出 Flutter 基础状态,但 OpenHarmony 平台不会被自动识别,需要手动确认工程里有 ohos 目录。接着创建工程:
bash复制flutter create family_medicine_box
cd family_medicine_box
创建完成后,适配版的 Flutter 模板会自动生成 ohos 目录,如果没有生成,可以通过社区脚本手工补上。然后用 DevEco Studio 打开 ohos 目录,等待工程同步完成,就能看到可编译的 OpenHarmony 应用工程。
这里有个细节:flutter create 默认生成的是标准多平台工程,如果 ohos 目录缺失,不要急着去手写整个工程结构,先在社区仓库里查一下对应版本的工程模板,直接复制过来改包名成本更低。
2.3 项目目录结构规划
工程结构上,我按功能模块拆分,这样便于后续扩展和维护:
code复制lib/
main.dart
pages/
home_page.dart
medicine_list_page.dart
medicine_edit_page.dart
reminder_page.dart
settings_page.dart
models/
medicine.dart
reminder.dart
services/
db_service.dart
notification_service.dart
settings_service.dart
widgets/
medicine_card.dart
empty_view.dart
utils/
date_utils.dart
models 目录放数据实体,services 目录放业务逻辑和跨端能力封装,pages 目录放页面 UI,widgets 目录放可复用组件。这样的分层有一个直接好处:后面换成不同的数据库实现,或者调整通知方案,只要改 services 里对应的类就行,UI 层完全不用动。
3. 家庭药箱核心功能实现
3.1 数据模型与本地存储方案
先定义药品数据模型。家庭药箱的数据字段比较多,我在设计的时候做了两类划分:必填字段和选填字段。必填字段是名称、数量、有效期,其他都算选填。为什么有效期是必填?因为整个 App 的核心价值就是效期管理,如果没有有效期,这个应用跟普通备忘录没有区别。
药品模型代码如下:
dart复制class Medicine {
int? id;
String name;
String spec;
String dosage;
int totalCount;
int remainCount;
DateTime productionDate;
DateTime expireDate;
String? manufacturer;
String remark;
Medicine({
this.id,
required this.name,
this.spec = '',
this.dosage = '',
required this.totalCount,
required this.remainCount,
required this.productionDate,
required this.expireDate,
this.manufacturer,
this.remark = '',
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'spec': spec,
'dosage': dosage,
'totalCount': totalCount,
'remainCount': remainCount,
'productionDate': productionDate.toIso8601String(),
'expireDate': expireDate.toIso8601String(),
'manufacturer': manufacturer,
'remark': remark,
};
}
factory Medicine.fromJson(Map<String, dynamic> json) {
return Medicine(
id: json['id'] as int?,
name: json['name'] as String,
spec: json['spec'] as String? ?? '',
dosage: json['dosage'] as String? ?? '',
totalCount: json['totalCount'] as int,
remainCount: json['remainCount'] as int,
productionDate: DateTime.parse(json['productionDate'] as String).toLocal(),
expireDate: DateTime.parse(json['expireDate'] as String).toLocal(),
manufacturer: json['manufacturer'] as String?,
remark: json['remark'] as String? ?? '',
);
}
}
本地存储方案我最初想用 sqflite,但踩了坑。sqflite 在 OpenHarmony 上的原生数据库路径处理有问题,表现为创建数据库文件失败或者读写异常。后来换成了 Hive,Hive 是纯 Dart 实现,不依赖原生端,这让我在 OpenHarmony 适配时省了不少事。对家庭药箱这个量级的数据来说,Hive 性能完全够用。
3.2 药品列表与有效期预警
药品列表页是用户打开 App 后的主界面。我用 ListView 做长列表,每个药品卡片展示名称、规格、剩余数量和效期状态。卡片下方用不同颜色标签区分状态,红色标签表示已过期,黄色标签表示临期,绿色标签表示正常。
效期状态判断的逻辑:
dart复制enum ExpireStatus { ok, warning, expired }
ExpireStatus getExpireStatus(DateTime expireDate) {
final days = expireDate.difference(DateTime.now()).inDays;
if (days < 0) return ExpireStatus.expired;
if (days <= 30) return ExpireStatus.warning;
return ExpireStatus.ok;
}
状态阈值我设的是 30 天。30 天这个值不是拍脑袋定的,而是综合了家庭用药习惯:一般家庭库存药的消耗周期在 1 到 4 周,30 天能给用户留出足够的时间去处理临期药。阈值也做进了设置项,用户可以按需调整,不过后续版本才开放。
这里要特别提醒日期处理问题。我在真机测试时遇到过“药品明明没过期却显示还有 29 天”的情况,查了半天发现是时区问题:从云端或手工输入保存的 ISO8601 时间字符串解析出来是 UTC 时间,如果不转成本地时间就直接和 DateTime.now() 比较,中国时区会差 8 小时,算出来的天数就可能差一天。我的解法是统一在模型层用 toLocal() 转换,所有日期字段在读取和写入时都转成本地时间再比较。
3.3 用药提醒机制
用药提醒是家庭药箱场景里使用频率最高的功能。我最初以为 Flutter 的通知插件可以直接用,实际测试发现 flutter_local_notifications 依赖 Android 原生通知服务,在 OpenHarmony 上走不通。最后我通过 MethodChannel 封装了 OpenHarmony 侧的通知能力。
Dart 侧封装:
dart复制class NotificationService {
static const platform = MethodChannel('family_medicine/notification');
static Future<void> scheduleReminder({
required int id,
required String title,
required String body,
required DateTime time,
}) async {
try {
await platform.invokeMethod('scheduleNotification', {
'id': id,
'title': title,
'body': body,
'timestamp': time.millisecondsSinceEpoch,
});
} on PlatformException catch (e) {
debugPrint('通知调度失败: ${e.message}');
}
}
static Future<void> cancelReminder(int id) async {
await platform.invokeMethod('cancelNotification', {'id': id});
}
}
OpenHarmony 侧用 NotificationManager 创建定时通知。一个重要的经验是:把提醒转换成系统级定时通知,而不是依赖 Dart 后台 isolate。因为 OpenHarmony 系统为了省电会限制后台任务执行,App 一旦被清理后台,Dart 代码就停止运行了。定时通知由系统统一调度,即使应用进程被杀死,通知也会按时弹出。
4. 设置功能实现
4.1 设置项梳理与页面结构
设置功能是整个 App 的“配置中枢”,也是本篇文章的重点。很多开发者会把设置页当作简单的静态页面,几个 ListTile 摆上去就完事,其实不然。家庭药箱这种工具类应用,用户对设置页的依赖度很高,主题模式、通知开关、数据导出这些能力都从这里进入。
我最终确定的设置项如下:
| 分组 | 设置项 | 说明 |
|---|---|---|
| 外观 | 深色模式 | 开启后 App 全局变为深色主题 |
| 外观 | 跟随系统 | 跟随系统主题模式自动切换 |
| 提醒 | 通知总开关 | 控制所有定时通知是否弹出 |
| 提醒 | 默认提醒时间 | 每日固定用药提醒时间,默认 08:00 |
| 数据 | 导出数据 | 将所有药品数据导出为 JSON 文件 |
| 数据 | 清除缓存 | 清理临时文件和日志 |
| 关于 | 版本号 | 显示当前版本 |
| 关于 | 开源许可 | 展示第三方库许可证 |
页面结构用 ListView 分组实现,每个分组是独立的 Section,用一个小标题区分。Flutter 自带 ListTile 完全可以胜任,不需要引入额外的 UI 组件库。
4.2 数据持久化:SharedPreferences 的适配
设置项的持久化我使用 shared_preferences 2.x 系列。这个插件在 OpenHarmony 上社区有适配版本,但版本号可能跟官方渠道不一致。建议查看 ohos 目录下的 pubspec 依赖,确认实际的适配版本,避免引入不兼容的版本。
所有设置项我封装成了 SettingsService,页面层不直接操作 SharedPreferences,而是调用这个服务:
dart复制class AppSettings {
bool darkMode;
bool followSystem;
bool notificationEnabled;
String defaultRemindTime;
AppSettings({
this.darkMode = false,
this.followSystem = true,
this.notificationEnabled = true,
this.defaultRemindTime = '08:00',
});
}
class SettingsService {
static Future<AppSettings> loadSettings() async {
final prefs = await SharedPreferences.getInstance();
return AppSettings(
darkMode: prefs.getBool('dark_mode') ?? false,
followSystem: prefs.getBool('follow_system') ?? true,
notificationEnabled: prefs.getBool('notification_enabled') ?? true,
defaultRemindTime: prefs.getString('default_remind_time') ?? '08:00',
);
}
static Future<void> saveSettings(AppSettings settings) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('dark_mode', settings.darkMode);
await prefs.setBool('follow_system', settings.followSystem);
await prefs.setBool('notification_enabled', settings.notificationEnabled);
await prefs.setString('default_remind_time', settings.defaultRemindTime);
}
}
封装的目的很简单:如果后面 shared_preferences 在 OpenHarmony 上出现新的兼容问题,我只改这一个文件就可以,不用满项目去替换调用点。
4.3 主题切换与深色模式
主题切换是设置页里用户感知最强的功能。深色模式不是简单的把背景变黑,而是需要一套完整的深色主题配色,保证文字对比度、卡片层次、状态标签色在深色背景下依然清晰。
我用了 ValueNotifier 管理主题模式,并且把 MaterialApp 用 ValueListenableBuilder 包裹起来,这样主题变化能实时作用到整个 App 所有页面:
dart复制final ValueNotifier<ThemeMode> themeModeNotifier = ValueNotifier(ThemeMode.system);
class FamilyMedicineApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ThemeMode>(
valueListenable: themeModeNotifier,
builder: (context, mode, _) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
brightness: Brightness.light,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
brightness: Brightness.dark,
),
themeMode: mode,
home: HomePage(),
);
},
);
}
}
设置页里的深色模式开关和跟随系统开关互斥:如果“跟随系统”打开,深色模式开关置灰;如果手动切换深色模式,跟随系统自动关闭。这个互斥逻辑要在 UI 层和状态层同时处理,避免出现“两个开关都是开”的冲突状态。
实际测试中我发现一个经验点:在深色模式下,药品卡片的状态色需要微调。红色、黄色和绿色标签在纯黑背景下饱和度会偏高,看起来刺眼。我最后在深色模式下降低了标签的饱和度,并增加一点透明度,视觉上舒服很多。
4.4 数据导出与缓存清理
数据导出功能我实现为生成 JSON 文件。考虑到家庭药箱的数据量不大,JSON 格式比 SQLite 文件更适合导出:体积小、可读性强、后续导入解析也简单。
导出代码:
dart复制Future<String> exportData() async {
final box = Hive.box<Medicine>('medicine_box');
final medicines = box.values.map((m) => m.toJson()).toList();
final exportMap = {
'app': 'family_medicine_box',
'version': '1.0.0',
'exportTime': DateTime.now().toIso8601String(),
'medicines': medicines,
};
final dir = await getApplicationDocumentsDirectory();
final file = File(
'${dir.path}/medicine_export_${DateTime.now().millisecondsSinceEpoch}.json',
);
await file.writeAsString(jsonEncode(exportMap));
return file.path;
}
exportTime 字段加上了,后面做导入功能时可以比对数据新鲜度。文件路径我建议用 path_provider 的 getApplicationDocumentsDirectory 获取,而不是直接拼绝对路径,因为 OpenHarmony 的沙箱目录结构跟 Android 并不完全一致。
缓存清理相对简单,遍历临时目录删除文件,同时清理 Hive 的日志文件。但要注意清理前先确认没有正在进行的 Hive 写操作,否则会抛异常。我用的方案是清理前标注一个 isCleaning 状态,写入前检查这个状态。
5. 常见问题与排查技巧实录
5.1 插件兼容性问题
这部分我踩过的坑比较多,整理成一个排查表,后续遇到类似问题可以直接对照:
| 插件 | 遇到的问题 | 解决方案 |
|---|---|---|
| sqflite | 数据库文件路径不可用,读写失败 | 替换为纯 Dart 的 Hive |
| shared_preferences | 官方版本不支持 OpenHarmony 目标 | 使用社区适配版 2.x |
| flutter_local_notifications | 依赖 Android 原生通知服务,OpenHarmony 上不可用 | 自写 MethodChannel 调 OpenHarmony 通知 |
| path_provider | 部分目录获取返回空 | 使用 getApplicationDocumentsDirectory 兜底 |
排查插件的思路是:先看插件是否依赖原生代码,再看 ohos 目录下有没有对应的原生实现文件。如果两者都没有,就直接放弃这个插件,换替代方案或者自写平台通道。这个过程我在项目里经历了三次,总结下来就是“不要死磕插件,早换早省心”。
5.2 UI 渲染与 ArkUI 交互问题
在 OpenHarmony 开发板上跑 Flutter 应用,UI 渲染最常见的现象是画面撕裂、黑屏或者部分区域闪烁。我最初也遇到了黑屏问题,查日志发现是 Flutter 引擎的 vsync 和 OpenHarmony 的垂直同步机制没对齐,导致渲染帧丢失。
社区的解决方案是开启软件渲染模式。虽然会损失一定的渲染性能,但对家庭药箱这类低频交互应用来说,稳定性远比帧数重要。开启方式是在 OpenHarmony 侧入口配置渲染参数,具体可以在 main.cpp 或者 Runner 工程里设置。如果只是做功能验证,软件渲染完全够用。
调试方面,我建议优先用 DevEco Studio 的 hdc 工具看日志,而不是只盯着 Flutter 侧的 debugPrint。在 OpenHarmony 上,很多 Flutter 侧异常最终会反映为 OpenHarmony 的 Ability 生命周期异常或者平台通道报错,只看 Dart 层日志会漏掉关键线索。
5.3 后台提醒失效问题
后台提醒失效是开发过程中最折磨人的问题。App 在前台时通知能正常弹出,一旦退到后台或者杀掉进程,通知就没了。这其实是系统后台限制策略导致的,OpenHarmony 和主流移动系统一样,都会限制后台任务。
我的解决方案是把提醒从“Dart 代码定时触发”改成“系统定时通知”。具体流程是:设置页保存提醒时间后,通过 MethodChannel 把时间传给 OpenHarmony 侧,由 NotificationManager 创建定时通知。这样即使应用进程被杀,系统也能在指定时间弹出通知。
还有一点必须提:OpenHarmony 的通知权限默认可能是关闭的。用户第一次启动 App 时,需要引导用户去系统设置里开启通知权限,否则所有提醒都会静默失败。我在设置页加了权限状态检测,如果通知权限是关闭状态,会显示一条黄色提示条,点击后跳转到系统的通知权限设置页。
5.4 时区与日期差一天问题
时区问题看起来小,实际影响很大。我在效期预警功能里踩过这个坑,前面提到过,这里展开讲一下解决思路。
问题根源在于 DateTime.parse() 在解析带时区标识的 ISO8601 字符串时,会保留 UTC 偏移量;而 DateTime.now() 返回的是本地时区。如果开发者不做转换,直接对两者相减,中国时区就会差 8 小时。
我的经验是:所有日期在进入模型层时统一用 toLocal() 转换。写入数据库前保存本地时间字符串,读取后也做本地化处理,这样全项目就不会出现“差一天”的诡异问题。尤其在 OpenHarmony 和 Android 双端共用一份备份数据的时候,这个处理不能省略。
6. 一些个人体会
6.1 OpenHarmony 适配层的现状判断
经过这个项目,我对 OpenHarmony 上跑 Flutter 的成熟度有了比较实际的感知。基础能力已经能支撑一个完整工具类应用:UI 渲染正常、事件交互流畅、纯 Dart 插件可用、系统通知可对接。但距离“开箱即用”还有距离,主要体现在插件生态的覆盖面上,尤其是涉及硬件、传感器、系统服务的插件,基本都要自己处理。
我给后来者的建议是:项目启动前先花两天做技术预研,把要用的插件逐个查一遍,确认 OpenHarmony 侧的适配方式,再决定方案。预研花的这两天,往往能省下项目后期一周的适配时间。
6.2 后续可以做的扩展
家庭药箱这个项目后续扩展空间很大。我自己列了几个方向:
- 扫码录入:通过摄像头扫描药盒条码,自动识别药品信息。这个需要调相机能力,社区适配层目前覆盖不全,要自研更多平台通道。
- 家庭成员权限:为老人和孩子建立独立档案,记录各自的用药记录和过敏史,需要引入账号体系。
- 多端云同步:同一份药箱数据在手机、平板、开发板上保持一致,用网络请求就能实现,跨端兼容性最好。
- 药品图片存储:拍照记录药品外观,数据量增大后需要处理图片压缩和缓存策略。
6.3 踩过几次坑之后的一点建议
最后说点实践层面的经验。App 的开发过程里,真正困扰我的不是业务代码怎么写,而是跨端适配中那些“看着是小问题、排查起来要半天”的坑。插件不兼容、时区偏移、通知权限、渲染模式,随便一个都能让你多调试两三天。
我个人比较推荐的做法是:把所有适配相关的问题单独记录在一个文档里,每个问题标注现象、排查步骤、最终方案。这个文档不光是给自己看,对整个团队后续做 OpenHarmony 适配都有参考价值。我就是靠着这份记录,在第二台开发板上重新部署应用时,只花了不到两个小时就走完了全部流程。
