
我是在一块OpenHarmony开发板上把视力保护提醒App跑起来之后才意识到通知设置这关有多重要的。UI层可以照搬移动端那套Flutter代码可一旦涉及到通知栏、定时提醒、点击跳转Android上那套Notification写法就完全不好使了——OpenHarmony的通知体系有自己的渠道定义、权限模型和wantAgent机制照着安卓习惯写大概率是代码没问题就是不出声。这篇文章就记录我从零搭建Flutter for OpenHarmony视力提醒App时通知设置这一路的完整实践包括通知渠道、定时提醒、权限声明和踩坑排查适合想在国产操作系统上做提醒类、健康类、工具类应用的Flutter开发者参考。1. 为什么我会在OpenHarmony上用Flutter写一个视力提醒App1.1 视力提醒App的特点与真正难点视力保护提醒App的核心功能并不复杂根据20-20-20法则每看屏幕20分钟远眺20英尺外至少20秒定时提醒用户休息。再往深做一点就是让用户自己设置提醒间隔、免打扰时段、点击通知进入App查看今日用眼统计。这类App有一个典型特征UI不重逻辑不重但对系统底层能力依赖极重。它需要两个东西做支撑一是可靠的定时触发机制二是通知系统能稳定呈现消息。恰好这两点都是跨端适配最容易翻车的地方。我在Android上用AlarmManager和NotificationChannel很熟练到了OpenHarmony上才发现定时任务的调度策略、通知渠道的管理方式、点击跳转的实现机制全都不一样。如果只看官方文档而不动手写很容易被代码没问题就是不生效折磨半天。1.2 为什么不直接用ArkTS而选Flutter每次提到鸿蒙开发评论区都会有人问arkts和flutter谁更流行。我的选择理由其实很实际如果只做OpenHarmony一个平台ArkTS当然更贴近系统毕竟它是原生语言调用系统API不经过任何中间层。但我的代码还要同时覆盖Android和iOS以后甚至想出一个手表端的小屏版本。Flutter的自绘引擎可以保证移动端三端UI高度一致热重载对调整提醒文案、配色、息屏显示这类的细节体验太舒服了。还有一个很多人忽略的点Flutter组件通信和状态管理的生态非常成熟。视力提醒App需要管理下次提醒时间当前开关状态免打扰时段这些跨页面共享状态用provider或riverpod就能轻松解决。这种状态管理需求在ArkTS体系下也有但方案和资料明显不如Flutter社区丰富。既然App的UI复杂度就这么高Flutter完全够用没必要为了原生而原生。1.3 Flutter for OpenHarmony 的工程现状与环境准备可能还有人以为Flutter跑不了鸿蒙其实从OpenHarmony的SIG组维护的分支开始Flutter就已经可以编译到鸿蒙设备上了。当前主流的做法是把flutter_ohos作为依赖通过flutter create --platformsohos .生成鸿蒙工程壳然后用DevEco Studio打开ohos目录编译。整个流程已经能走通只是版本跟进没有Google Play那样即时。环境准备建议按这个顺序来安装DevEco Studio配上OpenHarmony SDKAPI 11以上建议直接用API 12新版SDK对通知、后台任务的支持更完善。准备Flutter的ohos分支工具链加入PATH环境变量。执行flutter doctor确认能识别OpenHarmony工具链。项目里配好国内镜像源下载依赖会快很多。顺便回答另一个经常被搜的问题openharmony os是用什么语言编写的。系统底层主体是C/C上层应用开发支持ArkTS/JS/C等语言而Flutter的Dart代码是通过自绘引擎和系统服务桥接的所以你在Flutter侧写业务逻辑在ets侧写系统能力调用两者各管各的互不干扰。还有一个关于渲染引擎的细节Flutter的Impeller渲染器在OpenHarmony分支上还不是默认开启状态目前默认还是走Skia建议不要贸然开启Impeller的强缓存选项开发板上可能会闪退。2. 跑通工程只是第一步Flutter与鸿蒙侧的通信桥2.1 OpenHarmony工程的Ability与FlutterEngine一个Flutter for OpenHarmony项目实际运行的是entry模块里的AbilityFlutter渲染的内容加载在FlutterAbility或者FlutterFragment容器里。这里需要先理解两个核心概念Ability是OpenHarmony应用的基本单元类似Android的Activity负责生命周期和界面承载。FlutterEngine是Dart isolate的容器承载Flutter页面的渲染和逻辑执行。模板工程一般会生成一个FlutterAbility你只需要配置ModuleName和路由表然后在EntryAbility里初始化FlutterEngine通过setContentView把Flutter容器挂载上去。对通知设置来说有一个隐蔽的坑是FlutterEngine创建时机和MethodChannel注册时机存在竞争关系。如果原生侧还没把Channel注册好Dart侧就急着调invokeMethod会直接抛MissingPluginException。后面我专门写这个坑。2.2 MethodChannel在鸿蒙侧的注册方式通知设置这种系统能力Flutter侧没有现成的官方插件必须自己走MethodChannel把Dart的命令发给鸿蒙原生侧执行。Flutter侧代码import package:flutter/services.dart; class ReminderBridge { static const MethodChannel _channel MethodChannel(com.example.eye_care/reminder); static Futureint scheduleReminder({ required int minutes, required String title, }) async { final int id await _channel.invokeMethod(scheduleReminder, { minutes: minutes, title: title, }); return id; } static Futurevoid cancelReminder(int id) async { await _channel.invokeMethod(cancelReminder, {id: id}); } }鸿蒙侧以ets为例// ReminderPlugin.ets import { FlutterPlugin, FlutterPluginBinding, MethodChannel } from ohos/flutter_ohos; export class ReminderPlugin implements FlutterPlugin { private channel: MethodChannel | null null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel new MethodChannel( binding.getBinaryMessenger(), com.example.eye_care/reminder ); this.channel.setMethodCallHandler((call, result) { if (call.method scheduleReminder) { // 调用 reminderAgentManager声明在第四章 } else if (call.method cancelReminder) { // 调用 cancelReminder } }); } onDetachedFromEngine(binding: FlutterPluginBinding): void { this.channel?.setMethodCallHandler(null); this.channel null; } }关键点有三个Channel名称必须两端完全一致参数必须是JSON可序列化类型插件的注册时机放在FlutterPlugin的onAttachedToEngine回调里而不是直接在Ability的onCreate里手动注册否则容易重复注册或注册过晚。2.3 用Provider管理提醒状态的思路既然说到了Flutter组件通信我顺便聊聊状态管理。视力提醒App有一个典型的全局设置场景设置页改了提醒间隔首页的倒计时提示要立刻变用户点开通知跳转到统计页统计页要读取最新的提醒记录。这些跨页面共享的状态我是用provider解决的。具体做法是定义一个SettingsModel extends ChangeNotifier里面放提醒间隔、免打扰时段、当前提醒开关等字段用ChangeNotifierProvider包裹整个App。原生侧只负责执行命令比如创建提醒、取消提醒、返回提醒是否已授权所有业务决策都留在Dart侧。这样的分层让我在调试时很省心通知不弹了先看原生侧日志逻辑不对了直接看Dart侧状态。3. 通知设置的本质OpenHarmony通知体系与Android的差异3.1 Slot渠道与发布通知的基本写法Android 8.0引入了NotificationChannelOpenHarmony对应的概念是Slot插槽。发布通知时通过notificationSlotType指定SlotType系统根据Slot类型决定铃声、震动、重要性等级。最常用的类型有SOCIAL社交类适合需要明显提醒的场景。CONTENT内容类普通消息提醒。SERVICE服务类不建议用于视力提醒系统可能默认不展示。代码里发布一条普通通知import notificationManager from ohos.notificationManager; import wantAgent, { WantAgentInfo } from ohos.app.ability.wantAgent; const wantAgentInfo: WantAgentInfo { wants: [ { bundleName: com.example.eye_care, abilityName: EntryAbility, }, ], actionType: wantAgent.OperationType.START_ABILITY, requestCode: 10, actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG], }; const wantAgentObj await wantAgent.getWantAgent(wantAgentInfo); const notificationRequest: notificationManager.NotificationRequest { id: 1001, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: 该休息啦, text: 盯着屏幕已经20分钟看看远处吧, additionalText: 20-20-20法则, }, }, notificationSlotType: notificationManager.SlotType.SOCIAL, wantAgent: wantAgentObj, }; await notificationManager.publish(notificationRequest);这段代码跑通之后你会发现OpenHarmony发布通知的流程其实和Android相差不大只是API命名不同。真正的坑不在发布而在选择哪个Slot点击如何跳转定时怎么触发。3.2 wantAgent如何替代Android的PendingIntentAndroid里通知点击跳转靠PendingIntent传入Intent和Flag就行。OpenHarmony对应的机制是wantAgent它的本质是一个代理对象系统通过它来触发指定的行为比如启动Ability、发送公共事件、打开后台任务。我在第一次写点击跳转时想当然按Android习惯只传了一个bundleName结果通知能弹但点击毫无反应。原因就是wantAgent需要同时指定bundleName和abilityName并且actionType要选START_ABILITY。如果点击后想进入Flutter的某个具体页面而不是固定首页可以在wants里加parameterswants: [ { bundleName: com.example.eye_care, abilityName: EntryAbility, parameters: { targetPage: stats }, }, ],然后在EntryAbility的onNewWant回调里读取parameters通过FlutterEngine把参数传给Dart侧最后用Flutter的路由跳转到统计页。这个设计相当于把哪个页面的决策权交还给Flutter侧原生侧只负责把参数带上。3.3 对照表Android通知API与OpenHarmony通知API能力点AndroidOpenHarmony渠道/分类NotificationChannel(id, name, importance)notificationManager.addSlotByType(SlotType)发布通知NotificationManagerCompat.notify(id, notification)notificationManager.publish(NotificationRequest)点击行为PendingIntentwantAgent.getWantAgent(WantAgentInfo)权限要求POST_NOTIFICATIONS运行时权限普通通知无需特殊权限受系统总开关控制提醒代理AlarmManager BroadcastReceiverreminderAgentManager.publishReminder这张表建议收藏。每次从Android迁移到OpenHarmony我都靠这类对照表快速定位我应该找哪个API而不是漫无目的地翻文档。顺便说一句OpenHarmony的文档里很多接口在API 11和API 12之间有过调整比如ohos.notificationManager在新版本里可能被归类到kit.NotificationKit下。我用的是相对通用的ohos命名空间如果你用的SDK版本更新看到import { notificationManager } from kit.NotificationKit这种写法也别慌API名基本一致。4. 视力提醒的定时触发从普通通知到reminderAgentManager4.1 为什么Flutter侧Timer方案不靠谱最直观的定时方案是在Flutter侧开一个Timer.periodic倒计时到点后调用publish推通知。听起来没毛病实际上一锁屏就露馅。OpenHarmony对非活跃应用有一套省电调度机制应用切到后台后进程可能被冻结或低优先级运行Dart的Timer根本得不到执行。就算侥幸没被冻结休眠状态下系统的调度精度也会大打折扣你会发现20分钟的提醒实际变成26分钟的提醒。对视力提醒这种高频周期场景我的结论很明确别跟系统策略对抗直接用系统提供的提醒代理。4.2 系统级提醒代理用法与权限声明reminderAgentManager是OpenHarmony系统级的提醒代理服务。它允许应用注册REMINDER_TYPE_TIMER倒计时或REMINDER_TYPE_ALARM到点提醒类型的提醒由系统的提醒服务统一调度不依赖应用进程是否存活。注册之后即使App被杀掉到点系统一样会弹出通知。先声明权限在module.json5的requestPermissions里加requestPermissions: [ { name: ohos.permission.PUBLISH_AGENT_REMINDER, reason: 用于按用户设置的间隔定时提醒休息, usedScene: { abilities: [EntryAbility], when: inuse } } ]再创建提醒import reminderAgentManager from ohos.reminderAgentManager; const reminderRequest: reminderAgentManager.ReminderRequest { reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER, triggerTimeInSeconds: 20 * 60, title: 该休息啦, content: 盯着屏幕已经20分钟看看远处20秒, slotType: reminderAgentManager.SlotType.SOCIAL, notificationId: 1002, wantAgent: wantAgentObj, }; try { const reminderId await reminderAgentManager.publishReminder(reminderRequest); // 保存reminderId之后用 cancelReminder(reminderId) 取消 } catch (err) { // 处理权限未授权、提醒代理被关闭等情况 }这段代码看起来简单但有两个点必须注意。第一publishReminder返回的reminderId你要找地方存下来Flutter侧通常会用SharedPreferences存一份因为取消提醒时必须用到它。第二PUBLISH_AGENT_REMINDER属于重要权限声明之后系统不会静默授予。首次使用时要通过abilityAccessCtrl.createAtManager()触发用户授权弹窗授权结果回传到Dart侧设置页的开关状态要和这个结果同步。4.3 用户授权流程与Dart侧联动权限弹窗的最佳触发时机是用户主动打开视力提醒开关那一刻而不是App一启动就弹。原因很简单用户还没产生我需要这个功能的意图突然弹窗只会增加反感而且拒绝一次之后再次请求受限很多。Flutter侧的交互逻辑大概是Futurevoid onEnableReminderChanged(bool enabled) async { if (enabled) { await ReminderBridge.requestPermission(); // 原生侧弹出授权 final granted await ReminderBridge.hasPermission(); if (!granted) { // 提示用户去系统设置里授权 _settingsModel.updateReminderEnabled(false); return; } final minutes _settingsModel.intervalMinutes; final id await ReminderBridge.scheduleReminder( minutes: minutes, title: 该休息啦, ); _settingsModel.saveReminderId(id); } else { final id _settingsModel.reminderId; if (id ! null) { await ReminderBridge.cancelReminder(id); } _settingsModel.updateReminderEnabled(false); } }原生侧在scheduleReminder里做权限检查和请求在cancelReminder里调用reminderAgentManager.cancelReminder。只要这个链路理清了后面加再多功能都是在这个框架上扩展。4.4 兜底方案前台任务扛住高频提醒如果确实因为某种原因不能用reminderAgentManager比如某些定制版本的系统对提醒代理做了限制还有一个退而求其次的方案申请ohos.permission.KEEP_BACKGROUND_RUNNING配置backgroundModes为task再通过continuousTask.startBackgroundRunning把应用变成长驻任务配合Flutter侧的Timer使用。实测下来这个方案在开发板上可用但有两个明显代价一是耗电量显著增加二是部分设备上用户可以在系统设置里手动清理长驻任务稳定性不如系统代理。我的建议是能用reminderAgentManager就不用前台任务。视力提醒这种应用的目标是可靠地提醒而不是一直活着系统代理恰恰是对的路子。5. 通知设置踩坑实录从不弹通知到点击无响应的完整排查5.1 通知静默消失总开关和SlotType现象publish返回成功但通知栏就是没消息。排查链路其实很固定先用hiklog看有没有Notification的警告日志命令是hilog | grep NotificationHelper。打开设置 通知管理 应用通知确认应用的允许通知开关是开着的。在代码里调用notificationManager.isNotificationEnabled()返回false就是系统总开关没开。确认应用有没有注册对应的SlotType如果之前没调用过addSlotByType部分系统版本会默认使用系统渠道。我遇到的最隐蔽情况是发布成功后通知被系统撤销原因是我把SlotType设成了SERVICE而服务类通知在某些设备上默认不展示。视力提醒用SOCIAL或CONTENT就安全得多。5.2 点击无响应wantAgent的正确姿势现象通知能弹但点击没反应或者点了之后App没出现在预期页面。排查链路检查bundleName是否和module.json5里的bundleName完全一致连大小写都不能差。检查abilityName我这里是EntryAbility除非跨模块调用否则不需要加模块路径前缀。检查wantAgent对象是否被复用。如果每次publish都重新getWantAgent旧的NotificationRequest里绑定的代理和新生成的可能不一致点击时解析失败的概率很高。用hilog | grep wantAgent看系统日志错误码能直接提示是bundleName找不到还是abilityName解析失败。5.3 息屏后不准时省电策略与提醒Agent现象App在前台时提醒很准一锁屏就推迟三五分钟甚至彻底不响。最开始我以为是Timer不准后来发现是OpenHarmony的调度策略在起作用。对非活跃应用系统会降低CPU优先级息屏后甚至直接冻结进程不管Flutter还是原生侧普通定时器都扛不住。解决办法把定时触发全部交给reminderAgentManager。它运行在系统服务进程中不受单个App生命周期影响。如果App本身还承担着统计用眼时长的长驻任务那就要配合前台任务方案并且在开发测试阶段把设备的电池优化策略改为不限制排除干扰因素。5.4 MissingPluginExceptionChannel注册时机现象App启动后第一次打开设置页调用scheduleReminder偶发报MissingPluginException。这是标准的Channel注册时序问题。FlutterEngine创建和FlutterPlugin注册是异步的Dart侧如果在插件注册完成前就发起invokeMethod就会找不到对应的MethodChannel。修复思路有两种把首次调用延迟到用户明确交互之后比如用户点击开启提醒按钮时再触发而不是在首页initState里自动检测。在原生侧通过FlutterEngine的回调通知Dart侧引擎已就绪Dart侧接收事件后再开始调用。第二种更稳可以根治启动时的竞态问题代价是多写一点通信代码。5.5 通知图标显示与资源ID最后一个看起来小、实际上非常重要的坑OpenHarmony的通知展示依赖系统资源ID不是直接传一个网络URL或drawable文件就行的。如果没配图标系统会用默认图标顶替用户根本认不出是你的App在提醒。做法是把图片做成media资源放进工程在NotificationRequest里指定icon对应的资源ID。如果使用reminderAgentManager也需要在ReminderRequest里配置对应的资源ID。对视力提醒App来说通知图标直接关系到用户看到提醒就想起来休息的识别效率不值得在这里省事。6. 视力提醒App的体验设计与后续扩展思路6.1 提醒策略设计别让通知变成噪音20-20-20法则听起来很简单但如果真的每20分钟弹一次通知用户大概率一周后卸载。我做产品设计时加了几个约束提醒间隔可配置默认为20分钟提供30/45/60分钟选项。支持免打扰时段比如中午12点半到13点半午休晚上下班后自动静默。每次提醒允许用户点再提醒5分钟给用户一个缓冲也避免连续轰炸。休息结束后再发一条继续加油类提醒形成完整闭环。这些策略全放在Dart侧用一个SettingsModel管理通过shared_preferences持久化。系统侧的reminderAgentManager只管执行定时触发这一件事业务规则都留在Flutter层这样即使以后换平台策略逻辑也能复用。6.2 数据统计与多设备场景下一步我想做的功能是把每日用眼时长被提醒次数实际休息次数存到本地数据库用fl_chart画成简单的趋势图。这样用户能看到自己过去一周的用眼习惯提醒就不再是机械弹窗而是一个有反馈的健康管理闭环。多设备场景也值得考虑。如果家里有学习机、开发板和平板多个设备可以通过云同步配置提醒策略让家长直接改孩子设备的提醒间隔。这个方向把视力保护从单机App变成了家庭健康管理的小系统也是OpenHarmony多形态设备的一个典型落地场景。6.3 一点个人体会说实话刚开始我也怀疑过Flutter在OpenHarmony上会不会很折腾。实际跑下来UI层几乎无感迁移真正的成本都集中在系统能力适配。通知设置是最典型的一块理解了OpenHarmony的Slot、wantAgent、reminderAgentManager之后你会发现它和Android虽然API不同但设计思路一脉相承。与其和系统策略对抗不如顺着系统提供的机制走把提醒交给系统代理把状态管理留在Flutter里两边都清爽。最后再分享一个自己的习惯每次适配一个新平台我都会先写一个最小Demo把MethodChannel从Flutter到原生、再从原生回调Flutter的完整链路跑通再开始堆功能。这个最小Demo我通常只花半天但能避开后面绝大部分的排查时间。视力提醒App的通知设置就是最好的练手场景。