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 形态(screenStateStreamScreenStateEvent)保持不变,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 事件通道注册
平台代码
AndroidEventChannel(binding.binaryMessenger, "screenStateEvents")
OHOSnew EventChannel(binding.getBinaryMessenger(), "screenStateEvents")

差异:OHOS 使用 getBinaryMessenger(),与 Android 的 binaryMessenger 属性等价,均从 FlutterPluginBinding 获取。

2.3 事件源对照
平台事件源注册时机注销时机
AndroidBroadcastReceiver 监听 ACTION_SCREEN_ON/OFF/USER_PRESENTonListenonCancel
OHOScommonEventManager 订阅 COMMON_EVENT_SCREEN_ON/OFF/SCREEN_UNLOCKED/USER_PRESENTonListenonCancel
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_UNLOCKEDandroid.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.dartscreenStateStream 的平台守卫增加 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)
语言KotlinArkTS (TypeScript 语法)
插件接口FlutterPlugin, EventChannel.StreamHandlerFlutterPlugin, StreamHandler
类注册@Override 注解 + Flutter 自动发现getUniqueClassName() 返回类名
通道获取binding.binaryMessengerbinding.getBinaryMessenger()
事件源BroadcastReceiver + IntentFiltercommonEventManager.createSubscriber + subscribe
事件发送eventSink?.success(intent.action)eventSink.success(eventName)
注销时机onCancelunregisterReceiveronCancelunsubscribe

4.2 关键 ArkTS 语法差异

Android 语法ArkTS 语法备注
import io.flutter.plugin.common.EventChannelimport { 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.actiondata.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(ScreenScreenStateEventscreenStateStream)完全保持原样,仅将平台守卫从 Platform.isAndroid || Platform.isIOS 扩展为 || Platform.isOhos。示例工程则补充 TargetPlatform.ohos 判断——这是排查"鸿蒙不生效"问题时的关键发现:example 的 _isSupportedPlatform 未含 ohos,导致 UI 层从未调用 startListening()

维护策略:所有平台相关判断集中在 _isSupportedPlatform / screenStateStream 守卫中,便于统一维护。


六、测试与验证

测试环境
项目版本
Flutter3.41.10-ohos-1.0.0
Dart3.11.5
HarmonyOS SDK26.0.0(API 26)
IDEDevEco Studio 26.0.0
设备 ROMALN-AL00 7.0.0.105(SP6C00E105R4P3)
验证要点
  1. 静态分析flutter analyze 通过,无警告无错误。
  2. 编译验证 — hvigor assembleHap 构建成功(43 tasks),产出 entry-default-unsigned.hap
  3. 插件注册GeneratedPluginRegistrant.ets 正确导入并注册 ScreenStatePluginimport ScreenStatePlugin from 'screen_state')。
  4. 事件通道契约 — OHOS 侧 EventChannel('screenStateEvents') 与 Dart 侧 EventChannel('screenStateEvents') 通道名一致。
  5. 真机行为(待实测) — 配置签名后连真机,锁屏/解锁/熄屏时 UI 日志应输出 SCREEN_OFF / SCREEN_ON / SCREEN_UNLOCKED 事件。

七、运行效果

适配完成并通过构建验证。运行截图需在真机签名安装后通过以下命令获取(真机 ALN-AL00 已连接):

flutter screenshot -d <device_ip>:<port>

image-20260905161044052


八、遗留问题与改进方向

已知问题
  1. 进程存活依赖 — 屏幕事件仅在应用进程存活时可达;用户强杀应用后公共事件订阅失效(与 Android 行为一致)。
  2. USER_PRESENT 弃用 — 新版 HarmonyOS 弃用 COMMON_EVENT_USER_PRESENT,已通过同时订阅 COMMON_EVENT_SCREEN_UNLOCKED 兼容,但长期建议移除旧事件源。
  3. 签名依赖 — 示例工程需配置签名(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 事件

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐