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 平台。
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 插件模板:
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(入口导出文件)
import PhoneStatePlugin from './src/main/ets/components/plugin/PhoneStatePlugin';
export default PhoneStatePlugin;
oh-package.json5(包配置,版本与上游 pubspec 保持一致)
{
"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 摘要):
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 平台注册:
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 原生代码与配置可编译:
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)等硬编码值。
验证要点
- 静态检查 —
flutter analyze无任何 issues - 构建验证 —
flutter build hap --debug成功产出entry-default-signed.hap - 插件注册 —
GeneratedPluginRegistrant.ets正确导入并注册PhoneStatePlugin - 来电场景 — 真机来电,Dart 流收到
CALL_INCOMING,号码为 null(三方应用限制),随后接听进入CALL_STARTED - 去电场景 — 真机拨出电话,接通后上报
CALL_STARTED(映射策略与 Android 一致) - 通话时长 — 通话期间每秒收到
CALL_STARTED且callDuration递增,挂断收到CALL_ENDED并携带最终时长 - 取消订阅 —
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 端默认一致 |
已知问题
- 来电号码不可用 — OpenHarmony 将
number限定为system_basic系统权限,三方应用恒为 null(同 iOS)。 - CALL_OUTGOING 不产生 — 三方应用无法区分去电状态,去电统一映射为
CALL_STARTED(同 Android)。
未来优化
- 多卡订阅 — 通过
ObserverOptions{ slotId }支持双卡通话状态跟踪。 - 初始状态对齐 Android — 订阅时调用
telephony.call.getCallState查询当前状态,替代首事件 NOTHING。 - 系统签名场景 — 面向系统应用时声明
READ_CALL_LOG(含$stringreason 资源)以开放号码能力,并在文档标注 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 跨平台三方库生态的魅力所在。
参考文档
更多推荐


所有评论(0)