最近在做 Flutter for OpenHarmony 的健康类 App,功能做到“提醒设置”这一块时,发现网上能直接参考的实战资料少得可怜。大部分帖子要么停留在“跑通 Hello World”,要么只讲 Dart 层面的组件用法,一到 OpenHarmony 的权限、通知调度、生命周期限制这些真问题时,就没人往下写了。
这篇文章把我自己的实现过程完整复盘一遍:从为什么选 Flutter 来适配 OpenHarmony,到提醒功能的状态模型设计、UI 交互、系统通知调度,再到我踩过的几个 OpenHarmony 特有的坑。希望能给正在做同类应用的兄弟一些参考。
1. 为什么选 Flutter 啃 OpenHarmony 这块硬骨头
1.1 OpenHarmony 应用生态的现实处境
先聊点背景。OpenHarmony 从开源到现在,应用生态一直在成长期,这个“成长中”意味着什么?意味着官方文档更新快、兼容性碎片化、第三方库覆盖不全,很多在 Android/iOS 上成熟得不能再成熟的方案,换到 OpenHarmony 上就得自己重新趟一遍,尤其是 Notification、后台任务、权限申请这类与系统强相关的模块。
但业务不等人。我当时的诉求很直接:一个团队、有限人力,要同时覆盖现有移动端和 OpenHarmony 设备,最好的办法就是跨端方案。Flutter 在这种场景下优势很明显:Dart 层逻辑可以最大程度复用,UI 一致性好,OpenHarmony 社区也有官方和厂商推动的 Flutter 适配层——虽然还不是主流,但已经具备跑业务的能力。
1.2 Flutter 适配 OpenHarmony 的技术路线
说到适配,得先澄清一个容易混淆的概念。OpenHarmony 的 Flutter 支持并不是“把 Flutter 直接装到 OpenHarmony 上跑”这么简单。它其实是把 Flutter Engine 作为一个原生库编译进 OpenHarmony 应用,然后通过一套平台通道与 OpenHarmony 的系统能力对接。
我项目里用的是社区维护的 flutter_flutter 和 flutter_engine 的 OpenHarmony 分支。具体选型时我对比过两条线:一条是 OpenAtom 基金会仓库里的版本,一条是厂商自己的适配版本。实测下来,社区版本更新频率尚可,但需要自己盯 commit,OpenHarmony SDK 版本升级后很可能要跟着调整编译参数。
提示:环境搭建时一定注意 Flutter SDK、OpenHarmony SDK、以及适配分支三者的版本对应关系,最好把三者的版本号写进项目的 README 里,否则过两周你自己都会忘。
1.3 项目落地的环境准备
环境准备这里,我直接列一份经过验证的搭配,至少我项目跑起来是稳定的:
- OpenHarmony SDK API 9(当时设备系统是 3.1 Release)
- Flutter SDK(OpenHarmony 适配分支,基于 Flutter 3.x)
- DevEco Studio 用于构建 HAP 包和调试原生层
- 真机调试:优先用 OpenHarmony 设备,模拟器的通知行为差异太大,后面会细说
编译命令上,官方文档给的是用 hvigor 构建 HAP,但我实际操作中发现,最顺的方式是先flutter build hap打出产物,再用 DevEco Studio 打开工程补充签名和权限配置。直接一条命令全流程跑通的场景,至少在我这个版本里还不太现实。
2. 健康提醒的产品逻辑:先画清楚状态再写代码
2.1 提醒类型拆解:重复提醒与单次提醒
做提醒功能之前,我先把“健康记录”这个场景下的提醒需求盘了一遍。典型的提醒有这么几类:
- 每日定时测量体重、血压(重复提醒,周一到周日可自由勾选)
- 服药的单次提醒(一次性的,到点提醒)
- 喝水、久坐站立这类间隔提醒(按小时间隔循环)
第一版我先实现了前两类:重复提醒和单次提醒。间隔循环提醒看着简单,但实际要处理“暂停”“跳过”“重置”这些边界,建议放到二期再做,不要在第一个版本里给自己加戏。
每一条提醒,我最终抽出来的核心字段是:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 唯一标识,UUID |
| title | String | 提醒标题,例如“测量血压” |
| time | String | 时:分,例如 “08:30” |
| repeatDays | List<int> | 重复的星期,0=周一…6=周日,空数组表示单次 |
| enabled | bool | 是否开启 |
| createdAt | int | 创建时间戳,用于排序 |
这里有一个容易被忽略的设计点:不要把“提醒类型”单独列成一个枚举字段,用repeatDays的语义就能区分——为空就是单次,有值就是重复。类比的道理和很多后台接口设计一样:能用数据本身表达的,就不要增加额外的状态字段,否则后面同步、校验全是双倍的工作量。
2.2 提醒数据的存储模型
存储层我选了最稳的方案:SharedPreferences 存 JSON 字符串。为什么不直接上数据库?第一,提醒数量撑死几十条,JSON 序列化完全扛得住;第二,OpenHarmony 上 Flutter 的数据库插件适配不如 Android 成熟,less code less bug。
序列化结构大概长这样:
{ "reminders": [ { "id": "a1b2c3", "title": "测量血压", "time": "08:30", "repeatDays": [1, 2, 3, 4, 5], "enabled": true, "createdAt": 1699000000 } ] }读取和写入我封装在了一个ReminderStorage类里,对外只暴露loadAll()和saveAll(List<Reminder>)两个方法。内部自己管并发和缓存,UI 层根本不需要关心。
提示:写入的时候一定要做异常兜底。我有一次在真机上连续快速开关提醒,第三次直接抛 JSON 解析异常——后来加了 try-catch 和本地缓存备份,这个问题才算彻底解决。
2.3 用 Provider 组织状态流,而不是 setState 满天飞
热搜词里有人问“flutter provider 怎么用”,我正好结合这个项目说一下。提醒设置页涉及三个层级的 UI 状态:提醒列表、编辑页表单、列表与编辑页之间的联动。如果直接用 setState,你会发现编辑页保存后,列表页刷新要靠回调层层传,加一个“全部启用/停用”的功能就要崩。
我的拆分方式:
ReminderListModel:负责列表的加载、增删、启停切换,向外暴露List<Reminder>和操作成功的通知。ReminderEditModel:负责编辑表单内部的临时状态(选中的时间、勾选的星期),保存时才提交给 list model。AppState根级:管理 Provider 的作用域,以及跨页面需要共享的信息(比如当天已经触发了哪些提醒)。
消费端的写法很常规:
final reminderList = context.watch<ReminderListModel>().reminders;选择 Provider 的核心原因不是“流行”,而是它把“状态在哪、谁去改”这两件事分清楚了。对于提醒设置这种表单+列表联动的场景,这个区分能省掉后面无数的 bug 排查时间。
3. 提醒类型拆解与数据模型设计
3.1 可复制字段设计
在动手写 UI 之前,先把数据模型定扎实,后面才能少返工。
我最后确定的提醒对象长这样:
class Reminder { final String id; final String title; final String time; // HH:mm 格式 final List<int> repeatDays; // 1-7 表示周一到周日 final bool enabled; final DateTime createTime; }用List<int>而不是List<int>?是因为我统一用空列表表示“只提醒一次”的单次提醒。这个决定在后面写比较逻辑时帮了大忙:
/// 是否为重复提醒 bool get isRepeated => repeatDays.isNotEmpty;拿这个字段就能区分两种提醒类型,不需要再加一个 type 字段。加字段看似更明确,实际上平白增加状态组合,容易让逻辑互相矛盾(比如 type=once 但 repeatDays = [2,5] 这种非法组合)。能用数据本身表达的语义,就不该引入新的状态变量。
3.2 时间格式与排序规则
时间字段我统一存HH:mm的字符串。为什么不用 DateTime?因为提醒时间是用户设置的“时刻”,不是“时刻+日期”,用 DateTime 存反而会出现跨天比较的麻烦——比如“每天早上 08:30”用 DateTime 就没法直接表达。
排序规则是:先按 time 的数值排(取 hour*60+minute),再按 createdAt 排,保证同日期的提醒能稳定排序。这个顺序也是 App 内提醒列表的默认展示顺序,符合用户心智。
3.3 设置页面组件结构与联动逻辑
提醒设置页我分成了三个核心区域:
- 顶部:提醒标题输入框
- 中部:时间选择器和重复星期选择器
- 底部:保存按钮
其中,时间选择器我用的是 showTimePicker,这是 Flutter 自带的 Material 组件。Picker 结果处理有一个很容易踩的坑:它在 OpenHarmony 上打开的速度比 Android 稍慢,并且连续快速点击“确定”可能触发两次回调。我的处理方式是加了一个防抖开关:
bool _timePickerOpen = false; Future<void> _pickTime() async { if (_timePickerOpen) return; _timePickerOpen = true; final picked = await showTimePicker( context: context, initialTime: TimeOfDay.now(), ); _timePickerOpen = false; if (picked != null) { setState(() => _time = picked.format(context)); } }重复星期选择我直接用了一排 ChoiceChip,选中的项高亮。这里关键的是“全不选”的语义怎么处理——我把它解释为“仅当日提醒”,也就是单次提醒,并在 UI 上给出明确的提示文字“未选择重复日期时,将仅在设定时间提醒一次”。
3.4 Provider 在编辑与列表间的数据桥接
我这里用了 Provider 而不是 setState,因为列表页和编辑页之间的数据同步,用 setState 会写一大堆回调,跨页面传值也容易乱。
定义了一个ReminderModel继承 ChangeNotifier,核心方法:
class ReminderModel extends ChangeNotifier { List<Reminder> _reminders = []; List<Reminder> get reminders => _reminders; void addReminder(Reminder item) { _reminders.add(item); notifyListeners(); } }页面内通过context.read<ReminderModel>()拿到实例,保存时调用addReminder。这样列表页在Consumer或context.watch下会自动重新 build,不需要手动管理回调。
提示:在 OpenHarmony 的 Flutter 适配版里,ChangeNotifier 的 notifyListeners 触发的 rebuild 可能存在一帧延迟。我遇到过列表刷新慢半拍的问题,解决办法是在保存后延迟 300ms 再路由返回,体感上流畅很多。
4. 通知调度的核心实现:让系统在正确时间弹出正确内容
4.1 通知能力选型:本地通知还是系统 API
提醒设置页构建好了,接下来最核心的问题:通知怎么发?在 Android 上,主流做法是用 flutter_local_notifications 插件,但 OpenHarmony 上这个插件没有官方适配。我当时有几条路:
- 在原生侧写一个 NotificationHelper 的 Stage 模型工具类,Flutter 通过 MethodChannel 调它。
- 用 OpenHarmony 的 reminderAgentManager,它其实更适合“定时提醒”场景,并且能交给系统接管,App 被杀了也能提醒。
- 用纯 Flutter 的 Timer,不推荐,App 进程一死通知就没了,用户根本等不到提醒。
我选了第二条路:reminderAgentManager。原因很实在——健康提醒类 App,用户最不能接受的就是“明明设置了提醒,到点没响”。系统级调度能覆盖 App 被清理的场景。
4.2 reminderAgentManager 接入流程
在 OpenHarmony 的 Stage 模型里,接入 reminderAgentManager 的流程分四步:
- 在 module.json5 里声明权限:
ohos.permission.PUBLISH_AGENT_REMINDER。 - 获取 reminderAgentManager 实例。
- 创建 ReminderRequest 并配置时间、标题、震动等。
- 调用
publishReminder,系统返回一个 reminderId。
这里直接上我封装的原生侧代码片段:
import reminderAgentManager from '@ohos.reminderAgentManager'; export function publishReminder(time: number, title: string, content: string) { const reminder = { reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER, triggerTime: time, title: title, content: content, slotType: reminderAgentManager.SlotType.SOCIAL_COMMUNICATION, }; reminderAgentManager.publishReminder(reminder).then((reminderId) => { console.log('publish result: ' + reminderId); }); }Flutter 侧通过 MethodChannel 调用:
const _channel = MethodChannel('health_app/reminder'); Future<void> scheduleReminder(Reminder item) async { final triggerAt = _nextTriggerTime(item); await _channel.invokeMethod('publishReminder', { 'time': triggerAt.millisecondsSinceEpoch, 'title': item.title, 'content': '该测量了', }); }4.3 重复提醒的时间计算
单次提醒很简单:用户选的时间下一次到达的时刻,直接传给系统。
重复提醒要算的是“下一个符合星期条件的时刻”。我写了一个纯 Dart 函数:
DateTime _nextTriggerTime(Reminder r) { final now = DateTime.now(); final hour = int.parse(r.time.split(':')[0]); final minute = int.parse(r.time.split(':')[1]); if (r.isRepeated) { for (var i = 0; i < 7; i++) { final candidate = now.add(Duration(days: i)); if (r.repeatDays.contains(candidate.weekday) && (i > 0 || candidate.hour < hour || (candidate.hour == hour && candidate.minute < minute))) { return DateTime(candidate.year, candidate.month, candidate.day, hour, minute); } } } else { final today = DateTime(now.year, now.month, now.day, hour, minute); if (today.isAfter(now)) return today; return today.add(Duration(days: 1)); } return DateTime(now.year, now.month, now.day, hour, minute); }注意一个细节:重复提醒如果今天的星期命中但时间已过,要往后找,不能直接返回今天。我在第一次实现时漏了这个判断,结果早上 8:30 的提醒如果用户 8:31 才设置,系统马上弹了一次——这是产品上不能忍的 bug。
4.4 通知点击跳转与权限说明
点击通知跳回 App 的指定页面,我在 Module 的 UIAbility 里注册了 onNewWant 回调,根据 want.parameters 里的页面参数判断跳转目标。这里稍微绕一点,但 OpenHarmony 的通知点击行为目前就是这套逻辑——从 Want 的 parameters 里拿自定义参数。
另外权限提示文案务必提前准备。OpenHarmony 上第一次申请通知权限时系统会弹框,用户拒绝后再想诱导重新开启就要跳设置页,体验差一截。我的做法是:在进入提醒设置页前就弹一个自定义对话框,说明“提醒功能需要发送通知”,用户同意后再调系统申请接口,这样同意率高很多。
5. 踩坑实录:OpenHarmony 带来的那些“惊喜”
5.1 日期时间选择器在 Flutter 和 OpenHarmony 之间的适配差异
第一版我直接用了 Flutter 自带的 showTimePicker 和 showDatePicker,结果在 OpenHarmony 真机上发现两个问题:
- 弹窗样式错乱:个别版本的适配层对 Material 弹窗的 inset 处理不对,时间选择器会被拉伸变形。
- 取消按钮点击区域过小:在触屏上很难点中,用户容易误触保存。
我的解决办法是:不纠结原生控件,直接自绘一个滚轮式时间选择器,用 ListWheelScrollView 实现“时”“分”两个滚轮。虽然要多写一点代码,但换来了跨平台完全一致的交互体验和视觉呈现。做跨端 App 就要习惯这一点:平台自带控件在适配层不一定靠谱,自绘反而是最稳的。
5.2 后台通知失效与省电策略
这个坑差点让我推翻之前的方案。在使用 reminderAgentManager 做系统级提醒时,如果在设置里把 App 的“后台运行”权限关掉,有些设备上通知会被系统静默拦截。
排查过程我记录一下,供参考:
- 起初以为是时间算错了,反复验证
_nextTriggerTime,单测全过。 - 把通知改成最小间隔(1 分钟后触发),能用。
- 改到 10 分钟后,时灵时不灵。
- 偶然发现灵的时候 App 都在前台运行,不灵的时候都是锁屏或切后台状态。
- 去设置里给 App 打开“允许后台运行”,问题消失。
结论:OpenHarmony 的后台省电策略比 Android 更激进。解决方案有两个层面:产品层面在提醒设置页增加“保持后台运行”的引导说明;技术层面用bundleManager.getBundleInfoForSelf()查询当前应用的后台运行授权状态,如果不允许,就引导用户去设置页打开。不要试图绕过系统省电策略,OpenHarmony 在这块管得很严。
5.3 重复提醒与时间变更的处理
还有一个边界情况:用户在设备上手动改了系统时间,或者跨时区,已发布的重复提醒会不会错乱?
实测结论:reminderAgentManager 的提醒是基于系统时间计算的,改时间后会按照新的系统时间重新匹配触发条件。如果用户把时间往未来调,提醒会立刻触发一大堆——我遇到过用户反馈“一下子蹦了五个通知”。
诚实的说,这个问题我没有在 App 层完全解决,只是做了一层缓解:在提醒设置页保存时,提前提示“修改设备时间可能影响提醒的准确触发时间”。后续如果要做强一致,可以考虑在 Flutter 侧监听系统时间变化,然后重新计算并批量发布提醒,但这要配合原生侧写系统事件监听,时间成本不小。
5.4 随机崩溃新知识:XTS 认证对通知类应用的硬性要求
最后加一条最近才弄明白的事情:OpenHarmony 的应用如果要过 XTS 认证(设备商上架前都要过),通知栏弹出的内容、图标、样式有明确的规范。我在测试时发现,不设置通知图标(icon)时,部分设备会直接拒绝显示通知,但不会报任何错误,静默失败——这属于最容易忽略的一类问题。
解决方案:传入一个系统资源里的 icon id。在 OpenHarmony 上可以这样设:
const reminder = { reminderType: ..., triggerTime: ..., title: ..., content: ..., notificationId: 10001, };如果自定义图标不起作用,就用系统预留的默认图标兜底,至少不会让通知消失。这块细节 XTS 测试文档有清单,我建议做 OpenHarmony 通知类需求前就把文档通读一遍,不要等测试报告打回来才知道问题出在哪。
6. 调试与验证:提醒功能没有“看起来没问题”这一说
6.1 短周期触发测试法
提醒功能是最不适合“攒到最后一起测”的功能。我的测试方法是设置一个 1 分钟后触发的临时提醒,等通知弹出后立刻取消,并且所有测试用例都基于真实设备而不是模拟器——模拟器上通知调度和真机差异太大,跑不出真实结果。
6.2 多提醒并发互不干扰验证
同时设置 5 条不同时间的提醒,验证它们都能独立触发,并且取消其中一条不会影响其他四条。这个场景我在验证时发现过一个隐蔽 bug:原生侧 publishReminder 返回的 reminderId 如果没存好,后面要取消某条提醒时 id 对不上,会导致取消失败。所以我对 id 的管理方式是:
- 每次 publish 成功后,把 reminderId 存进 SharePreferences,和 Reminder.id 做映射。
- 取消时先用映射关系找到系统那边登记的 reminderId,再调 cancelReminder。
- 落盘失败时宁可提醒用户“操作可能未生效”,也不能静默失败。
6.3 提醒列表的增删改查回归用例
我把提醒列表的增删改查回归测试点整理成一份清单,实际跑起来效率很高:
| 场景 | 预期结果 |
|---|---|
| 新增单次提醒 | 到点触发一次,之后不再触发 |
| 新增重复提醒(周一至周五) | 工作日触发,周末不触发 |
| 修改时间后保存 | 旧提醒取消,新时间生效 |
| 删除提醒 | 到点不再触发 |
| 全部提醒关闭 | 不触发任何通知 |
这些用例跑完后,提醒这块我才敢说“基本稳了”。
7. 下一步的优化空间
最后聊聊后续方向。提醒设置这块做完,后面顺理成章的工作有几个:
- 把间隔提醒(比如“每 2 小时提醒喝水”)加进来,核心改动是原生侧要支持 interval 类型,需要查一下 reminderAgentManager 的 ALARM 类型支持情况。
- 从提醒升级为“待办任务”,加上“是否已完成”的状态,这样就是从一个“提醒”功能进化成了半个任务管理工具。
- 云端同步:把提醒配置同步到云,换机不丢数据。这里要用到账号服务和数据同步的 SDK,和本地 JSON 存储是两套体系。
我个人做这个项目的最大体会是:Flutter 的 UI 开发效率和跨端能力在 OpenHarmony 上确实能兑现,但凡是涉及系统能力的地方(通知、权限、后台调度),都要先花时间搞明白 OpenHarmony 的规范,再动手写代码。它跟 Android 相似,但不完全相同,每一个“想当然”都会成为后面调试时的坑。把这篇文章里提到的几个问题提前规避掉,你大概率能少走至少三天的弯路。