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 插件模板:
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(入口导出文件)
importPhoneStatePluginfrom'./src/main/ets/components/plugin/PhoneStatePlugin';exportdefaultPhoneStatePlugin;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 状态 | OHOSCallState | 上报的 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 摘要):
privatehandleCallStateChange(info:observer.CallStateInfo):void{if(this.eventSink==null){return;}this.phoneNumber=info.number.length>0?info.number:null;conststate:number=info.state;if(state===CALL_STATE_RINGING){// 来电响铃:重置计时并上报来电this.resetCallDuration();this.sendState(STATUS_CALL_INCOMING);}elseif(state===CALL_STATE_OFFHOOK||state===CALL_STATE_ANSWERED){this.startCallDuration();// 通话建立:开始秒级计时this.sendState(STATUS_CALL_STARTED);}elseif(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_statepluginClass:PhoneStatePluginios:pluginClass:PhoneStatePluginohos:# ← 新增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 原生代码与配置可编译:
cdexample 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 语义对齐
OHOSCallState与 AndroidTelephonyManager状态值语义相同(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 跨平台三方库生态的魅力所在。
参考文档
- phone_state 官方仓库(GitHub)
- phone_state 鸿蒙适配仓库(AtomGit)
- HarmonyOS Flutter 适配指南
- @ohos.telephony.observer 参考文档
- Flutter OHOS 插件适配技能(flutter-ohos-plugin-adapter)