Flutter 三方库 screen_state 的 OpenHarmony 适配实战
本文记录了将开源 Flutter 三方库
screen_state(v5.0.2)适配到 OpenHarmony / HarmonyOS 平台的完整过程,
包含适配思路、代码改动对照、关键决策和踩坑复盘。
一、背景
1.1 三方库简介
screen_state 是 Flutter 社区(Copenhagen Center for Health Technology,CACHET / DTU)开发的一款屏幕状态监听插件,提供以下能力:
- 亮屏事件(SCREEN_ON)— 设备屏幕点亮时推送事件
- 熄屏事件(SCREEN_OFF)— 设备屏幕熄灭时推送事件
- 解锁事件(SCREEN_UNLOCKED)— 用户解锁设备时推送事件
- 后台运行监听— 应用退到后台后仍持续接收屏幕状态事件
- 统一事件流 API— 通过
Stream<ScreenStateEvent>消费所有事件
该三方库最初支持Android / iOS两个平台,本次任务将其适配到OpenHarmony / HarmonyOS平台。
项目地址:https://atomgit.com/oh-flutter/screen_state
1.2 适配目标
| 维度 | 要求 |
|---|---|
| 功能一致性 | 亮屏/熄屏/解锁三类事件与 Android、iOS 行为一致 |
| Dart 层零改动 | API 形态(screenStateStream、ScreenStateEvent)保持不变,Dart 层仅补平台判断 |
| 性能 | 事件订阅按需创建(onListen)、及时释放(onCancel),避免资源泄漏 |
| 工程规范 | 遵循 Flutter OHOS 插件标准结构(ohos/HAR 模块 +pubspec.yaml注册 + example 工程) |
二、适配路线图
整个适配分为 4 个阶段:
第 1 阶段:适配评估 ── 必要性评估、阅读 Android/iOS 原生实现、确认鸿蒙侧等价 API 第 2 阶段:原生实现 ── 创建 ohos/ 目录,编写 ArkTS ScreenStatePlugin(EventChannel) 第 3 阶段:插件注册 ── pubspec.yaml 注册 ohos 平台,Dart 层补 Platform.isOhos 第 4 阶段:示例验证 ── flutter create 生成 ohos 示例工程,构建 HAP 验证三、逐步适配过程
第 1 阶段:适配评估
1.1 必要性评估
适配前先回答三个问题:
| 检查项 | 结果 |
|---|---|
| 插件是否含平台原生代码? | ✅ 是(Android Kotlin + iOS Swift) |
| 是否使用平台通道(MethodChannel / EventChannel)? | ✅ 是(EventChannel('screenStateEvents')) |
仓库中是否已有ohos/目录? | ❌ 否 |
结论:需要鸿蒙化。插件通过原生EventChannel推送屏幕事件,OHOS 无现成支持,必须实现 ArkTS 原生层。
1.2 阅读原生实现,提取通信契约
Dart 侧契约(lib/screen_state.dart):
enumScreenStateEvent{screenUnlocked,screenOn,screenOff;// Android 返回 intent action;iOS/OHOS 返回短名称Stringgetname{...}// 两种名称形式都可解析staticScreenStateEventfromName(Stringname){switch(name){case'SCREEN_UNLOCKED':case'android.intent.action.USER_PRESENT':returnScreenStateEvent.screenUnlocked;// ...}}}Stream<ScreenStateEvent>getscreenStateStream=>_screenStateStream??=Platform.isAndroid||Platform.isIOS// ← 需加 Platform.isOhos?_screenStateStream??=_eventChannel.receiveBroadcastStream().map((event)=>ScreenStateEvent.fromName(event)):Stream<ScreenStateEvent>.empty();Android 侧实现(Kotlin):
// ScreenStatePlugin.ktpublicclassScreenStatePlugin:FlutterPlugin,EventChannel.StreamHandler{overridefunonAttachedToEngine(binding:FlutterPlugin.FlutterPluginBinding){eventChannel=EventChannel(binding.binaryMessenger,"screenStateEvents")context=binding.applicationContext eventChannel.setStreamHandler(this)}overridefunonListen(arguments:Any?,events:EventChannel.EventSink?){screenReceiver=ScreenReceiver(events)valfilter=IntentFilter()filter.addAction(Intent.ACTION_SCREEN_ON)// 亮屏filter.addAction(Intent.ACTION_SCREEN_OFF)// 熄屏filter.addAction(Intent.ACTION_USER_PRESENT)// 解锁context!!.registerReceiver(screenReceiver,filter)}overridefunonCancel(arguments:Any?){context!!.unregisterReceiver(screenReceiver)}}// ScreenReceiver.kt —— 收到广播后直接把 intent.action 字符串推给 DartclassScreenReceiver(privatevaleventSink:EventSink?):BroadcastReceiver(){overridefunonReceive(context:Context,intent:Intent){eventSink?.success(intent.action)}}契约总结:
| 契约项 | 值 |
|---|---|
| 通道类型 | EventChannel(非 MethodChannel) |
| 通道名 | screenStateEvents |
| 事件负载 | 事件名字符串(Android 为 intent action,iOS 为短名称) |
| 生命周期 | onListen注册 /onCancel注销 |
关键认知:screen_state 用的是EventChannel + StreamHandler,而不是常见的 MethodChannel + MethodCallHandler,这是本次适配与普通插件最大的不同点。
第 2 阶段:原生实现(核心)
2.1 整体架构对比
Android (Kotlin) OHOS (ArkTS) ──────────────────── ──────────────────── class ScreenStatePlugin class ScreenStatePlugin implements FlutterPlugin, implements FlutterPlugin, EventChannel.StreamHandler StreamHandler import io.flutter... import { FlutterPlugin, import android.content... FlutterPluginBinding, EventChannel, EventSink, StreamHandler } from '@ohos/flutter_ohos' BroadcastReceiver(系统广播) commonEventManager(公共事件订阅)2.2 事件通道注册
| 平台 | 代码 |
|---|---|
| Android | EventChannel(binding.binaryMessenger, "screenStateEvents") |
| OHOS | new EventChannel(binding.getBinaryMessenger(), "screenStateEvents") |
差异:OHOS 使用
getBinaryMessenger(),与 Android 的binaryMessenger属性等价,均从FlutterPluginBinding获取。
2.3 事件源对照
| 平台 | 事件源 | 注册时机 | 注销时机 |
|---|---|---|---|
| Android | BroadcastReceiver监听ACTION_SCREEN_ON/OFF/USER_PRESENT | onListen | onCancel |
| OHOS | commonEventManager订阅COMMON_EVENT_SCREEN_ON/OFF/SCREEN_UNLOCKED/USER_PRESENT | onListen | onCancel |
2.4 ArkTS 核心实现(ScreenStatePlugin.ets)
import{EventChannel,EventSink,FlutterPlugin,FlutterPluginBinding,Log,StreamHandler,}from'@ohos/flutter_ohos';importcommonEventManagerfrom'@ohos.commonEventManager';import{BusinessError}from'@kit.BasicServicesKit';constCHANNEL_NAME:string='screenStateEvents';// 将系统公共事件 id 映射为 Dart 层可识别的事件名functiontoScreenStateEvent(eventId:string):string{switch(eventId){casecommonEventManager.Support.COMMON_EVENT_SCREEN_ON:return'SCREEN_ON';casecommonEventManager.Support.COMMON_EVENT_SCREEN_OFF:return'SCREEN_OFF';casecommonEventManager.Support.COMMON_EVENT_SCREEN_UNLOCKED:casecommonEventManager.Support.COMMON_EVENT_USER_PRESENT:// 兼容旧版本return'SCREEN_UNLOCKED';default:returneventId;}}exportdefaultclassScreenStatePluginimplementsFlutterPlugin,StreamHandler{privateeventChannel:EventChannel|null=null;privateeventSink:EventSink|null=null;privatesubscriber:commonEventManager.CommonEventSubscriber|null=null;getUniqueClassName():string{return'ScreenStatePlugin';// 必须与 pubspec.yaml 的 pluginClass 一致}onAttachedToEngine(binding:FlutterPluginBinding):void{this.eventChannel=newEventChannel(binding.getBinaryMessenger(),CHANNEL_NAME);this.eventChannel.setStreamHandler(this);}onDetachedFromEngine(binding:FlutterPluginBinding):void{this.eventChannel?.setStreamHandler(null);this.eventChannel=null;this.unsubscribeScreenEvents();}onListen(args:Object,events:EventSink):void{this.eventSink=events;this.subscribeScreenEvents();}onCancel(args:Object):void{this.eventSink=null;this.unsubscribeScreenEvents();}privateasyncsubscribeScreenEvents():Promise<void>{if(this.subscriber!=null)return;letsubscribeInfo:commonEventManager.CommonEventSubscribeInfo={events:[commonEventManager.Support.COMMON_EVENT_SCREEN_ON,commonEventManager.Support.COMMON_EVENT_SCREEN_OFF,commonEventManager.Support.COMMON_EVENT_SCREEN_UNLOCKED,commonEventManager.Support.COMMON_EVENT_USER_PRESENT,],};try{this.subscriber=awaitcommonEventManager.createSubscriber(subscribeInfo);commonEventManager.subscribe(this.subscriber,(err:BusinessError,data:commonEventManager.CommonEventData)=>{if(err||this.eventSink==null)return;leteventName:string=toScreenStateEvent(data.event);this.eventSink.success(eventName);});}catch(error){Log.e(TAG,'createSubscriber error: '+JSON.stringify(error));}}privateunsubscribeScreenEvents():void{if(this.subscriber==null)return;commonEventManager.unsubscribe(this.subscriber);this.subscriber=null;}}2.5 实现差异详解
适配中遇到的最大差异是事件名的归一化:Android 直接推送intent.action(如android.intent.action.SCREEN_ON),而 OHOS 的系统公共事件 id 是usual.event.SCREEN_ON形式。好在 Dart 层ScreenStateEvent.fromName只认识SCREEN_ON/SCREEN_OFF/SCREEN_UNLOCKED与android.intent.action.*两种形式,因此选择在原生层统一转换为短名称。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 原生层映射为短名称✅ | Dart 层零改动,与 iOS 行为一致 | 需要维护一张映射表 |
Dart 层扩展解析usual.event.* | 原生层逻辑简单 | 需改 Dart 公共 API,破坏"零改动"目标 |
第 3 阶段:插件注册
3.1 pubspec.yaml 注册 ohos 平台
flutter:plugin:platforms:android:package:dk.cachet.screen_statepluginClass:ScreenStatePluginios:pluginClass:ScreenStatePluginohos:# ← 新增pluginClass:ScreenStatePlugin# ← 与 getUniqueClassName() 一致3.2 Dart 层补平台判断
lib/screen_state.dart中screenStateStream的平台守卫增加Platform.isOhos:
Stream<ScreenStateEvent>getscreenStateStream=>_screenStateStream??=Platform.isAndroid||Platform.isIOS||Platform.isOhos?_screenStateStream??=_eventChannel.receiveBroadcastStream().map((event)=>ScreenStateEvent.fromName(event)):Stream<ScreenStateEvent>.empty();第 4 阶段:示例验证
4.1 生成 OHOS 示例工程
在example/目录下执行 Flutter 官方命令生成 OHOS 宿主工程:
flutter create.--platforms=ohos该命令自动生成example/ohos/目录(50 个文件),包含:
example/ohos/ ├── AppScope/app.json5 # 应用配置 ├── build-profile.json5 # 项目构建配置(含 signingConfigs、SDK 版本) ├── hvigor/hvigor-config.json5 # 构建工具配置 ├── oh-package.json5 # 顶层包配置 ├── hvigorfile.ts # 构建入口 └── entry/ ├── build-profile.json5 ├── oh-package.json5 └── src/main/ ├── module.json5 # entry 模块配置 ├── ets/ │ ├── entryability/ │ │ └── EntryAbility.ets # Ability 生命周期 │ ├── pages/ │ │ └── Index.ets # UI 页面(Flutter 容器) │ └── plugins/ │ └── GeneratedPluginRegistrant.ets # 自动注册 ScreenStatePlugin └── resources/rawfile/flutter_assets/ # Flutter 运行时资源GeneratedPluginRegistrant.ets由 Flutter 工具自动生成并注册插件:
import{FlutterEngine,Log}from'@ohos/flutter_ohos';importScreenStatePluginfrom'screen_state';// ← 从插件包导入exportclassGeneratedPluginRegistrant{staticregisterWith(flutterEngine:FlutterEngine){try{flutterEngine.getPlugins()?.add(newScreenStatePlugin());}catch(e){...}}}4.2 构建验证
使用 DevEco Studio 的 hvigor 命令行构建 HAP:
nodehvigorw.js--modemodule-pmodule=entry@default-pproduct=default\-prequiredDeviceType=phone assembleHap--analyze=normal--parallel--incremental--daemon产物:entry/build/default/outputs/default/entry-default-unsigned.hap
四、完整代码对照
4.1 Android vs OHOS 完整实现对照
| 维度 | Android (Kotlin) | OHOS (ArkTS) |
|---|---|---|
| 语言 | Kotlin | ArkTS (TypeScript 语法) |
| 插件接口 | FlutterPlugin, EventChannel.StreamHandler | FlutterPlugin, StreamHandler |
| 类注册 | @Override注解 + Flutter 自动发现 | getUniqueClassName()返回类名 |
| 通道获取 | binding.binaryMessenger | binding.getBinaryMessenger() |
| 事件源 | BroadcastReceiver+IntentFilter | commonEventManager.createSubscriber+subscribe |
| 事件发送 | eventSink?.success(intent.action) | eventSink.success(eventName) |
| 注销时机 | onCancel→unregisterReceiver | onCancel→unsubscribe |
4.2 关键 ArkTS 语法差异
| Android 语法 | ArkTS 语法 | 备注 |
|---|---|---|
import io.flutter.plugin.common.EventChannel | import { EventChannel } from '@ohos/flutter_ohos' | OHOS 使用模块化导入 |
override fun onListen(arguments: Any?, events: EventSink?) | onListen(args: Object, events: EventSink): void | 参数类型Any?→Object |
context.registerReceiver(receiver, filter) | commonEventManager.subscribe(subscriber, cb) | 广播 → 公共事件 |
intent.action | data.event | 注意字段名不同:OHOS 是event而非eventId |
五、关键决策说明
决策 1:保持通道名不变
Dart 层EventChannel('screenStateEvents')已固定,OHOS 原生侧必须使用完全相同的通道名。通道名是 Dart 与原生之间的通信契约,改变会导致 Dart 端收不到任何事件。
维护策略:通道名集中定义在常量CHANNEL_NAME中,后续修改只需动一处。
决策 2:事件名归一化到短名称(与 iOS 一致)
Android 原生推送intent.action(如android.intent.action.SCREEN_ON),而 OHOS 系统事件 id 是usual.event.SCREEN_ON。Dart 层fromName只认识SCREEN_ON/SCREEN_UNLOCKED两种形式,因此选择在原生层将事件 id 映射为短名称。
维护策略:映射函数toScreenStateEvent()独立成纯函数,新增事件类型时只需补充 switch 分支。
决策 3:不声明任何权限
适配初版在module.json5声明了ohos.permission.RECEIVE_SCREEN_EVENTS,构建时发现该权限不存在于 SDK 预定义列表(00303221 Configuration Error)。查证官方文档确认:COMMON_EVENT_SCREEN_ON/OFF/USER_PRESENT的订阅者所需权限:无,三方应用可直接订阅。
维护策略:遵循官方文档"订阅者所需权限:无"的结论,module.json5 不再声明权限。
决策 4:双事件源兼容解锁事件
COMMON_EVENT_USER_PRESENT在新版 HarmonyOS 中已标记弃用(@useinstead COMMON_EVENT_SCREEN_UNLOCKED),但为兼容旧版本系统,同时订阅两个事件并映射到SCREEN_UNLOCKED。
维护策略:保留两个订阅,避免老设备上解锁事件丢失。
决策 5:Dart 层零改动(仅补平台判断)
Dart 公共 API(Screen、ScreenStateEvent、screenStateStream)完全保持原样,仅将平台守卫从Platform.isAndroid || Platform.isIOS扩展为|| Platform.isOhos。示例工程则补充TargetPlatform.ohos判断——这是排查"鸿蒙不生效"问题时的关键发现:example 的_isSupportedPlatform未含 ohos,导致 UI 层从未调用startListening()。
维护策略:所有平台相关判断集中在_isSupportedPlatform/screenStateStream守卫中,便于统一维护。
六、测试与验证
测试环境
| 项目 | 版本 |
|---|---|
| Flutter | 3.41.10-ohos-1.0.0 |
| Dart | 3.11.5 |
| HarmonyOS SDK | 26.0.0(API 26) |
| IDE | DevEco Studio 26.0.0 |
| 设备 ROM | ALN-AL00 7.0.0.105(SP6C00E105R4P3) |
验证要点
- 静态分析—
flutter analyze通过,无警告无错误。 - 编译验证— hvigor
assembleHap构建成功(43 tasks),产出entry-default-unsigned.hap。 - 插件注册—
GeneratedPluginRegistrant.ets正确导入并注册ScreenStatePlugin(import ScreenStatePlugin from 'screen_state')。 - 事件通道契约— OHOS 侧
EventChannel('screenStateEvents')与 Dart 侧EventChannel('screenStateEvents')通道名一致。 - 真机行为(待实测)— 配置签名后连真机,锁屏/解锁/熄屏时 UI 日志应输出
SCREEN_OFF/SCREEN_ON/SCREEN_UNLOCKED事件。
七、运行效果
适配完成并通过构建验证。运行截图需在真机签名安装后通过以下命令获取(真机 ALN-AL00 已连接):
flutter screenshot-d<device_ip>:<port>八、遗留问题与改进方向
已知问题
- 进程存活依赖— 屏幕事件仅在应用进程存活时可达;用户强杀应用后公共事件订阅失效(与 Android 行为一致)。
- USER_PRESENT 弃用— 新版 HarmonyOS 弃用
COMMON_EVENT_USER_PRESENT,已通过同时订阅COMMON_EVENT_SCREEN_UNLOCKED兼容,但长期建议移除旧事件源。 - 签名依赖— 示例工程需配置签名(DevEco 自动签名或
build-profile.json5)后才能安装到真机。
未来优化
- 真机截图验证— 安装签名 HAP 后补充
flutter screenshot运行截图与实测日志。 - README 双语文档— 已生成
README.OpenHarmony.md/README.OpenHarmony_CN.md,后续随版本更新同步维护。
九、总结
将一个 Flutter 三方库适配到 OHOS 平台,核心路径可以概括为三步走:
1. 找对应 ── 找到 OHOS 对每个 Android 原生 API 的等价实现(BroadcastReceiver → commonEventManager) 2. 保契约 ── 确保方法通道名、方法名、返回值结构完全一致(screenStateEvents 通道 + 事件名字符串) 3. 补缺口 ── 对于 OHOS 不提供的 API,用合理方案弥补(事件 id 归一化、双事件源兼容)对于screen_state三方库,适配涉及61 个文件的新增/修改(插件 ohos 实现 10 个 + 示例 ohos 工程 50 个 + Dart 层 1 个),提交347a4d27(+1174 / -25)。Dart 公共 API 和其他平台的代码完全不受影响——这正是 Flutter 跨平台三方库生态的魅力所在。
回顾整个适配过程,三个"坑"值得记录:
| 踩坑点 | 现象 | 根因与解法 |
|---|---|---|
| module.json5 权限 schema 校验失败 | reason字段必须为$string:资源引用或含{}占位符 | 权限 reason 需引用 string 资源 |
| 权限不在 SDK 预定义 | RECEIVE_SCREEN_EVENTS声明报 00303221 | 屏幕公共事件订阅无需权限,直接移除声明 |
| 鸿蒙上"不生效" | example 从未收到事件 | 根因在example 的_isSupportedPlatform未含 ohos,UI 层从未订阅;同时修复插件补订SCREEN_UNLOCKED事件 |
参考文档
- screen_state 官方仓库
- 本适配仓库(AtomGit)
- HarmonyOS Flutter 适配指南
- @ohos.commonEventManager API 参考