ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Flutter迁移OpenHarmony:喝水提醒功能完整实现与踩坑实践

Flutter迁移OpenHarmony:喝水提醒功能完整实现与踩坑实践 去年做设备端业务和技术调研时我把团队里已有的 Flutter 代码库尝试往 OpenHarmony 生态迁移第一个拿来练手的就是生活助手类 App 里的喝水提醒模块每天到了设定时间弹一条通知用户点一下记录喝了多少水。听起来就是一个 Timer 加一个通知真开始做才发现这个功能横跨了 Dart 层的状态管理、鸿蒙原生通知与提醒代理、本地存储、混合栈页面生命周期而且每一步都跟 Android/iOS 上的习惯不太一样。这篇文章就把我的完整实现思路和踩坑记录整理出来适合想在 OpenHarmony 上复用 Flutter 代码、或者准备做鸿蒙端生活助手类应用的开发者参考。1. 为什么在 OpenHarmony 上用 Flutter 做喝水提醒1.1 功能需求拆解喝水提醒听上去简单实际拆开看有四个独立的能力点:定时调度、系统通知、数据记录、状态展示。定时调度决定什么时候触发提醒通知决定提醒怎么触达用户数据记录负责把每次喝水的时间、杯数存下来状态展示则是首页上的今日进度、下次喝水倒计时这些 UI。任何一个环节都不能省而且它们之间的依赖关系是串行的:调度触发通知通知被点击后写记录记录最终驱动 UI 刷新。还有一个隐性需求:提醒要尽量可靠。很多同类 App 只在应用开着的时候用 Timer 弹对话框App 一旦被杀就彻底失声了。对于喝水这种高频刚需场景用户要的是系统级的、App 不在前台也能收到的提醒这就必须借助鸿蒙原生的提醒代理能力而不能只靠 Flutter 侧的前台计时器。1.2 为什么选择 Flutter 而不是纯 ArkTS 开发我在技术选型时反复权衡过纯 ArkTS 开发方案。OpenHarmony 的声明式 UI 框架这几年迭代很快写一个简单页面完全不虚但如果团队已有成熟的 Flutter 业务代码纯 ArkTS 意味着所有业务逻辑、UI 组件、状态管理方案全部重写这个成本在项目启动阶段几乎不可接受。Flutter 的优势在于 UI 层完全自绘不依赖系统组件树。这意味着页面渲染逻辑在 OpenHarmony 上的行为和 Android 上几乎一致迁移时主要改的是平台通道和原生插件层而不是 Dart 业务代码。对喝水提醒这个模块来说真正涉及鸿蒙原生的地方只有通知调度和提醒代理其他全部是纯 Dart 逻辑所以用 Flutter 做业务层、ArkTS 做宿主壳是最短路径。1.3 混合栈架构的整体设计OpenHarmony 的 HAP 应用必须有一个原生壳入口Flutter 在里面只是页面渲染引擎这跟安卓原生项目嵌入 Flutter 页面的模式本质上是一样的只是宿主从 Android 工程换成了鸿蒙工程。我的架构分成三层:宿主层:鸿蒙侧的 MainAbility 负责应用生命周期同时持有 Flutter 容器用于渲染首页、统计页等 Flutter 页面。桥接层:MethodChannel 负责 Flutter 调用鸿蒙原生能力比如发布通知、注册提醒EventChannel 负责鸿蒙原生把通知点击事件、提醒触发结果回传 Flutter。业务层:纯 Dart 实现配置管理、记录存储、倒计时状态、UI 刷新。选择 MethodChannel EventChannel 组合的原因很直接:通知的发布是典型的单向调用适合 MethodChannel提醒被点击、提醒触发回调是持续产生的事件流EventChannel 更合适。如果只用一个 MethodChannel 做轮询不仅费电而且很难做到实时性。2. 环境准备与工程接入2.1 版本选型是第一个坑Flutter 官方主线目前并不直接支持 OpenHarmony需要拉取 OpenHarmony SIG 维护的 Flutter 分支。这个分支从官方 Flutter 仓库 fork 出来持续跟进上游版本同时维护了鸿蒙侧的引擎适配和插件适配。我当时没有太在意版本匹配直接拉了一个较新的 Flutter 分支结果运行flutter doctor时立刻触发了一直被提到的警告:The current configured Flutter SDK is not known to be fully supported. Please ...这个警告的本质是:本地 Flutter SDK 版本比当前工程模板期望支持的版本要新工具链认为存在未知的兼容性风险。网上很多人遇到这个提示就直接忽略了但我在实际调试中发现版本跨度大的时候真的会出现一些诡异的编译报错问题往往很难查。我的建议是查看 SIG 分支的仓库说明确认它当前跟踪的是官方 Flutter 哪个稳定版本然后让本地分支跟工程模板保持同一基准。2.2 创建鸿蒙工程并接入 Flutter 模块创建工程的流程不算复杂但顺序很重要。先在 DevEco Studio 里新建一个 Empty Ability 工程作为宿主壳拿到包名和工程结构之后再在这个工程里集成 Flutter 模块。这里要注意不要在鸿蒙工程里直接flutter create因为 OpenHarmony 分支生成的模块结构跟普通 Flutter 工程不完全一样最好按 SIG 仓库的指引操作。接入的核心逻辑是:Flutter 代码构建出鸿蒙侧可加载的产物宿主壳启动时把 Flutter 页面挂载到指定的 Ability 上。你可以理解为鸿蒙的 HAP 是一个浏览器壳Flutter 是里面的页面引擎两者通过 build 阶段生成的中间产物完成绑定。由于不同分支的产物格式有差异这一步我强烈建议直接照抄 SIG 仓库 README 里的配置不要自己发挥。我当时就是因为少配了一个依赖项导致运行时一直报找不到 So 库排查了两天才意识到是产物没打进 HAP。2.3 构建配置中的两个高频报错构建阶段最常见的 Gralde 报错是:You are applying Flutters main Gradle plugin imperatively using the apply script method, which is not supported. Remove this apply statement ...这个报错出现在使用新版 Flutter Gradle 插件的工程里。旧版 Flutter 工程习惯在android/settings.gradle里用apply from的方式加载 Flutter 工具脚本新版插件要求改用pluginsDSL 声明式加载两者机制完全不同。我当时的解决方式是把settings.gradle里的apply from拿掉改成:plugins { id com.android.application id dev.flutter.flutter-gradle-plugin }改完记得执行一次 Gradle Sync并且让本地的 Flutter SDK 路径通过local.properties里的flutter.sdk明确指定。这个报错在鸿蒙分支上同样会出现因为鸿蒙 Flutter 分支沿用同一套构建体系排查思路完全一致。3. 喝水提醒核心功能实现3.1 数据模型与本地存储设计先把数据层定下来。喝水提醒要存两类数据:一类是用户配置包括每日目标杯数、每杯容量、提醒间隔另一类是每天的饮水记录包含喝水时间戳和杯数。配置我用 SharedPreferences 存记录我选择以 JSON 文件方式存在应用私有目录这样后续做历史统计时更容易扩展也不会被轻量存储的 key-value 限制束缚。Dart 侧的记录模型很简单:class WaterRecord { final DateTime time; final int cupVolumeMl; WaterRecord({required this.time, required this.cupVolumeMl}); MapString, dynamic toJson() { time: time.toIso8601String(), cupVolumeMl: cupVolumeMl, }; factory WaterRecord.fromJson(MapString, dynamic json) WaterRecord( time: DateTime.parse(json[time] as String), cupVolumeMl: json[cupVolumeMl] as int, ); }配置类同样用简单模型承载。这里有个小技巧:把目标的dailyGoal用杯数而不是毫升数存储避免用户修改杯容量时历史目标跟着漂移。例如目标 8 杯、每杯 200ml今天喝到 1200ml如果改成 250ml 每杯按毫升算就变成 4.8 杯了按杯数算则始终稳定。这类细节点在生活类 App 里很影响用户体验。3.2 双重提醒调度策略提醒调度是整个模块的核心我最终采用了前台计时器和系统级提醒代理并行的方案。前台计时器负责应用存活期间的即时反馈。我用Timer.periodic每 30 秒检查一次当前时间是否命中提醒窗口命中就弹应用内对话框同时刷新首页倒计时。不用固定时间的Timer是因为用户可能随时修改提醒间隔或关闭某段时间的提醒轮询方式对配置变更的响应最及时。Timer? _timer; void startReminderLoop() { _timer?.cancel(); _timer Timer.periodic(const Duration(seconds: 30), (timer) { final now DateTime.now(); if (_shouldNotifyNow(now) _checkCooldown(now)) { _triggerReminder(); } }); }_shouldNotifyNow里判断当前时间距上次喝水是否超过设定的间隔_checkCooldown保证同一时段不会重复轰炸用户。这两个判断很关键不然用户午休睡个觉起来手机上可能堆了十几条提醒。系统级提醒用的是鸿蒙的提醒代理服务。这部分必须走原生侧编码因为应用进程被清理后 Dart 代码就停了只有系统级的提醒代理能保证触达。我在鸿蒙侧通过reminderAgentManager注册一个定时提醒设置触发时间和点击后要跳转的 Ability。这里要注意把提醒的wantAgent指向自己的应用否则点击通知后无法正确唤起页面。3.3 MethodChannel 与 EventChannel 桥接原生能力Flutter 侧通过 MethodChannel 调起鸿蒙原生提醒代理。通道名我用ohos_water_app/reminder方法名scheduleReminder参数带上标题、内容和触发时间戳:static const _reminderChannel MethodChannel(ohos_water_app/reminder); Futurevoid scheduleSystemReminder({ required String title, required String content, required DateTime triggerTime, }) async { await _reminderChannel.invokeMethod(scheduleReminder, { title: title, content: content, triggerTimestamp: triggerTime.millisecondsSinceEpoch, }); }鸿蒙侧对应实现时我用了ohos.reminderAgentManager:import reminderAgentManager from ohos.reminderAgentManager; export function scheduleReminder(title: string, content: string, triggerTime: number): void { const reminder { reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER, triggerTimeInSeconds: Math.floor((triggerTime - Date.now()) / 1000), actionItems: [ { title: 喝水, type: 0 } ], wantAgent: { pkgName: com.example.waterapp, abilityName: MainAbility } }; reminderAgentManager.publishReminder(reminder) .then((id: number) { console.info(reminder published: ${id}); }) .catch((err: Error) { console.error(publish reminder failed: ${err.message}); }); }triggerTimeInSeconds是按秒计算的相对时间不是绝对时间戳我一开始传了绝对时间戳导致提醒提前了不知多少倍触发调试时差点以为是鸿蒙系统 bug。这个转换关系在文档里其实有写但很容易被忽略。通知点击事件的回传我用了 EventChannel。鸿蒙侧在用户点击通知的wantAgent回调里把信息通过 eventSink 发给 FlutterDart 侧用receiveBroadcastStream监听:static const _clickChannel EventChannel(ohos_water_app/notification_click); void listenNotificationClick() { _clickChannel.receiveBroadcastStream().listen((event) { // event 里带通知 id、点击时间等 _onNotificationClicked(event); }, onError: (error) { // 通道断开时处理 }); }这个设计让日志记录和统计变得非常顺畅。用户点了提醒通知原生侧把事件推回 DartDart 侧直接写一条喝水记录并刷新首页整个过程没有额外的轮询成本。3.4 首页交互与页面状态保持首页 UI 我分了三块:今日进度环形图、下次喝水倒计时、历史记录入口。状态管理用的是 ValueNotifier 加自定义组合没有引重量级状态管理框架因为喝水提醒的共享状态并不多ValueNotifier 足够且直观。有一个坑必须重点说:底部Tab切换到统计页再切回来首页的倒计时不走了。这个问题正是热词里讨论的flutter navigator切换页面后会丢失状态吗。默认情况下 Navigator 的 push/pop 会销毁下层路由状态不走常规保存逻辑倒计时 Timer 自然就停了。解决办法是给首页加AutomaticKeepAliveClientMixin并且把底部 Tab 切换改成IndexedStack而不是反复 push 新路由。IndexedStack 会同时保持所有子页面的状态配合 keepAlive 才能真正做到切换不丢计时器:class HomePage extends StatefulWidget { override StateHomePage createState() _HomePageState(); } class _HomePageState extends StateHomePage with AutomaticKeepAliveClientMixin { override bool get wantKeepAlive true; // build 方法中不要忘记 super.build(context) }这个操作虽然只是加一个 Mixin但它直接决定了提醒功能在真实使用中可不可靠。如果用户切一下页面就把计时器丢了这个功能等于残废。3.5 记录统计与连续天数历史记录我做了两个维度:按天聚合的总杯数和一周趋势。每天的数据从 JSON 文件里读取按日期分组后渲染。连续喝水天数这个指标稍微处理了一下:用户可能凌晨喝完就直接跨天了所以我在判断连续天数时允许当天还没结束时就算作连续中否则用户会看到连续天数偶尔倒退。统计页还顺手处理了 TabBar 的默认点击动画。热词里有人在找flutter tabbar点击取消动画效果我个人偏好切换更干脆的手感所以把 TabBar 的animationDuration设成了零同时监听了 TabController 的 index 变化来同步页面内容而不是依赖默认的动画感知。这种细节在生活类工具里很影响主观手感实际调试时值得花点时间。4. 常见问题与排查技巧实录4.1 Flutter SDK 兼容性警告问题现象是开头提到的The current configured Flutter SDK is not known to be fully supported警告。它本身不影响编译但会让人心慌而且确实可能在后续构建中暴露 API 差异。处理思路是三步走:第一步查看项目模板要求的具体 Flutter 版本一般在pubspec.yaml或工程配置里能发现痕迹。第二步检查当前flutter --version输出如果两者差距过大把 Flutter 分支切换到与模板配套的版本。第三步跑一次flutter doctor -v确认 OpenHarmony 工具链识别正常再看警告是否消失。我最后发现是工程模板太旧升级模板依赖后就干净了。4.2 构建时的 AssertionError 与缓存问题热词里有人遇到过flutter打包 java.lang.assertionerror: java.lang.exception: could not close i...这类错误。这个报错经常让人误以为是代码问题其实绝大多数时候是 Gradle 缓存或文件句柄异常。我的排查顺序是:先执行flutter clean和flutter pub get清除 Dart 侧的缓存。再删除鸿蒙工程下的build目录和 Gradle 缓存目录注意别删错了。执行./gradlew --stop干掉后台 Gradle 守护进程释放占用的文件锁。重新构建。如果重建还是失败重点检查工程路径是否包含中文或空格以及资源文件里有没有文件名带特殊字符的图片。这类问题在 Windows 和部分 CI 环境特别常见项目路径稍微复杂一点就会引发资源读取异常。4.3 Navigator 状态丢失问题深入排查前面说的AutomaticKeepAliveClientMixin是主解法但实际还有更隐秘的场景:用户在统计页停留很久系统因为内存压力回收了底层页面结果切回首页发现状态还是丢了。这时只靠 keepAlive 不够还需要在页面initState里判断是否有缓存的数据并重建状态。我的做法是在首页 State 里把倒计时目标和下次提醒时间持久化到内存缓存didChangeAppLifecycleState里监听应用回到前台事件如果发现计时器已经停止就主动重新启动。同时把喝水量、目标等关键数据在dispose前写入本地配置这样即使页面真的被销毁恢复到前台时也能无缝还原。4.4 第三方 Flutter 插件鸿蒙适配流程喝水提醒本身没有用太多第三方插件但我在这个项目里顺带摸清了给平台插件补鸿蒙适配的完整流程这个经验对后续接入登录、推送、地图等插件非常有用。以某个提供原生能力的平台插件为例适配鸿蒙的步骤是先在插件工程中新增鸿蒙原生实现新建继承自平台通道接口的类把 Android/iOS 原生方法重写为鸿蒙 API 调用然后在插件的注册入口把实现类挂上去最后在pubspec.yaml里声明鸿蒙侧的支持。流程本身不复杂难点在于大部分第三方插件的鸿蒙原生实现需要自己写工作量和插件的复杂度成正比。所以在选型阶段就要考察插件的社区维护情况尽量选那些已经在公开仓库里提供ohos目录或harmonyos适配的版本而不是自己从零补全。4.5 常见问题速查表现象根因解决方案Flutter SDK 版本警告本地 SDK 与模板要求版本不一致对齐分支版本升级模板依赖Gradle 提示 apply 方式不支持新版插件要求 plugins DSL改用 plugins 块声明打包 AssertionErrorGradle 缓存损坏或文件句柄异常clean、删 build、停 Gradle 守护进程切 Tab 后计时器停路由状态被销毁IndexedStack KeepAlive提醒时间提前triggerTimeInSeconds 误传绝对时间戳改成相对秒数后台无法提醒仅靠前台 TimerApp 被杀后失效使用 reminderAgentManager 系统提醒4.6 关于通知权限和用户可见性最后提一个容易被忽略的细节:鸿蒙系统的通知权限。用户在系统设置里关掉通知后无论 Flutter 侧还是提醒代理侧都无法触达用户所以 App 首次启动时要主动引导用户开启通知权限。我的做法是进入设置页时检查通知权限状态如果未授权弹一个引导对话框说明喝水提醒的场景然后调用鸿蒙侧打开设置授权页面。这个引导文案不能写得太单薄要说明提醒用于记录每日饮水目标不会产生广告类打扰授权率会明显高一些。写在最后就我个人而言在 OpenHarmony 上做 Flutter 开发最大的感受是功能本身不难难在版本组合和工具链的匹配。只要 Flutter 分支、鸿蒙 SDK、Gradle 插件三者版本对不上各种莫名其妙的问题就会接踵而至。如果你准备走这条路我的建议是先照着 SIG 仓库的 Release 分支配好一套可用组合锁定版本后不要再随意升级等业务跑通后再考虑版本滚动。喝水提醒虽小但把 Flutter 与 OpenHarmony 之间的桥接路径完整走通了:MethodChannel 调用原生能力EventChannel 回传实时事件本地存储管理业务数据混合栈页面处理好生命周期。这个能力组合可以直接复用到番茄钟、吃药提醒、久坐提醒等一整套生活助手功能上后续要做的只是把调度层和通知层抽成公共模块让不同业务各自注册提醒。希望这篇文章能帮你少踩几个坑尤其在版本对齐和状态保持上真的值得多花一点心思。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表