资讯详情

资讯详情

Flutter 三方库在 OpenHarmony 上的适配:以拨号库为例的实战指南

这两年在做 OpenHarmony 应用适配的时候我自己踩得最多的坑就是 Flutter 三方库。你兴冲冲地在 pubspec 里加了一堆依赖编译一跑整屏报错一看全是原生平台通道没实现。尤其是那些涉及系统能力、硬件能力、通话能力的库基本不能指望原封不动跑起来。今天要聊的flutter_phone_direct_caller就是其中一个典型它解决的事情很小——在 App 里一键拨打电话但放在 OpenHarmony 这个新生态里涉及的适配细节、权限坑、平台通道实现方式一点都不少。这篇内容会从三方库选型讲起再逐步拆解 Flutter for OpenHarmony 环境下的依赖接入、权限申请、代码调用和问题排查。无论你是在做 OpenHarmony 移植还是单纯想在鸿蒙设备上用 Flutter 做通话类功能这篇都可以当一份能直接照着抄的实战笔记。1. 项目背景与方案选型为什么非要选这个库1.1 从“能跑”到“能用”Flutter 在三方设备上的鸿沟Flutter 本身是个跨平台 UI 框架UI 层面做到一次编写、多端渲染但真正和系统能力打交道的时候靠的还是平台通道加原生插件。社区里大量第三方库默认只实现了 Android 和 iOS 的端侧代码到了 OpenHarmony 上端侧压根没有对应实现你编译的时候不报错真机调用的时候就会直接给你甩一个MissingPluginException。我自己最早做 OpenHarmony 适配时试过用url_launcher来拨号结果在鸿蒙设备上调launch(tel:123456)原生侧根本没有处理telscheme 的逻辑调用直接失效。后来才意识到OpenHarmony 虽然有兼容 Android 的 API 设计但它的Ability机制、权限模型、路由跳转方式都和 Android 有着本质区别。直接从 Android 的思路平移过来踩坑是必然的。所以当项目里需要“一键拨号”这类高频能力时最好的做法不是自己去封装平台通道而是找一个已经在 OpenHarmony 侧做好适配的库。flutter_phone_direct_caller就是这样一个方案它没有把 UI 层做重只暴露一个静态方法原生侧拿到号码后拉起系统拨号盘或者直接发起呼叫。这种“小而专”的工具库反而最容易被移植到新平台上。1.2 为什么在众多拨号方案里选中了它社区里能实现拨号的三方库不少主流的有url_launcher、flutter_phone_direct_caller、tel还有通过 platform channel 自己写的方案。如果你把四类方案放在 OpenHarmony 适配场景下对比差距就非常清晰了。方案实现复杂度OpenHarmony 适配现状适合场景url_launcher打开tel:低需自行处理 scheme跳转浏览器、邮件等通用场景拨号只是附带功能flutter_phone_direct_caller低插件本身设计简单可自行实现鸿蒙端通道项目里需要高频调用、返回调用状态自己写 MethodChannel中完全可控但工作量大有精力维护端侧代码想要彻底掌控逻辑我最终选flutter_phone_direct_caller有几个硬理由。第一它的 API 极简一个静态方法直接调用没有任何生命周期和上下文依赖封装成本低。第二它内部把“调用系统拨号”和“返回结果”两个核心动作做了隔离在适配鸿蒙端时你只需要在原生侧实现对应逻辑不需要管 Dart 侧任何东西。第三它支持可选的requestCode参数能帮你拿到拨号后的返回状态这在业务上比较实用。1.3 这个库在 OpenHarmony 上到底是怎么工作的在 Android 上flutter_phone_direct_caller的实现逻辑是通过Intent.ACTION_CALL或Intent.ACTION_DIAL拉起系统拨号页。在 OpenHarmony 里类比的思路是通过Ability的AbilityContext.startAbility()方法传入一个Want指定系统通话应用的bundleName和abilityName然后系统会弹出通话界面。听起来很简单实际上有几个细节要注意。OpenHarmony 的Want参数设计里明确要求了action、entities、uri等属性。拨号场景下你需要把号码拼到uri里比如uri: tel:10086同时指定action为系统拨号页的 action 标识。如果少了这一步系统很可能不知道你要干嘛。另外url_launcher之所以拨不了号是因为它的 OpenHarmony 端实现里压根就没有解析telscheme 的逻辑。而如果我们自己给flutter_phone_direct_caller补上鸿蒙端实现核心工作其实就是把这个Want组装逻辑写好再配合权限申请就能稳定地拉起拨号页。2. 环境准备与依赖接入先把地基打牢2.1 搭建 Flutter for OpenHarmony 开发环境在真正写代码之前环境是最容易卡住的点。普通 Flutter 环境编的是 Android/iOS到了 OpenHarmony 上你得用适配过的 Flutter SDK 和对应的镜像仓库。我的建议是直接下载官方发布的 OpenHarmony 版本的 Flutter SDK不要在自己原有的 Flutter 环境里折腾否则版本冲突会让你怀疑人生。具体操作用一句话说就是把 OpenHarmony 适配版的 Flutter SDK 拉下来配置到环境变量FLUTTER_STORAGE_BASE_URL和PUB_HOSTED_URL指向的镜像然后执行flutter doctor检查环境。git clone -b OpenHarmony-3.2-Release https://gitee.com/openharmony-sig/flutter_flutter.git export PATH$PWD/flutter_flutter/bin:$PATH export FLUTTER_STORAGE_BASE_URLhttps://mirrors.huawei.com/flutter export PUB_HOSTED_URLhttps://mirrors.huawei.com/flutter/pub flutter doctor顺嘴提醒一句网络配置这块一定要保持国内可访问的镜像源不然下载 Flutter 引擎产物能卡到怀疑人生。配好之后用flutter create --platforms ohos创建一个空工程能跑起来再往下走。2.2 在 pubspec.yaml 中接入库环境就绪以后接库就简单多了。在pubspec.yaml的dependencies节点下添加依赖。我这里用的是当时已经验证过的版本你如果是在新项目里实操建议去看下最新的发布版本号。dependencies: flutter: sdk: flutter flutter_phone_direct_caller: ^2.1.1执行flutter pub get等依赖拉完。拉完之后你可以去~/.pub-cache或者项目根目录的build目录下看一眼找到flutter_phone_direct_caller包里的源码结构。正常情况下你会看到lib/目录下只有一个 Dart 文件里面是callNumber等静态方法同时还有android/和ios/目录分别存放原生平台代码。这里我要特意说一句在 OpenHarmony 上你拉到包后不要指望原生侧代码已经自动适配。截至我写这篇内容的时间这个库还没有提供默认的鸿蒙端通信代码所以你需要自己在工程里补一个ohos平台的 MethodChannel 实现。不过这恰好也是这篇文章最核心的部分。2.3 配置 OpenHarmony 权限声明OpenHarmony 的权限模型严格程度远超 Android。如果你只是拉起系统拨号盘、让用户自己手动点拨打那么只申请ohos.permission.PLACE_CALL就够了。要是你想直接替用户拨出电话那得申请ohos.permission.CALL_PHONE这个权限属于受限权限普通应用默认申请不了需要走权限弹窗让用户授权。权限配置文件在entry/src/main/module.json5里。以拉起拨号盘为例你需要加上这样的配置{ module: { requestPermissions: [ { name: ohos.permission.PLACE_CALL, reason: 用于拉起系统拨号盘方便用户直接拨打电话, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }这里有个细节reason字段在部分系统版本上会直接展示给用户所以别随便写尽量做到“说明用途、用途明确”。还有usedScene的abilities要和实际调用权限的 Ability 保持一致不然在部分设备上会有权限校验不通过的情况。3. 核心代码实现与原理拆解3.1 初始化与调用入口Dart 侧一行代码flutter_phone_direct_caller的 Dart 侧代码非常轻。你不需要初始化什么单例不需要手动MethodChannel只需要在点击事件里调用静态方法import package:flutter_phone_direct_caller/flutter_phone_direct_caller.dart; Futurevoid _callPhone(String phoneNumber) async { bool? res await FlutterPhoneDirectCaller.callNumber(phoneNumber); if (res true) { // 调用成功已拉起拨号 } else { // 调用失败可能是权限不足或系统无拨号应用 } }要是你想拿回调返回值还有一个带参数的版本await FlutterPhoneDirectCaller.callNumber(phoneNumber, requestCode: 1);requestCode的作用是当系统拨号页关闭后Flutter 侧能收到一个返回码告诉你用户是在拨号页里取消还是已经完成操作。这个参数对业务统计挺重要比如你想知道点击“通话”按钮后有多少人真正拨了出去就能以这个返回码作为分析依据。可能有人会问为什么不直接Uri.parse(tel:xxx)然后launch原因很简单直接用launch在 OpenHarmony 上十次有八次会被系统拦截因为系统不知道你想用哪个Ability去处理。而flutter_phone_direct_caller的扩展点恰好在这里——你可以用原生侧代码明确指定Want的action和entities让系统直接把页面切到拨号界面。3.2 在 OpenHarmony 原生侧实现平台通道既然官方库默认没有鸿蒙端实现我们就得自己动手补上。下面这段逻辑我建议直接放到entry/src/main/ets/EntryAbility.ets里在onCreate阶段注册一个MethodChannel名字和 Dart 侧保持一致。import { Ability } from ohos/app.ability.Ability; import { MethodChannel } from ohos.napi; export default class EntryAbility extends Ability { onCreate(want, launchParam): void { let methodChannel new MethodChannel(this.context, flutter_phone_direct_caller); methodChannel.setMethodCallHandler((call, result) { if (call.method callNumber) { let args call.arguments as Recordstring, Object; let number args[number] as string; let requestCode args[requestCode] as number || 0; // 拉起拨号页 let want { action: ohos.want.action.dial, uri: tel: number }; this.context.startAbility(want).then(() { result.success(true); }).catch(() { result.success(false); }); } else { result.notSupported(); } }); super.onCreate(want, launchParam); } }这里有几个容易踩的点。第一MethodChannel的名称必须和 Dart 侧严格一致大小写、下划线都不能错。如果不一样Dart 侧调用会直接报MissingPluginException而且这个错误在真机上特别难查。第二call.arguments的解析。flutter_phone_direct_caller传递参数时是把number和requestCode塞进去了所以一定要用Record类型去接。我见过有人直接拿call.arguments当字符串传给uri最后号码直接变成了 undefined。第三startAbility是个异步方法务必在.then()里返回result.success不要在startAbility调用之后立即返回。平台通道的result是一次性的你提前返回了后面再调就无效了。3.3 权限申请时序为什么不能直接弹窗OpenHarmony 的权限弹窗逻辑和 Android 不太一样。Android 上你调用requestPermissions系统会直接弹窗。OpenHarmony 上PLACE_CALL这类权限有时候需要先设置好请求参数再由Ability触发请求弹窗和实际调用之间是有时序关系的。以拉起拨号页为例官方推荐的做法是在EntryAbility的onWindowStageCreate阶段先调用requestPermissionsFromUser等用户同意后再调用startAbility。如果顺序反了或者压根没申请权限就直接拉起页面系统会静默失败日志里只留一行权限拒绝记录界面毫无反应。onWindowStageCreate(windowStage): void { let permissionList [ohos.permission.PLACE_CALL]; this.context.requestPermissionsFromUser(permissionList).then((data) { // 授权结果这里可以先缓存到全局后续调用拨号时判断 }); }这个设计初看确实有点绕但好处是权限申请是持久的用户授权一次之后后续再拨号就不需要重复弹窗了。真机调试的时候务必注意要先在系统设置里检查一下当前应用是否已经授予了通话权限免得排查半天以为是代码问题结果只是测试机上没点“允许”。4. 常见问题与排查技巧实录4.1 权限弹窗不出现界面毫无反应这是我遇到的第一个问题。我在模拟器上跑代码点击拨号按钮后期待系统弹权限框结果什么都没有。一开始怀疑是module.json5配置写错检查了权限名、usedScene都没问题。最后发现部分 OpenHarmony 版本中PLACE_CALL属于“静默权限”不需要运行时弹窗系统直接授权而CALL_PHONE才需要弹窗。所以如果你用的权限是PLACE_CALL不弹窗是正常现象直接拿模拟器里测试就会发现拨号页其实已经拉起来了只是界面切换太快你没注意到。4.2 真机调用报 MissingPluginException这个问题在 OpenHarmony 原生侧代码补全之前几乎必现。报错信息很明确No implementation found for method callNumber on channel flutter_phone_direct_caller。排查思路分三步第一步确认原生侧到底有没有注册这个 channel。如果你没有改原生代码那肯定是没有的这就是报错原因。第二步确认注册的 channel 名称和 Dart 侧是否一致。第三步确认你使用的是标准 Flutter 引擎而不是某些精简版引擎。OpenHarmony 上的第三方发行版很多有的内部引擎裁剪掉了 MethodChannel 的默认注册逻辑这种情况下你需要自己手动初始化。4.3 拨号后返回结果永远是 false还有一个高发问题拨号页已经成功拉起用户也能正常通话但callNumber返回的res一直是false。问题出在原生侧startAbility的返回值处理上。我最初写的代码里.then(() { result.success(true); })外面还包了一层try-catch所有异常都统一返回result.success(false)。但 OpenHarmony 的startAbility在拉起拨号页时偶尔会在.then回调里收到一个非标准异常这就导致 Flutter 侧拿到false。我的解决办法是不再依赖startAbility的回执来判断是否成功改为在调用发起前先用canIUse(SystemCapability.AppAffinity)或者直接判断this.context是否正常只要没抛异常就返回true。IDE 里强制看返回值反而容易把自己坑了。4.4 在 OpenHarmony 上和其他三方库的冲突实际开发中一个 App 里肯定不止接一个拨号库。比如有的项目同时用了permission_handler而permission_handler默认的鸿蒙实现又会去声明一片自己的权限列表。如果你在module.json5里同时声明了两个库的权限且声明方式不一致系统可能会有权限申请冲突表现为某些权限拿不到。这个问题的排查思路也比较纯粹把module.json5里的权限列表全部打出来逐个核对来源。如果是permission_handler自动生成的依赖权限直接删掉统一由你自己的业务侧去申请如果是拨号库和权限库都申请了同一个权限那没问题重复声明不报错。真实项目踩下来90% 的权限冲突都出在“重复但表述不同的权限名”上比如冷门的ohos.permission.DIAL和ohos.permission.PLACE_CALL。4.5 拨号页弹出后号码显示不对这个坑属于比较隐蔽的。有时候你传入的号码带空格、86前缀或者-分隔符系统拨号页解析时会把这些字符一并带到号码里。Android 的Intent会自动处理这些格式OpenHarmony 的Want.uri则不会你拼进去什么就是什么。我在原生侧做了个参数清洗把空格、横线、括号全部过滤掉再拼tel:。如果你有类似场景建议在 Dart 侧就做好号码规范化传到原生侧时已经是干净的纯数字。顺带说一句86这个前缀在鸿蒙拨号页里能正常识别不需要去掉。5. 实操细节与性能体验从能用到好用一波踩坑之后库终于在 OpenHarmony 真机上稳定跑起来了。结合这段时间的实际体验我整理了几个从“能用”到“好用”必须注意的点。第一界面上没有回调是常态别把 UI 卡在等待状态。callNumber从点击到系统拨号页真正显示完中间有 300~500ms 的间隔如果你在按钮上加了 loading就会看到一闪而过的菊花。比较好的做法是点击后立即跳转不等待返回把成功/失败都放到 SnackBar 提示里。第二重复点击要防抖。startAbility不是幂等的连续点两次系统会拉起两个拨号页堆叠在任务栈里用户返回的时候会连续关两页。第三注意冷启动和热启动的行为差异。在 App 完全冷启动后第一次调用拨号权限校验、Ability 路由加载都会慢一些耗时可能到 1 秒多。这个问题我遇到过几次所以建议是在应用启动后的首屏先静默申请好权限把耗时往后挪。第四真机和模拟器行为不完全一致。我用的官方模拟器能正常拉拨号页但不支持真正打电话有的第三方模拟器压根没有拨号应用调用startAbility会直接报错。所以拨号类功能务必准备真机否则很多问题你根本没法复现。第五参数校验不要只放在 Dart 层。原生侧拿到的number有可能为空字符串或者传进来一个非数字字符串这时去拼tel:空系统可能直接闪退或者无反应。原生侧做好空值判断返回一个自定义错误码比什么都强。6. 结语一句真心建议最后分享一点我自己的体会。flutter_phone_direct_caller本身是个很小的库但它在 OpenHarmony 上的适配过程折射出整个 Flutter 跨端生态向新平台迁移时的共性问题Dart 层是通的原生层是断的。不要迷信“加个依赖就能跑”不管是拨号、拍照还是定位拿到一个三方库后先去翻它的 android/ios 原生目录再想清楚 OpenHarmony 侧需要补哪些逻辑。如果你准备在项目里引入这个库做一键拨号我的建议是核心代码直接用我上面给的方案权限声明按项目实际场景配真机调试前先确认权限状态。踩过一轮坑之后你会发现其实拨号功能真的没有太多难点最多半天时间就能跑通。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →