ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Flutter开发鸿蒙NEXT实战:护眼提醒APP从零到hap打包

Flutter开发鸿蒙NEXT实战:护眼提醒APP从零到hap打包 这两年的HarmonyOS NEXT把不少原生Android依赖都切断了很多人第一时间想到的替代方案是ArkUI但如果你手里已经有一套成熟的Flutter业务代码或者团队里没人写过arkts完全没必要推倒重来。我最近用Flutter做了一个护眼提醒APP目标平台直接锁鸿蒙端到端跑通了从环境配置、功能开发、平台通道调用到hap打包的完整流程。这篇文章把我踩过的坑和值得照抄的代码逻辑都整理出来给正在纠结“Flutter到底能不能好好上鸿蒙”的同学一个明确参考。这个项目选的场景是护眼提醒看起来很轻量但它恰好覆盖了Flutter跨平台开发的几个关键难点Timer计时器的生命周期可靠性、本地数据存储选型、系统通知能力的平台通道调用以及鸿蒙后台任务限制下的提醒策略。可以说把这套逻辑理顺再做更复杂的功能也只是往上堆模块的事。1. 先搞明白鸿蒙NEXT上跑Flutter和你想的不太一样1.1 为什么用Flutter而不是ArkUI原生开发我不是说ArkUI不好。客观讲HarmonyOS NEXT对ArkUI的投入很猛声明式UI、状态管理、跨端流转都做得有模有样。但技术选型不是哪个新用哪个而是看团队已有的资产能不能复用。我这个护眼提醒APP之前在Android和iOS上都已经有版本了界面、业务逻辑、数据层全是Flutter写的。如果鸿蒙单独用ArkUI重写一遍意味着两套代码库长期并行维护UI细节打磨两遍上线排期多一倍。而Flutter社区很早就有人在推动鸿蒙适配华为那边也有自己的SIG分支在维护跑通一个纯Dart业务少量系统能力调用的APP完全可行。另一个原因在于护眼提醒这个品类本身它需要的系统能力非常克制无非是一个定时器、一个通知、几个本地数据存储的key-value真正复杂的业务逻辑全部在Dart层。系统能力部分我完全可以用MethodChannel写一个薄薄的原生通道其余的一切都继续交给Flutter跨平台框架处理。有一个认知必须纠正HarmonyOS NEXT 5.0开始不再支持直接安装APK所以你不能像以前那样在鸿蒙手机上装一个Flutter打出来的Android包来“兼容运行”。Flutter要跑在鸿蒙手机上必须是Flutter代码生成鸿蒙自己的hap包Flutter引擎也必须在鸿蒙环境里有对应的适配版本。这是整个技术选型的起点所有环境配置都要围绕它展开。1.2 Flutter官方支持与OpenHarmony SIG适配现状很多人一搜“Flutter鸿蒙”就蒙了因为Flutter官方主线的flutter repo里并没有一个叫harmonyos的平台目录。鸿蒙支持目前主要由OpenHarmony SIG特别兴趣小组在推进核心仓库是OpenHarmony-SIG/flutter_flutter常用的分支是dev/ohos同时还有一个配套的flutter engine分支做鸿蒙侧引擎适配。这套东西的实际效果是你得先切换到ohos分支的Flutter SDK然后flutter create的时候加--platforms ohos参数生成工程里会出现ohos目录构建时用hvigor而不是gradle最终产物是.hap文件。整个路径走下来和以前做Android原生插件适配的体验很像只是有些细节需要自己试。版本匹配这一块很容易踩坑。我最初装的是Flutter 3.16左右的ohos分支搭配DevEco Studio 5.0.x结果构建时老是提示hvigor版本过低。后来换了SIG仓库明确兼容的组合才稳定下来。这里我给一个当时验证可行的参考组合组件参考版本Flutter SDKOpenHarmony-SIG flutter_flutter dev/ohos分支3.16系列DevEco Studio5.0.3 Release及以上HarmonyOS SDK API12以上hvigor5.0.x随DevEco安装Node.js18以上hvigor依赖这个表格不一定长期有效因为SIG仓库的更新频率不低但思路是对的不要单独装最新版Flutter再去碰运气优先盯SIG仓库的README里写了哪些版本组合经过CI验证。这样能省下大量排查编译错误的时间。1.3 选型小结与技术边界用Flutter做鸿蒙APP的边界我这次也摸得很清楚纯Dart的UI、状态管理、网络库、本地key-value存储基本都能直接跑难点集中在设备平台相关的插件上比如支付IAP、系统图库、推送SDK这类需要鸿蒙原生服务支撑的功能必须找鸿蒙适配版或者自己写通道。护眼提醒APP恰恰避开了这些高难度依赖。通知能力我用原生通道自己实现本地数据存储我选了纯Dart实现的Hive整个项目没有引用任何需要深度绑定Android/iOS系统的第三方插件。这也算一个选型经验如果想降低Flutter鸿蒙的风险功能设计上要刻意“去平台化”把需要系统能力的部分收敛到几个接口后面。2. 环境搭建省下3小时的版本匹配清单2.1 必备工具与版本对应表环境搭建是第一个劝退点。如果你直接用flutter官方SDK去执行flutter create --platforms ohos大概率会报错说不认识这个平台。因为我前面提到的ohos支持只存在于SIG分支里普通Flutter SDK不包含。我这次的实际安装顺序是这样的安装DevEco Studio它会顺带装好HarmonyOS SDK、hvigor和相关命令行工具。把OpenHarmony-SIG的flutter_flutter仓库clone下来切到dev/ohos分支。把该分支下的bin目录配到PATH里重新打开终端让flutter命令生效。执行flutter doctor确认flutter命令能跑起来再检查环境变量。装Node.js因为在构建hap时hvigor会调用npm相关能力。这套顺序里最容易犯的错是先去配Android SDK或者把旧版Flutter的路径残留着。同一个终端里如果先加载了旧Flutter的PATH再加载ohos分支flutter --version显示的还是旧版本后面所有步骤全乱。我当时是直接用一个独立的终端Profile来管理Flutter SDK路径的避免和日常Android开发环境冲突。2.2 接入ohos平台的两种方式接入流程分两种情况。第一种是全新项目直接敲flutter create --platforms ohos eye_care_app这样生成的项目会同时带上lib目录和ohos原生工程目录ohos目录的结构和HarmonyOS标准工程一致里面会预置好Flutter引擎的依赖引用。第二种是已有Flutter项目需要把它扩展为支持ohos平台flutter create --platforms ohos .在已有项目根目录执行后它会自动补出ohos子目录。这里要特别注意执行之前先把项目里引用的第三方插件清一遍凡是没有ohos平台实现的插件在flutter pub get阶段就可能报错。我当时遇到最多的问题就是某个插件只有android和ios目录没有ohos目录hvigor去编的时候直接找不到对应的har依赖。2.3 本地构建配置环境变量与签名环境变量这块不同版本的SDK要求不太一样我最后配置的是这两个export OHOS_SDK_HOME你的DevEco Sdk目录 export DEVECO_SDK_HOME你的DevEco Sdk目录调试时在flutter项目下的ohos目录里运行hvigorw assembleHap --mode module -p productdefault或者直接用Flutter侧的命令flutter build hap --debug签名是很多人的第一道坎。HarmonyOS真机安装hap必须签名否则安装阶段直接失败。最简单的方式是在DevEco Studio里打开项目的ohos目录进入Project Structure里的Signing Configs登录华为账号后让它自动生成签名文件。它会自动在build-profile.json5里写入对应的证书信息之后命令行构建也会读取这份配置。有一点要提醒签名是跟着包名走的如果你Flutter项目里设置的应用包名和ohos工程里的bundleName不一致签名成功但安装到真机上会提示应用异常。我第一次跑真机就撞上了这个后来统一在flutter create时就指定好包名后面不再改来改去。3. 护眼提醒APP的需求拆解与数据模型3.1 最小可用功能清单护眼提醒这个品类市面上很多但真正落到逻辑上无非就几件事提醒用户别过度用眼引导用户休息记录用户的用眼数据。我没有一上来就做花哨的AI识别坐姿、疲劳检测那种功能而是先稳住最小可用版本用眼计时记录当前一段连续用眼的时长达到阈值后进入提醒状态。到点提醒弹全屏休息页并发送系统通知告知用户需要休息。休息倒计时休息页大字显示剩余时间倒计时结束自动回到专注状态。护眼贴士休息页随机展示一条用眼健康建议。每日统计本地记录当天累计用眼时长、休息次数和休息时长。基础设置允许用户自定义用眼时长阈值和休息时长。这个功能清单有个明显特点每一项都不依赖远端服务器也不依赖复杂系统API非常适合作为Flutter鸿蒙这个组合的验证项目。做完以后核心代码可以直接迁移回Android/iOS版本复用。3.2 UI交互流程与界面结构交互流程我设计成一条直线降低用户理解成本主页显示今日累计与当前状态 - 点击开始专注 - 用眼计时中 - 到达阈值 - 全屏休息页 - 倒计时结束 - 回到主页 - 生成一条统计记录页面结构上分了四个页面加一个控制器lib/ main.dart controllers/eye_care_controller.dart models/eye_care_config.dart models/daily_record.dart pages/home_page.dart pages/rest_page.dart pages/stats_page.dart pages/settings_page.dart services/notification_service.dart services/storage_service.dart颜色主题我没有用什么花哨设计用Material 3的ColorScheme.fromSeed直接以护眼绿为种子色生成一套配色final theme ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4CAF50)), );界面风格主打大数字、大按钮、高对比度。因为护眼App的使用场景往往是用户盯屏幕已经很累了界面再花哨就是另一种视觉负担。主页中央一个半透明进度环环内显示当前用眼时长或剩余休息时间环下方一个大按钮切换开始/暂停。3.3 本地数据模型设计数据层我用两个模型就够了正好对应设置项和统计项。第一个模型是全局配置class EyeCareConfig { final int focusThresholdMinutes; final int restMinutes; const EyeCareConfig({ this.focusThresholdMinutes 25, this.restMinutes 5, }); }第二个模型是每日记录以自然日为单位存储class DailyRecord { final String dateKey; // 如 2025-06-01 final int focusSeconds; final int breakCount; final int breakSeconds; const DailyRecord({ required this.dateKey, this.focusSeconds 0, this.breakCount 0, this.breakSeconds 0, }); DailyRecord copyWith({ int? focusSeconds, int? breakCount, int? breakSeconds, }) { return DailyRecord( dateKey: dateKey, focusSeconds: focusSeconds ?? this.focusSeconds, breakCount: breakCount ?? this.breakCount, breakSeconds: breakSeconds ?? this.breakSeconds, ); } }这两个模型的设计遵循一个原则不存“开始时间”这种过程态只存可累加的数值。因为过程态要依赖定时器连续触发才能保持而可累加的数值可以用时间戳差值随时补算就算App被杀死一段时间恢复后也能把这段时长补进记录里。4. 核心逻辑实现计时、存储与提醒的三段式落地4.1 用眼计时器的可靠实现方式护眼提醒最核心的就是计时器。很多新手会直接写一个Timer.periodic(Duration(seconds: 1), ...)然后每秒去减倒计时。这在App保持前台运行时没问题但一旦锁屏、切后台、被系统省电策略挂起Timer回调就会停掉导致计时严重失真。我的做法是Timer只负责每秒刷新UI真正的时间记录靠DateTime.now()的时间戳差值计算。简单说Timer是显示驱动时间戳才是数据源。Timer? _ticker; DateTime? _focusStartTime; void startFocus() { _focusStartTime DateTime.now(); state EyeCareState.focusing; _focusElapsed Duration.zero; _ticker?.cancel(); _ticker Timer.periodic(const Duration(seconds: 1), (_) { final elapsed DateTime.now().difference(_focusStartTime!); _focusElapsed elapsed; notifyListeners(); if (elapsed.inMinutes config.focusThresholdMinutes) { triggerBreak(); } }); }你可能会问既然Timer在后台会停止那后台这段时间的计时不就断了答案是重启Timer的那一瞬间时间戳差值会自动补上根本不需要逐秒累积。而提醒这件事不能只靠Dart层Timer必须依赖鸿蒙原生通知或代理提醒能力这部分放到后面讲。4.2 休息倒计时与状态切换休息倒计时的实现和专注计时原理一样用结束时间减去当前时间得到剩余时长。区别在于休息状态必须是一个明确的“终态”用户不能看两眼手机就算休息过了。我定义了一个状态枚举enum EyeCareState { idle, focusing, resting }进入休息状态时记录restEndTime倒计时每秒刷新剩余秒数归零后自动回到idle并保存一条统计数据void startRest() { _ticker?.cancel(); state EyeCareState.resting; _restEndTime DateTime.now().add(Duration(minutes: config.restMinutes)); _ticker Timer.periodic(const Duration(seconds: 1), (_) { final remain _restEndTime!.difference(DateTime.now()); if (remain.isNegative) { finishRest(); } else { _restRemain remain; notifyListeners(); } }); } void finishRest() { _ticker?.cancel(); state EyeCareState.idle; _todayRecord _todayRecord.copyWith( breakCount: _todayRecord.breakCount 1, breakSeconds: _todayRecord.breakSeconds config.restMinutes * 60, ); _storageService.saveTodayRecord(_todayRecord); notifyListeners(); }状态切换的关键是所有分支都收敛到这三个状态里不会出现既在专注又在休息的中间态。这也方便后面做界面联动rest页和home页直接根据state判断谁显示。4.3 本地数据库Hive记录每日数据本地存储我选了Hive原因是它纯Dart实现不依赖平台原生数据库在Flutter鸿蒙这种三方插件兼容性不确定的情况下风险最低。它处理配置和每日统计这种轻量数据非常合适不需要引入sqflite或者drift那种偏重的方案。初始化两步走void main() async { WidgetsFlutterBinding.ensureInitialized(); final dir await getApplicationSupportDirectory(); Hive.init(dir.path); runApp(const EyeCareApp()); }注意getApplicationSupportDirectory来自path_provider插件而path_provider本身也要有ohos适配。如果你怕这个也有坑可以手动拿鸿蒙的沙箱路径传给Hive.init这个思路是通用的。我实际测试时path_provider的ohos适配版是可以工作的如果你用的Flutter分支版本旧自己传路径也完全可行。存储服务封装如下class StorageService { static const _boxName eye_care_box; late Box _box; Futurevoid init() async { _box await Hive.openBox(_boxName); } EyeCareConfig getConfig() { final focus _box.get(focusThreshold, defaultValue: 25) as int; final rest _box.get(restMinutes, defaultValue: 5) as int; return EyeCareConfig(focusThresholdMinutes: focus, restMinutes: rest); } void saveConfig(EyeCareConfig config) { _box.put(focusThreshold, config.focusThresholdMinutes); _box.put(restMinutes, config.restMinutes); } DailyRecord getTodayRecord() { final key _todayKey(); final focusSeconds _box.get(focus_$key, defaultValue: 0) as int; final breakCount _box.get(break_count_$key, defaultValue: 0) as int; final breakSeconds _box.get(break_seconds_$key, defaultValue: 0) as int; return DailyRecord(dateKey: key, focusSeconds: focusSeconds, breakCount: breakCount, breakSeconds: breakSeconds); } }用日期拼key的好处是无需遍历天然支持按天统计。如果你想扩展每周/每月趋势多存一份周维度或月维度的box就行。4.4 触发提醒平台通道调用鸿蒙原生通知前面已经反复强调过纯Dart层的Timer在后台不可靠所以提醒动作必须交给鸿蒙原生能力。我在Dart侧定义了一个细薄的NotificationService统一负责所有提醒相关调用class NotificationService { static const _channel MethodChannel(com.eyecare.app/ohos); Futurevoid showRestNotification() async { try { await _channel.invokeMethod(sendNotification, { title: 该休息啦, content: 连续用眼已到设定时间起来活动一下吧, }); } on PlatformException catch (e) { debugPrint(通知发送失败: ${e.message}); } } }这里有个设计决策值得说一下为什么不在Dart层用flutter_local_notifications因为这个插件默认支持Android/iOS鸿蒙适配并不成熟与其等一个不确定的插件更新不如自己写一个20行左右的MethodChannel。护眼提醒这种场景只需要发一条文案固定的通知完全够用。MethodChannel在鸿蒙侧的具体实现放到下一节讲那是整个鸿蒙适配的关键。5. 鸿蒙侧的适配细节权限、后台与生命周期5.1 通知权限与后台代理提醒鸿蒙的通知体系要分两层看一层是普通应用通知走的是notificationManager应用在前台或后台都可以发送另一层是系统代理提醒类似“闹钟提醒”走reminderAgentManager它的优势是即使应用进程被系统杀掉到点了一样能拉起提醒。护眼提醒这个场景“到点提醒”是刚需用户不希望App被杀掉就完全没有提醒。所以理想方案是同时做两手在App存活的情况下用notificationManager发布一条普通通知展示在通知栏。同时注册一个reminderAgentManager的定时提醒作为兜底保证进程被杀也能触发。权限声明需要在ohos工程里的module.json5中加上相关权限字段并在代码里引导用户打开通知开关。这里提个容易踩的坑静态权限声明和用户开关不是一回事鸿蒙对通知的控制更接近“用户可关、应用只能引导”所以代码里要有一个“去设置打开通知”的引导入口。5.2 应用生命周期与计时校准App在前台和后台切换时计时器需要做校准。Flutter本身提供WidgetsBindingObserver或AppLifecycleListener鸿蒙侧的生命周期也会通过Flutter引擎透传到Dart层所以这套机制在Flutter鸿蒙里是通用的。我用的处理方式AppLifecycleListener( onResume: () { // 回到前台时重新校准计时不用依赖Timer是否连续触发 controller.recalcElapsedAtResume(); }, onInactive: () { // 记录进入后台的时刻 controller.markBackground(); }, );onResume里做的事情很简单重新读取DateTime.now()把当前时刻减去进入后台的时刻补进专注时长或者从休息剩余时间里扣掉。这个过程的可靠性建立在时间戳差值上不依赖Timer的连续性所以即使鸿蒙系统在后台把Flutter的Timer整个冻结回到前台也能还原真实时长。5.3 平台通道的ArkTS侧实现MethodChannel的鸿蒙侧实现是这次项目里最有价值的原生代码。在ohos工程的主页Ability里注册通道ArkTS侧大致长这样import { MethodCall, MethodChannel, common } from kit.AbilityKit; import { notificationManager } from kit.NotificationKit; import { BusinessError } from kit.BasicServicesKit; const CHANNEL_NAME com.eyecare.app/ohos; function sendNotification(title: string, content: string): void { const request: notificationManager.NotificationRequest { id: 1, content: { notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title: title, text: content, }, }, }; notificationManager.publish(request).catch((err: BusinessError) { console.error(publish failed, code is ${err.code}, message is ${err.message}); }); } export function registerEyeCareChannel(context: common.UIAbilityContext): void { const channel new MethodChannel(context, CHANNEL_NAME); channel.setMethodCallHandler((call: MethodCall, promise) { if (call.method sendNotification) { const title call.arguments[title] as string; const content call.arguments[content] as string; sendNotification(title, content); promise.resolve(true); } else { promise.reject(new Error(method not found)); } }); }这段代码需要登录鸿蒙开发者账号、申请通知权限才能完整跑通。在我的实测中普通通知在应用处于前台和后台时都能正常弹出这个体验和Android原生通知很接近。6. 真机调试与hap打包过程中的常见报错6.1 签名与包名配置真机调试的第一个关卡就是包名统一。Flutter项目里的applicationId和ohos工程里的bundleName必须一致否则签名后安装会出现应用异常。我在flutter create时用它默认的生成规则创建然后再把DevEco工程里的bundleName改成同一个值。签名配置我推荐在DevEco Studio的Signing Configs里自动生成打开ohos目录对应的DevEco工程。进入File - Project Structure - Signing Configs。勾选Automatically generate certificate登录华为账号。等待生成cer和p12文件并自动写入build-profile.json5。命令行构建时会自动读取这份签名配置不需要再手动指定。6.2 编译报错清单这部分我把这一路上遇到的高频报错整理成一张表方便你遇到时直接定位报错/现象原因解决方案提示无法识别的平台ohosFlutter SDK不是ohos分支换成OpenHarmony-SIG的flutter_flutter切分支hvigor版本过低新版构建脚本需要更高hvigor升级DevEco Studio或手动指定高版本hvigor依赖某个pub依赖找不到ohos实现插件只写了android/ios目录找社区适配版或把对应功能改为MethodChannel自研hap安装失败提示证书错误签名配置过期或包名不一致重新生成签名核对bundleName与Flutter包名一致真机运行时MethodChannel调用没响应通道名不一致或未在正确Ability注册核对Dart侧和ArkTS侧通道名注册放onWindowStageCreate之后构建产物运行即闪退Flutter engine版本与DevEco SDK不匹配换用SIG仓库README验证过的SDK版本组合这里我要特别强调第一个报错很多人的Flutter环境早装好了直接跑flutter create --platforms ohos就会报错。这个错不是你项目的问题是SDK分支的问题别在项目文件里反复找原因。第二个高频问题是第三方插件。护眼提醒App里我用了path_provider和hive这两个都有ohos适配痕迹所以问题不大。但如果你引用了类似video_player、image_picker、微信登录这类插件就要提前确认是否支持ohos不支持的话要么替换成鸿蒙侧原生实现要么放弃该功能。你可能会看到“flutter兼容鸿蒙拉起iap支付”这类讨论思路其实一样支付这类强系统能力依赖最终都绕不开鸿蒙原生SDKFlutter侧只是封装调用关键看原生适配是否到位。6.3 性能与耗电实测我在一台HarmonyOS NEXT真机上跑了一个星期的日常使用测试感受比较真实Flutter页面滑动流畅度没问题60帧渲染稳定浅色护眼主题下没有明显发热。应用包体积比空Flutter工程大一些因为要带鸿蒙引擎库不过对于工具类App完全可以接受。耗电主要集中在前台计时刷新的场景后台退到桌面后CPU占用会迅速降下来。Timer在后台确实会停但因为我用的是时间戳差值回到前台后补算正确用户不会发现统计数据“少了一段”。这个项目形态很适合做性能验证它既有持续的前台动画刷新又有隔一段时间才触发的后台通知基本把Flutter鸿蒙常见运行时路径都覆盖到了。如果你后面要做更重的业务可以参照这个测试方式来评估瓶颈。7. 一些只有做完整流程才会有的体会整个过程跑下来我的判断是Flutter跑鸿蒙这条路已经走通了但它现在更适合理想型项目而不是什么功能都无缝迁移的万能方案。所谓理想型就是业务逻辑尽量集中在Dart层、对第三方插件依赖少、系统能力需求克制。护眼提醒APP恰好就是这样的项目所以整体风险被我控制在一个很小的范围内。如果让我给你的项目提建议我会说动手之前先做一次“插件适配体检”把你需要的所有pub依赖查一遍凡是只写了android/ios目录的要么找ohos适配版要么想清楚备选实现。这一项检查比任何环境配置教程都重要它能直接决定你是顺利编译还是卡在第一天。二次开发方面这个项目还可以继续加很多有意思的东西把每日统计通过后端同步到多设备接入蓝牙硬件做坐姿监测或者根据时间动态调节护眼提示强度。这些扩展点在架构上都已经留好了位置Flutter的跨平台能力会在后续版本里体现出更大的复利价值。
返回列表