ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter开发OpenHarmony喝水提醒应用:跨端调度与本地通知实战

Flutter开发OpenHarmony喝水提醒应用:跨端调度与本地通知实战 如果你手里刚好有一台OpenHarmony开发板想用Flutter给它写点实用工具那喝水提醒这个项目绝对是个不错的起点。我最初的想法很简单手机上的提醒类App不能直接装到开源鸿蒙设备上原生开发又得重新学ArkTS干脆走Flutter跨端路子硬啃。折腾了两周把喝水提醒完整跑通了——从Flutter侧定时调度到鸿蒙系统的本地通知再到App被系统回收之后提醒依然能响。整个过程踩了不少坑也把Flutter和OpenHarmony之间的通信机制摸了一遍。这篇文章把核心实现思路、关键代码和实际测试结果都记录下来适合想在OpenHarmony上用Flutter做生活助手、工具类应用的开发者参考。1. 为什么选Flutter写OpenHarmony应用以及能写到什么程度1.1 一套代码跑多端的真实收益生活助手类App往往不是只在手机上用。我手头这台设备的场景是OpenHarmony开发板放工位上旁边偶尔用旧Android手机盯一下数据回家还想在桌面上开个窗口看统计。如果三端各写一套逻辑光维护成本就够呛。Flutter正好把这块吃下来了。它的UI是自己渲染的不依赖系统原生控件所以同一套界面在OpenHarmony、Android、Windows上呈现出来的效果基本一致。Dart语言写业务逻辑也很顺手依赖管理一个pubspec.yaml搞定不用像原生工程那样为每个平台维护一堆配置文件。关键是团队技能复用。你如果已经有一批熟悉Flutter的开发者不需要让他们重新学ArkTS和ArkUI就能在OpenHarmony上出可用的产品。对我这种个人开发者来说这套路节省的时间尤其明显。1.2 Flutter和OpenHarmony原生各管一摊不过别指望Flutter能包办一切。通知、传感器、蓝牙这类系统能力最终还是得靠鸿蒙侧的原生能力配合。Flutter负责的是业务编排和UI展示真正发通知的动作要通过平台通道交给鸿蒙侧完成。我当时把边界划得很清楚UI层、状态管理、提醒调度逻辑全放Flutter侧调用系统通知服务、注册后台代理提醒这一类动作全部下沉到鸿蒙侧通过MethodChannel暴露给Flutter。这个分层想明白之后项目结构一下就清晰了后面加功能也顺手。1.3 社区支持现状能做什么不能做什么Flutter for OpenHarmony目前主要是社区维护不是官方主推这一点要有心理准备。好在我实际用下来工具类应用完全够用页面跳转、状态管理、数据存储、平台通道这些基础能力都跑得稳。社区里的flutter_ohos版本也在持续更新常用的第三方库大部分能直接编译。不适合的场景也有比如重度系统集成、底层渲染性能要求极高的游戏、复杂的原生地图交互这些在OpenHarmony上的Flutter适配还比较吃紧不如直接写原生。做生活助手类应用Flutter完全在舒适区里。2. 先把手弄脏工程配置里最容易翻车的权限与通知渠道2.1 通知权限的声明很多人第一次在OpenHarmony上集成通知上来就调接口然后发现通知死活不弹。原因多半是权限和通知设置没有处理好。在当前我使用的SDK版本中发布通知一般需要在module.json5里声明相关权限同时还要保证用户在系统设置里给应用打开了通知开关。权限配置的示意写法长这样{ module: { requestPermissions: [ { name: ohos.permission.NOTIFICATION_CONTROLLER } ] } }这里要特别提醒不同API版本的权限名称和授权方式有差异有些版本已经把通知权限改成运行时申请了。你开发的时候先查一下目标设备对应的SDK文档别照抄我的配置。另外就算权限声明了用户没在设置里打开通知开关通知一样不显示这部分要做引导后面排错章节我会详细讲。2.2 通知渠道不是可选项OpenHarmony的通知机制和Android 8.0之后很像通知必须要绑定渠道。渠道的作用是让用户能单独控制某类通知的开关、声音和震动。喝水提醒如果和别的通知混在一起用户在设置里没法定制体验大打折扣。我建的通知渠道很简单渠道IDwater_reminder渠道名称喝水提醒渠道描述定时喝水提醒和今日饮水总结后面发布通知的时候请求里带上这个渠道ID。用户可以在系统设置里单独关掉喝水提醒的声音但不会误伤其他通知。这个体验细节在做生活助手类App时一定要保留。2.3 环境版本匹配先把底打好Flutter for OpenHarmony的适配和SDK版本强相关。我用的是社区维护的Flutter 3.44版本SDKOpenHarmony侧的API版本对应API 12左右。版本对不上轻则编译警告重则平台通道直接连不上。我踩过一次坑一开始手滑装了旧版本的Flutter SDK结果鸿蒙侧的MethodChannel注册接口和Flutter侧对不上查了半天发现是新旧版本接口签名变了。建议开工前先把下面这个对应关系确认好组件版本说明Flutter SDK3.44社区ohos版本以你下载的SDK发布说明为准OpenHarmony API10/11/12均可推荐12DevEco Studio中配置开发工具DevEco Studio 5.x鸿蒙侧工程管理依赖管理pub ohpmFlutter侧用pub鸿蒙侧用ohpm版本确认这一步没什么技术含量但它决定了后面所有代码能不能跑起来值得花十分钟提前查清楚。3. 喝水提醒的核心调度设计轮询校准与后台代理提醒3.1 纯Timer为什么不行第一版我图省事直接用Flutter的Timer.periodic每分钟触发一次到点就弹通知。结果一测就露馅了App退到后台几分钟Timer还能跑时间长了之后进程一旦被系统回收Dart isolate整个消失定时器也就不存在了。就算进程没被杀Dart的Timer在系统休眠状态下也不会准点触发它不保证实时性只是把回调放到事件循环里等待执行。App在后台被系统冻结的情况下Timer回调能拖多久完全看系统脸色。所以我的结论很明确Flutter侧的Timer只能做辅助性的状态校准不能作为提醒的兜底机制。3.2 我的双层调度方案最终的调度设计分两层第一层是Flutter侧的分钟级轮询校准。App活着的时候每分钟检查一次当前时间判断是否到了计划的提醒时间到了就立即触发通知并更新下一次提醒时间。这一层解决的是计划和实际执行之间的偏差也会在用户打开App时马上同步一次状态。核心逻辑大概长这样Timer.periodic(const Duration(minutes: 1), (timer) async { final now DateTime.now(); final settings await ReminderSettings.load(); final nextReminder settings.nextReminderTime(now); if (now.isAfter(nextReminder)) { await _scheduler.showNotification( title: 喝水时间到, content: 距离上次喝水已过去 ${settings.intervalMinutes} 分钟, ); settings.updateNextReminder( now.add(Duration(minutes: settings.intervalMinutes)), ); await settings.save(); } });第二层才是兜底使用OpenHarmony的Reminder Agent Kit注册定时提醒。即使App进程被系统回收到点时系统自己会弹出提醒完全不需要App活着。这一层才是喝水提醒在锁屏状态下依然能响的真正保障。3.3 后台代理提醒的注册OpenHarmony的reminderAgentManager专门负责这种定时提醒场景。我通过Mate的纯Flutter写法是调用不了这个能力的所以还是老规矩Flutter侧发起注册请求鸿蒙侧负责真正的API调用。鸿蒙侧的注册逻辑用ArkTS写核心代码是这样的import { reminderAgentManager } from kit.ReminderAgentKit; function publishWaterReminder(hour: number, minute: number) { const reminder: reminderAgentManager.ReminderRequestAlarm { reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM, hour: hour, minute: minute, title: 喝水时间到, content: 该喝水啦起来活动一下, notificationId: 1024, slotType: reminderAgentManager.SlotType.SOCIAL_COMMUNICATION, }; reminderAgentManager.publishReminder(reminder).then((reminderId) { console.info(喝水提醒已注册reminderId: ${reminderId}); }); }这里提醒一下slotType要选对它决定了提醒的优先级和声音策略。SOCIAL_COMMUNICATION适合社交沟通类提醒比普通通知的打扰级别高一些喝水提醒用这个比较合适。你要是做用药提醒级别甚至可以更高。3.4 今日状态记录喝水提醒不能只弹通知还得记录用户今天已经喝了多少杯设置页面里要能调整间隔。我用的是shared_preferences做轻量持久化字段就三个今日已记录杯数、上次提醒时间、提醒间隔分钟数。量不大没必要上数据库存个JSON就行。提醒间隔第一次可以默认60分钟用户可以在设置页改成40或90分钟。改完之后把下次提醒时间重新计算一遍保证改动立刻生效。4. 让Flutter和鸿蒙通知服务握手MethodChannel与EventChannel的全过程4.1 平台通道的基本结构Flutter和鸿蒙侧通信主要靠平台通道。通道本身是个字符串名字两端保持一致就行剩下就是按约定好的方法名和参数传数据。通道名建议做成域名倒写避免冲突com.example.reminder/notification。方法名我定义了showNotification和publishWaterReminder两个一个用于立即通知一个用于注册后台代理提醒。参数用Map传递Flutter侧会自动序列化成鸿蒙侧能读的对象。4.2 Flutter侧调用端代码Dart侧封装一个调度器所有平台通道调用都收敛在这个类里别把MethodChannel散落在页面各处class ReminderScheduler { static const _channel MethodChannel(com.example.reminder/notification); Futurebool showNotification({ required String title, required String content, int delaySeconds 0, }) async { try { final result await _channel.invokeMethodbool(showNotification, { title: title, content: content, delaySeconds: delaySeconds, channelId: water_reminder, }); return result ?? false; } on PlatformException catch (e) { debugPrint(通知调用失败: ${e.message}); return false; } } Futureint? registerReminder({ required int hour, required int minute, }) async { try { final id await _channel.invokeMethodint(publishWaterReminder, { hour: hour, minute: minute, }); return id; } on PlatformException catch (e) { debugPrint(后台提醒注册失败: ${e.message}); return null; } } }注意invokeMethod返回值的类型如果你在鸿蒙侧返回的是整数Dart侧要对应接收int返回布尔就对应bool。类型对不上会直接抛类型转换异常这个我在开发中遇到过排查起来还挺费劲的。4.3 鸿蒙侧注册通道与发布通知鸿蒙侧的核心是在Ability的onConnect生命周期里注册MethodChannel。因为Flutter for OpenHarmony的工程模板和纯鸿蒙工程不太一样入口类的位置可能不同但思路都是一样的拿到能力对象之后调registerMethodChannel注册通道。处理MethodChannel调用的示意代码如下import { MethodChannel } from kit.ArkTS; import { notificationManager } from kit.NotificationKit; methodChannel.onMethodCall((method, args) { if (method showNotification) { const content args[content] as string; const title args[title] as string; const channelId args[channelId] as string; const request: notificationManager.NotificationRequest { id: Date.now(), content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: content, }, }, notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, }; notificationManager.publish(request, (err) { if (err) { console.error(通知发布失败: ${JSON.stringify(err)}); } }); return true; } });这里几个细节值得注意。一是request.id不能重复我直接用时间戳做id简单可靠二是publish接口的回调一定要处理不然发通知失败时日志里什么都看不到三是发布通知时要带上渠道ID否则系统会默认丢到未分类渠道里用户想单独控制喝水提醒的声音就找不着。4.4 EventChannel什么时候需要它MethodChannel是Flutter主动调用鸿蒙侧典型的请求-响应模式。但生活助手类App里还有一种场景鸿蒙侧持续往Flutter侧推数据比如计步传感器每秒钟上报一次步数这时候用MethodChannel就得反复拉取效率低还容易卡UI。EventChannel就是为这个设计的。鸿蒙侧作为事件源Flutter侧注册监听。真要用的时候在鸿蒙侧建立EventChannel并设置sendEvent回调Flutter侧用receiveBroadcastStream()接收。这里提醒一个容易踩的坑通道名必须全局唯一如果MethodChannel和EventChannel用了同一个名字注册的时候不会报错但事件会互相污染数据全串了。4.5 排错经验MethodChannel常见的三种翻车方式我调试过程中最常见的错误有三种排错顺序基本固定。第一通道名对不上。Flutter侧写的通道名和鸿蒙侧注册的名字哪怕差一个字符调用就直接抛MissingPluginException。解决办法就是复制粘贴不要手敲确保两端完全一致。第二参数类型不匹配。Dart侧传的Map到了鸿蒙侧不一定是同一种类型。我试过把布尔值true传过去鸿蒙侧拿到的可能是个字符串true直接用的话逻辑会出错。调试时先打日志看类型再写处理逻辑。第三通知不出现。这个大概率不是平台通道的问题而是通知权限没开或者渠道ID不对。排查链路是先确认权限声明有没有在module.json5里再去系统设置里看应用的通知开关是不是关着最后检查通知请求里的slotType和渠道ID是否合法。按这个顺序查一般十分钟内能定位问题。5. 切后台、锁屏、被杀死三种场景下的提醒存活测试5.1 测试矩阵必须覆盖的四个场景功能开发完不能只在App开着的时候测一次就完事。我建了个简单的测试矩阵覆盖生活场景里最常见的几个状态场景操作提醒表现结论App前台运行正常等待到点准时弹出基本盘必须稳定切到后台按Home键回桌面到点依然弹出Timer轮询兜底后台代理提醒锁屏熄屏等待到点锁屏界面显示提醒后台代理提醒的功劳杀死进程从最近任务划掉App到点依然弹出只有后台代理提醒能覆盖设备重启重启后等待到点默认失效需重新注册需要业务侧做恢复处理我实际测下来前三个场景都能通过第四个场景能不能响取决于你注册的是哪种提醒。如果用的是reminderAgentManager到点系统会自己拉起提醒App死不死都没关系。但如果只依赖Flutter侧Timer进程被杀死后提醒就彻底没了。5.2 设备重启后的恢复机制有个坑必须单独说一下设备重启后之前通过reminderAgentManager注册的提醒可能会失效。这意味着用户重启了一次设备喝水提醒就悄悄消失了如果不重新注册后面不会再响。解决思路是在App启动时做一次自检拉起App后主动读取本地的提醒配置把今天剩下的提醒全部重新注册一遍。同时注册一个系统事件监听检测到开机事件后尽可能触发一次自检。不过说实话开机自启在OpenHarmony上也有权限限制不是想开就能开实际做的时候要注意引导用户手动开启App一次来触发重注册。5.3 实测观察与保活建议在开发板上测下来锁屏状态下提醒照常弹出这一点让人很有信心。但我也发现不同设备的后台策略差异很大。有些设备对第三方应用的后台限制很严格App切后台几分钟就被回收有些设备则宽松得多。所以我的建议是开发者能做的都做满——选对后台代理提醒API、引导用户开通知权限、在设置页里加上提醒失效修复按钮让用户主动重注册。剩下的就交给系统不要试图去绕过系统的后台限制那既不稳定也不被允许。6. 喝水提醒只是起点生活助手功能扩展中的技术选型与坑6.1 复用调度框架久坐提醒和用药提醒喝水提醒跑通之后我第一时间意识到这套调度框架可以原样复用到其他提醒类功能上。久坐提醒、用药提醒结构和喝水提醒几乎一模一样只是提醒内容、间隔、图标不同。我把提醒配置抽象成了一个模型提醒名称、提醒内容、间隔分钟、生效时间段、是否启用。页面里配置好之后统一走同一个调度器。加一个新提醒类型只需要在设置页加一行配置项不需要重写任何调度逻辑。这个重构花了一个晚上但后续加功能省下的时间远超这个数。做生活助手类App这种配置驱动的思路越早落地越好。6.2 状态管理和页面交互的取舍生活助手类App页面不算多但状态穿插频繁今日饮水数据、提醒开关状态、健康建议文案这些数据在多个页面之间共享。我用的是flutter_cubit轻量又好理解适合这种中小型App。Bloc太重了Provider在团队协作时依赖全局注入后期维护会有点绕。cubit把状态变化收敛在独立的Cubit类里页面只负责订阅和展示逻辑清晰调试也方便。页面交互上我遇到两个高频问题。一个是TabBar点击切换时默认会有一段动画过渡闪一下总感觉不够干脆。去掉动画的办法很简单给TabController设置一个特别短的动画时长就解决了。另一个是Navigator切换页面后之前页面的状态偶尔会丢失。这个一般是因为页面没有正确处理keepAlive用AutomaticKeepAliveClientMixin包一层就能把状态保住。6.3 平台视图与其他系统能力的接入再往后扩展你可能会碰到需要展示地图、摄像头预览这类原生控件的情况。Flutter侧的通用解法是PlatformView通过视图类型ID把原生View嵌入Flutter页面。这个能力在OpenHarmony上也能用但性能和绘制效率比纯Flutter控件差一截非必要不用。蓝牙和IoT设备控制是另一个热门方向。比如用App连ESP32做个智能水杯灯Flutter侧负责UI和业务逻辑蓝牙的扫描、连接、收发数据这些动作通过MethodChannel交给鸿蒙侧做。思路和前面通知模块完全一致只是方法名换成了scanDevices、connectDevice、sendData。最后说点实在的。这个项目做完我心里最深的体会是提醒类功能的可靠性比拼功能数量重要得多。我之前一直在调通知样式、加动画效果结果发现真正决定用户是否愿意把App装进手机里的是那个提醒在锁屏状态下到底响没响。如果你打算复刻这个项目建议先跑通最小闭环——定时调度加本地通知能在切后台、锁屏、杀进程三种场景下稳定触发再考虑扩展功能。代码整体的结构我做了精简适合直接拿去改想体验的朋友可以从喝水提醒这个模块开始摸索。
返回列表