Flutter 三方库 phone_state 的 OpenHarmony 适配实战
2026/9/6 2:27:40 网站建设 项目流程

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.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_CHANGEDtelephony.observer.on('callStateChange', callback)通话状态事件,回调返回CallStateInfo{ state, number }
Timer(每秒刷新通话时长)setInterval(每秒回调)时长上报节奏保持一致
ContextCompat.checkSelfPermissionSDK 权限模型三方应用仅能获取statenumber属系统权限(见决策 3)
2.2 通道注册
平台代码
AndroidEventChannel(binding.binaryMessenger, "PHONE_STATE_STREAM").setStreamHandler(handler)
OHOSnew 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 生命周期
时机AndroidOHOS
插件绑定引擎onAttachedToEngine创建 EventChannelonAttachedToEngine创建 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 状态OHOSCallState上报的 PhoneStateStatus
CALL_STATE_RINGINGCALL_STATE_RINGING(1)CALL_INCOMING
CALL_STATE_OFFHOOKCALL_STATE_OFFHOOK(2)/CALL_STATE_ANSWERED(3)CALL_STARTED(通话中,含去电接通)
CALL_STATE_IDLECALL_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.callStateCallStateInfo.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 : FlutterPluginexport 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;号码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补齐(见"未来优化")。


六、测试与验证

测试环境

项目版本
Flutter3.41.10-ohos-1.0.0
Dart3.11.5
HarmonyOS SDKcompatibleSdkVersion: 5.1.0(18),targetSdkVersion: 26.0.0(读取自example/ohos/build-profile.json5
IDEDevEco Studio 26.0.0
设备HarmonyOS 真机(验证通过)

版本获取方式:

版本项获取方式
Flutter / Dartflutter --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
设备 ROMhdc 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(含$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)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询