Flutter 三方库 phone_state 的 OpenHarmony 适配实战

Flutter 三方库 phone_state 的 OpenHarmony 适配实战

本文记录了将开源 Flutter 三方库 phone_state 适配到 OpenHarmony / HarmonyOS 平台的完整过程,

包含适配思路、代码改动对照、关键决策和踩坑复盘。


一、背景

1.1 三方库简介

phone_state 是一个 Flutter 社区广泛使用的通话状态检测插件,提供以下能力:

  • 通话状态感知 :实时上报来电中(CALL_INCOMING)、通话中(CALL_STARTED)、通话结束(CALL_ENDED)等状态
  • 通话时长统计:通话建立后每秒刷新通话时长
  • 来电号码获取:来电时获取对方号码(Android 平台可用)
  • 无额外操作副作用:仅感知状态,不接听、不拒接、不结束通话

该三方库最初支持 Android、iOS 两个平台,本次任务将其适配到 OpenHarmony / HarmonyOS 平台。

项目地址https://atomgit.com/oh-flutter/phone_state

1.2 适配目标

维度 要求
功能一致性 通话状态(来电/通话中/结束)与通话时长行为与 Android 端对齐
Dart 层零改动 PhoneState.stream 事件结构与通道名完全不变
性能 状态变化秒级响应,时长统计与原生保持同步
工程规范 遵循 CPF 适配规范:ohos/ HAR + example/ohos/ 示例、文档 SDK 版本动态读取不硬编码

二、适配路线图

整个适配分为 4 个阶段:

复制代码
第 1 阶段:项目初始化  ── 生成 ohos 平台脚手架,清理模板残留
第 2 阶段:原生实现    ── Android 通话状态监听翻译为 ArkTS(telephony.observer)
第 3 阶段:三方库注册  ── pubspec.yaml 增加 ohos pluginClass 配置
第 4 阶段:验证与交付  ── pub get / analyze / assembleHap 构建 + 真机验证 + 双语文档

三、逐步适配过程

第 1 阶段:项目初始化

使用 Flutter(ohos 分支工具链)命令行生成 OHOS 插件模板:

bash 复制代码
flutter create . --template=plugin --platforms=ohos

该命令会自动生成 ohos/ 目录的标准模板结构,包含必要的构建配置和入口文件:

复制代码
ohos/
├── index.ets                              # 模块入口,导出插件类
├── oh-package.json5                       # 包配置
├── build-profile.json5                    # 构建配置
├── src/main/
│   ├── module.json5                       # HAR 模块配置
│   └── ets/components/plugin/
│       └── PhoneStatePlugin.ets           # 原生插件实现(核心)

关键配置文件:

index.ets(入口导出文件)

typescript 复制代码
import PhoneStatePlugin from './src/main/ets/components/plugin/PhoneStatePlugin';
export default PhoneStatePlugin;

oh-package.json5(包配置,版本与上游 pubspec 保持一致)

json5 复制代码
{
  "name": "phone_state",
  "version": "4.0.1",
  "description": "Flutter plugin that reports the phone call state and call duration on HarmonyOS.",
  "main": "index.ets",
  "author": "",
  "license": "Apache-2.0",
  "dependencies": {}
}

@ohos/flutter_ohos 由 Flutter 引擎在构建时自动链接,无需在 dependencies 中显式声明。

清理模板残留 :本仓库使用 Groovy Gradle(android/build.gradle)与 Swift Package Manager(ios/phone_state/Sources),

flutter create 会额外生成与仓库结构不符的模板文件(android/build.gradle.ktsios/Classeslib/*_platform_interface.dart

example/test、根目录 test/ 等)。清理时务必先 git status 甄别,只删除新增且未跟踪 的文件------本次清理曾误删

android/src/test 下已跟踪的单元测试(PhoneStatePermissionsTest.kt 等),需立即用 git restore --source=HEAD 恢复,

最终仅保留 ohos/example/ohos/ 的增量。

第 2 阶段:原生实现(核心)

适配前先判断插件类型:phone_state 属于事件流型 (Dart 端 EventChannel.receiveBroadcastStream() 被动订阅,原生侧主动推送状态事件),因此必须实现 StreamHandler 接口的 onListen / onCancel,而非 MethodCallHandler

2.1 整体架构对比
复制代码
 Android (Kotlin)                          OHOS (ArkTS)
 ────────────────────                      ────────────────────
 PhoneStatePlugin                          PhoneStatePlugin
   implements FlutterPlugin                  implements FlutterPlugin,
   FlutterHandler:                            StreamHandler
     EventChannel + setStreamHandler         import { FlutterPlugin,
   BroadcastReceiver + IntentFilter            FlutterPluginBinding,
     (ACTION_PHONE_STATE_CHANGED)             EventChannel, EventSink,
   TelephonyManager.callState                  StreamHandler
                                             } from '@ohos/flutter_ohos'
                                             telephony.observer.on('callStateChange')

事件源映射(Android 事件源 → OHOS 等价物):

Android 事件源 OHOS 等价物 说明
BroadcastReceiver 动态注册监听 TelephonyManager.ACTION_PHONE_STATE_CHANGED telephony.observer.on('callStateChange', callback) 通话状态事件,回调返回 CallStateInfo{ state, number }
Timer(每秒刷新通话时长) setInterval(每秒回调) 时长上报节奏保持一致
ContextCompat.checkSelfPermission SDK 权限模型 三方应用仅能获取 statenumber 属系统权限(见决策 3)
2.2 通道注册
平台 代码
Android EventChannel(binding.binaryMessenger, "PHONE_STATE_STREAM").setStreamHandler(handler)
OHOS new EventChannel(binding.getBinaryMessenger(), "PHONE_STATE_STREAM").setStreamHandler(this)

差异 :两侧接口基本一一对应;OHOS 的 EventChannel 构造参数为 (messenger, name, codec?)codec 默认 StandardMethodCodec.INSTANCE,与 Dart 端默认值一致,无需显式传入。通道名 PHONE_STATE_STREAM 与 Dart 端 Constants.EVENT_CHANNEL 完全一致,这是两端通信的契约。

2.2.1 StreamHandler 生命周期
时机 Android OHOS
插件绑定引擎 onAttachedToEngine 创建 EventChannel onAttachedToEngine 创建 EventChannel
Dart 开始订阅 onListenregisterReceiver 注册广播 onListenobserver.on('callStateChange', cb) 订阅
事件到达 回调中 eventSink.success(map) 回调中 eventSink.success(map)
Dart 取消订阅 onCancelunregisterReceiver + 停表 onCancelobserver.off + 清空计时器
插件解绑引擎 onDetachedFromEnginesetStreamHandler(null) onDetachedFromEnginesetStreamHandler(null) + 兜底注销
2.3 通话状态机映射

Android 通过 TelephonyManager / 广播 EXTRA_STATE 判断 RINGING / OFFHOOK / IDLE,OHOS 通过 CallStateInfo.state 判断。两者状态值语义一致,直接映射:

Android 状态 OHOS CallState 上报的 PhoneStateStatus
CALL_STATE_RINGING CALL_STATE_RINGING(1) CALL_INCOMING
CALL_STATE_OFFHOOK CALL_STATE_OFFHOOK(2) / CALL_STATE_ANSWERED(3) CALL_STARTED(通话中,含去电接通)
CALL_STATE_IDLE CALL_STATE_IDLE(0) CALL_ENDED(携带最终时长)
订阅开始 --- NOTHING(先上报一次空状态,同 iOS 无活动通话行为)

OHOS 端核心事件处理(ArkTS 摘要):

typescript 复制代码
private handleCallStateChange(info: observer.CallStateInfo): void {
  if (this.eventSink == null) {
    return;
  }
  this.phoneNumber = info.number.length > 0 ? info.number : null;
  const state: number = info.state;
  if (state === CALL_STATE_RINGING) {          // 来电响铃:重置计时并上报来电
    this.resetCallDuration();
    this.sendState(STATUS_CALL_INCOMING);
  } else if (state === CALL_STATE_OFFHOOK || state === CALL_STATE_ANSWERED) {
    this.startCallDuration();                  // 通话建立:开始秒级计时
    this.sendState(STATUS_CALL_STARTED);
  } else if (state === CALL_STATE_IDLE) {      // 通话结束:结算时长并上报
    this.updateCallDuration();
    this.sendState(STATUS_CALL_ENDED);
    this.resetCallDuration();
  }
}

通话时长通过 setInterval(1000) 每秒刷新并推送 CALL_STARTED,与 Android 端 Timer 行为一致。

第 3 阶段:三方库注册

pubspec.yaml 中添加 OHOS 平台注册:

yaml 复制代码
flutter:
  plugin:
    platforms:
      android:
        package: it.mainella.phone_state
        pluginClass: PhoneStatePlugin
      ios:
        pluginClass: PhoneStatePlugin
      ohos:                              # ← 新增
        pluginClass: PhoneStatePlugin    # ← 必须与 index.ets 默认导出的类名一致

三点约束:① pluginClass 必须与 OHOS 实现类名完全一致 (区分大小写);② 实现类必须提供 getUniqueClassName() 并返回该类名;③ 引擎构建时读取 ohos/index.ets 的默认导出完成注册,示例工程的 GeneratedPluginRegistrant.ets 会自动生成,无需手写。

第 4 阶段:示例应用与依赖

flutter create . --template=plugin --platforms=ohos 会在插件根目录生成 ohos/(插件 HAR)的同时,自动生成 example/ohos/(示例宿主工程,含签名配置、SDK 版本、测试模块与 Flutter 运行时资源):

复制代码
example/ohos/
├── AppScope/app.json5                     # 应用配置
├── build-profile.json5                   # 项目构建配置(compatibleSdkVersion / targetSdkVersion / signingConfigs)
├── hvigorfile.ts                         # 构建入口
├── oh-package.json5                      # 顶层包配置
└── entry/
    ├── src/main/module.json5             # entry 模块配置(含 requestPermissions)
    └── src/main/ets/
        ├── entryability/EntryAbility.ets # Ability 生命周期
        └── pages/Index.ets               # UI 页面(Flutter 容器)

由于上游 pubspec 声明了 sdk: ^3.12.2 / flutter: '>=3.44.0',而本机 OHOS 工具链为 3.41.10-ohos(Dart 3.11.5),flutter pub get 会直接失败。经确认后放宽根包与 example 的约束至 sdk: ">=3.11.5 <4.0.0"flutter: ">=3.41.10-ohos"(详见决策 4)。

第 5 阶段:构建验证(HAP)

使用 Flutter OHOS 工具链直接构建示例 HAP,验证 ArkTS 原生代码与配置可编译:

bash 复制代码
cd example
flutter pub get
flutter build hap --debug

成功产出:example/build/ohos/hap/entry-default-signed.hap。同时 flutter analyze 无任何告警。


四、完整代码对照

4.1 Android vs OHOS 完整实现对照

维度 Android (Kotlin) OHOS (ArkTS)
事件通道 EventChannel(binding.binaryMessenger, "PHONE_STATE_STREAM") new EventChannel(binding.getBinaryMessenger(), "PHONE_STATE_STREAM")
事件源 BroadcastReceiver + IntentFilter(ACTION_PHONE_STATE_CHANGED) observer.on('callStateChange', cb) / observer.off('callStateChange')
状态来源 intent.getStringExtra(EXTRA_STATE) / TelephonyManager.callState CallStateInfo.state
号码来源 intent.getStringExtra(EXTRA_INCOMING_NUMBER) CallStateInfo.number(三方应用恒空)
时长刷新 Timer.schedule(task, 0, 1000) setInterval(cb, 1000) / clearInterval
权限判断 checkSelfPermission(READ_PHONE_STATE) 无(订阅 state 无需权限)
事件体 mapOf("status" to ..., "phoneNumber" to ..., "callDuration" to ...) { 'status': ..., 'phoneNumber': ..., 'callDuration': ... }

4.2 关键 ArkTS 语法差异

Android 语法 ArkTS 语法 备注
import io.flutter.embedding.engine.plugins.* import { FlutterPlugin, FlutterPluginBinding, EventChannel, EventSink, StreamHandler } from '@ohos/flutter_ohos' OHOS 使用模块化导入
class X : FlutterPlugin export default class X implements FlutterPlugin, StreamHandler 默认导出供 index.ets 引用
getStringExtra(...) 回调结构体字段直取 info.state / info.number 类型使用 SDK 正式类型 observer.CallStateInfo
enum class PhoneStateStatus(JVM 枚举) 顶层 const string + 事件体字符串 Dart 端按字符串 name 反查枚举
回调参数无需标注 回调参数需与 SDK 类型一致 自定义同形接口会触发 arkts-no-structural-typing(见踩坑 3)

五、关键决策说明

决策 1:保持事件通道名与事件结构不变

Dart 端 Constants.EVENT_CHANNEL = 'PHONE_STATE_STREAM' 与事件结构 {status, phoneNumber, callDuration} 是既定的通信契约,OHOS 原生侧严格复用:通道名一字不差,事件字段名、类型(String / String? / int 秒数)与 Android 完全一致,Dart 层实现零改动。

维护策略:任何一侧改动字段/通道名都必须同步另一侧,并在示例中回归验证。

决策 2:通话状态机与 Android 语义对齐

OHOS CallState 与 Android TelephonyManager 状态值语义相同(RINGING/OFFHOOK/IDLE/ANSWERED),但三方应用无法区分去电状态,因此沿用 Android 策略:去电接通统一上报 CALL_STARTED,不产生 CALL_OUTGOING(该枚举仍保留,仅为 iOS 特有)。

维护策略 :状态映射集中在 handleCallStateChange 一处,新增状态值只需扩展该分支。

决策 3:不声明系统级权限,来电号码按 null 处理

OpenHarmony 文档明确:三方应用订阅 callStateChange 仅能获取 state;号码 numberREAD_CALL_LOG / GET_TELEPHONY_STATEsystem_basic,仅系统应用)。若在 HAR 中声明系统权限,普通应用安装 HAP 会报 9568289。因此:

方案 优点 缺点
不声明任何权限,number 置 null 三方应用可正常安装使用,状态能力完整 号码不可用(与 iOS 行为一致)
声明 READ_CALL_LOG / GET_TELEPHONY_STATE 系统应用可读号码 normal 级应用安装失败(9568289),需 system_basic 签名

维护策略 :若面向系统签名场景,可在文档中说明手动补充权限与 reason 资源后由系统应用获取号码。

决策 4:放宽环境约束以适配本机 OHOS 工具链

上游要求 Dart ^3.12.2 / Flutter >=3.44.0,而当前 OHOS 工具链为 3.41.10-ohos(Dart 3.11.5),导致 pub get 失败。经确认采用放宽方案:根包与 example 的 environment 调整为 sdk: ">=3.11.5 <4.0.0"flutter: ">=3.41.10-ohos",使 OHOS 工具链可完整解析、构建与验证。

维护策略 :随 OHOS Flutter 版本演进可逐步收窄下限;文档兼容性信息遵循"SDK 版本动态读取、勿硬编码"规范,均以 build-profile.json5 实际值为准。

决策 5:首事件上报 NOTHING

订阅建立后先推送一次 NOTHING,使 Dart 流立即有数据(与 iOS 无活动通话时的行为一致),避免 UI 长期停留在"不可用"状态;Android 在无权限时同样不产生事件,两者不冲突。

维护策略 :如需与 Android 权限模型完全一致(初始查询当前 callState),可后续用 telephony.call.getCallState 补齐(见"未来优化")。


六、测试与验证

测试环境

项目 版本
Flutter 3.41.10-ohos-1.0.0
Dart 3.11.5
HarmonyOS SDK compatibleSdkVersion: 5.1.0(18),targetSdkVersion: 26.0.0(读取自 example/ohos/build-profile.json5
IDE DevEco Studio 26.0.0
设备 HarmonyOS 真机(验证通过)

版本获取方式:

版本项 获取方式
Flutter / Dart flutter --version
HarmonyOS SDK 读取 example/ohos/build-profile.json5compatibleSdkVersion / targetSdkVersion(或 ~/Library/OpenHarmony/Sdk/<version>/ 目录名)
IDE /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" /Applications/DevEco-Studio.app/Contents/Info.plist
设备 ROM hdc shell param get const.product.software.version

规范提示 :文档中 SDK 版本一律按上述方式动态读取,勿照抄其他文章的 5.0.0(12) 等硬编码值。

验证要点

  1. 静态检查 --- flutter analyze 无任何 issues
  2. 构建验证 --- flutter build hap --debug 成功产出 entry-default-signed.hap
  3. 插件注册 --- GeneratedPluginRegistrant.ets 正确导入并注册 PhoneStatePlugin
  4. 来电场景 --- 真机来电,Dart 流收到 CALL_INCOMING,号码为 null(三方应用限制),随后接听进入 CALL_STARTED
  5. 去电场景 --- 真机拨出电话,接通后上报 CALL_STARTED(映射策略与 Android 一致)
  6. 通话时长 --- 通话期间每秒收到 CALL_STARTEDcallDuration 递增,挂断收到 CALL_ENDED 并携带最终时长
  7. 取消订阅 --- cancel() 后原生侧停止上报,重新订阅可再次收到事件(onCancel / onListen 配对生效)

七、运行效果

真机运行 example/ 示例工程,页面实时展示通话状态文本(Status of call: CALL_INCOMING / CALL_STARTED / CALL_ENDED)与时长,来电、去电、挂断全程状态流转正确。

如需补充截图,可用以下命令抓取:


八、遗留问题与改进方向

踩坑复盘

踩坑点 现象 / 报错 根因与解法
模板残留清理 flutter create . --template=plugin 生成了 android/*.ktsios/Classeslib/*_platform_interface.dartexample/test 等与仓库结构不符的文件;清理时误删已跟踪测试 模板按"全新插件"而非"既有仓库"生成。解法:只删除 git status新增未跟踪 的模板文件;误删已跟踪文件立即 git restore --source=HEAD 恢复
pub get 失败 Because phone_state requires SDK version ^3.12.2, version solving failed 上游约束高于本机 OHOS 工具链(Dart 3.11.5)。解法:与维护者确认后放宽 environmentsdk: ">=3.11.5 <4.0.0" / flutter: ">=3.41.10-ohos"
ArkTS 结构类型 hvigor ERROR: arkts-no-structural-typing at PhoneStatePlugin.ets 为回调自定义了同形的 interface CallStateInfo,ArkTS 禁止跨类型结构匹配(nominal typing)。解法:改用 SDK 正式类型 observer.CallStateInfo 标注回调与处理方法参数
权限声明分歧 文档对 callStateChange 所需权限描述不一(GET_TELEPHONY_STATE / READ_CALL_LOG,均 system_basic) 三方应用仅能获取 state。解法:HAR 不声明 系统权限(避免安装报 9568289),number 置 null,真机验证状态上报正常
EventChannel API 未知 不确定 @ohos/flutter_ohos 是否导出 EventChannel 解包引擎产物 flutter.har(gzip + tar)确认 index.ets 导出 EventChannel, StreamHandler, EventSink,且 codec 默认 StandardMethodCodec.INSTANCE,与 Dart 端默认一致

已知问题

  1. 来电号码不可用 --- OpenHarmony 将 number 限定为 system_basic 系统权限,三方应用恒为 null(同 iOS)。
  2. CALL_OUTGOING 不产生 --- 三方应用无法区分去电状态,去电统一映射为 CALL_STARTED(同 Android)。

未来优化

  • 多卡订阅 --- 通过 ObserverOptions{ slotId } 支持双卡通话状态跟踪。
  • 初始状态对齐 Android --- 订阅时调用 telephony.call.getCallState 查询当前状态,替代首事件 NOTHING。
  • 系统签名场景 --- 面向系统应用时声明 READ_CALL_LOG(含 $string reason 资源)以开放号码能力,并在文档标注 APL 要求。

九、总结

将一个 Flutter 三方库适配到 OHOS 平台,核心路径可以概括为 三步走

复制代码
1. 找对应 ── 找到 OHOS 对每个 Android 原生 API 的等价实现(广播 → telephony.observer)
2. 保契约 ── 确保事件通道名、事件结构、时长上报语义完全一致
3. 补缺口 ── 对 OHOS 不提供的 API 用合理方案弥补(号码降级为 null、权限不声明)

对于 phone_state 三方库,适配共新增/修改 45 个文件(约 1089 行),其中 ohos/ 原生实现集中在单个 PhoneStatePlugin.ets(约 176 行)。Dart 层与 Android/iOS 代码完全不受影响------这正是 Flutter 跨平台三方库生态的魅力所在。


参考文档

相关推荐
ljt27249606614 小时前
Flutter笔记--本地通知
笔记·flutter
lqj_本人5 小时前
Flutter 三方库 Pedometer 的鸿蒙化适配指南
flutter·华为·harmonyos
Android-Flutter21 小时前
flutter GetX 详解
android·flutter
坚果的博客1 天前
Flutter OHOS 环境搭建实战:oh-3.44.9-dev 从 0 到 1 完整记录
flutter·华为·harmonyos
Crazy_MT1 天前
Android Studio 运行 Flutter iOS 真机白屏,但 Xcode 和命令行正常的排查记录
flutter·android studio
Android-Flutter1 天前
flutter async/await 详解
android·flutter
SoaringHeart1 天前
Flutter 进阶 | 组件封装:用 CustomPainter 实现光环动画组件AnimatedHalo
前端·flutter
Light Gao1 天前
企业级移动端 APP 架构设计:从原生双端到 Hybrid、React Native 与 Flutter
flutter·react native·react.js
末代iOS程序员华仔2 天前
iOS 5.6 条例解决方法
flutter·ios·swift