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.kts、ios/Classes、lib/*_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 权限模型 三方应用仅能获取 state,number 属系统权限(见决策 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 开始订阅 onListen → registerReceiver 注册广播 onListen → observer.on('callStateChange', cb) 订阅
事件到达 回调中 eventSink.success(map) 回调中 eventSink.success(map)
Dart 取消订阅 onCancel → unregisterReceiver + 停表 onCancel → observer.off + 清空计时器
插件解绑引擎 onDetachedFromEngine → setStreamHandler(null) onDetachedFromEngine → setStreamHandler(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;号码 number 需 READ_CALL_LOG / GET_TELEPHONY_STATE(system_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.json5 的 compatibleSdkVersion / 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_STARTED 且 callDuration 递增,挂断收到 CALL_ENDED 并携带最终时长
  7. 取消订阅 --- cancel() 后原生侧停止上报,重新订阅可再次收到事件(onCancel / onListen 配对生效)

七、运行效果

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

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


八、遗留问题与改进方向

踩坑复盘

踩坑点 现象 / 报错 根因与解法
模板残留清理 flutter create . --template=plugin 生成了 android/*.kts、ios/Classes、lib/*_platform_interface.dart、example/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)。解法:与维护者确认后放宽 environment 为 sdk: ">=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 跨平台三方库生态的魅力所在。


参考文档

相关推荐
Geek_Vison11 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo11 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_7381858212 小时前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙
梦想的颜色17 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
yuanlaile2 天前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭2 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白2 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫3 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘3 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185823 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙