Flutter 三方库 screen_state 的 OpenHarmony 适配实战
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 平台。
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):
enum ScreenStateEvent {
screenUnlocked, screenOn, screenOff;
// Android 返回 intent action;iOS/OHOS 返回短名称
String get name { ... }
// 两种名称形式都可解析
static ScreenStateEvent fromName(String name) {
switch (name) {
case 'SCREEN_UNLOCKED':
case 'android.intent.action.USER_PRESENT':
return ScreenStateEvent.screenUnlocked;
// ...
}
}
}
Stream<ScreenStateEvent> get screenStateStream =>
_screenStateStream ??= Platform.isAndroid || Platform.isIOS // ← 需加 Platform.isOhos
? _screenStateStream ??= _eventChannel
.receiveBroadcastStream()
.map((event) => ScreenStateEvent.fromName(event))
: Stream<ScreenStateEvent>.empty();
Android 侧实现(Kotlin):
// ScreenStatePlugin.kt
public class ScreenStatePlugin : FlutterPlugin, EventChannel.StreamHandler {
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
eventChannel = EventChannel(binding.binaryMessenger, "screenStateEvents")
context = binding.applicationContext
eventChannel.setStreamHandler(this)
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
screenReceiver = ScreenReceiver(events)
val filter = IntentFilter()
filter.addAction(Intent.ACTION_SCREEN_ON) // 亮屏
filter.addAction(Intent.ACTION_SCREEN_OFF) // 熄屏
filter.addAction(Intent.ACTION_USER_PRESENT) // 解锁
context!!.registerReceiver(screenReceiver, filter)
}
override fun onCancel(arguments: Any?) {
context!!.unregisterReceiver(screenReceiver)
}
}
// ScreenReceiver.kt —— 收到广播后直接把 intent.action 字符串推给 Dart
class ScreenReceiver(private val eventSink: EventSink?) : BroadcastReceiver() {
override fun onReceive(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';
import commonEventManager from '@ohos.commonEventManager';
import { BusinessError } from '@kit.BasicServicesKit';
const CHANNEL_NAME: string = 'screenStateEvents';
// 将系统公共事件 id 映射为 Dart 层可识别的事件名
function toScreenStateEvent(eventId: string): string {
switch (eventId) {
case commonEventManager.Support.COMMON_EVENT_SCREEN_ON:
return 'SCREEN_ON';
case commonEventManager.Support.COMMON_EVENT_SCREEN_OFF:
return 'SCREEN_OFF';
case commonEventManager.Support.COMMON_EVENT_SCREEN_UNLOCKED:
case commonEventManager.Support.COMMON_EVENT_USER_PRESENT: // 兼容旧版本
return 'SCREEN_UNLOCKED';
default:
return eventId;
}
}
export default class ScreenStatePlugin implements FlutterPlugin, StreamHandler {
private eventChannel: EventChannel | null = null;
private eventSink: EventSink | null = null;
private subscriber: commonEventManager.CommonEventSubscriber | null = null;
getUniqueClassName(): string {
return 'ScreenStatePlugin'; // 必须与 pubspec.yaml 的 pluginClass 一致
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.eventChannel = new EventChannel(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();
}
private async subscribeScreenEvents(): Promise<void> {
if (this.subscriber != null) return;
let subscribeInfo: 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 = await commonEventManager.createSubscriber(subscribeInfo);
commonEventManager.subscribe(this.subscriber, (err: BusinessError, data: commonEventManager.CommonEventData) => {
if (err || this.eventSink == null) return;
let eventName: string = toScreenStateEvent(data.event);
this.eventSink.success(eventName);
});
} catch (error) {
Log.e(TAG, 'createSubscriber error: ' + JSON.stringify(error));
}
}
private unsubscribeScreenEvents(): 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_state
pluginClass: ScreenStatePlugin
ios:
pluginClass: ScreenStatePlugin
ohos: # ← 新增
pluginClass: ScreenStatePlugin # ← 与 getUniqueClassName() 一致
3.2 Dart 层补平台判断
lib/screen_state.dart 中 screenStateStream 的平台守卫增加 Platform.isOhos:
Stream<ScreenStateEvent> get screenStateStream =>
_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';
import ScreenStatePlugin from 'screen_state'; // ← 从插件包导入
export class GeneratedPluginRegistrant {
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new ScreenStatePlugin());
} catch (e) { ... }
}
}
4.2 构建验证
使用 DevEco Studio 的 hvigor 命令行构建 HAP:
node hvigorw.js --mode module -p module=entry@default -p product=default \
-p requiredDeviceType=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 事件 |
参考文档
更多推荐


所有评论(0)