拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter 插件鸿蒙适配实战:用 MethodChannel 实现 OpenHarmony 系统分享

Flutter 插件鸿蒙适配实战:用 MethodChannel 实现 OpenHarmony 系统分享 1. OpenHarmony 上的分享功能为什么不能照搬 Android 思路做 Flutter 跨端开发做了几年遇到分享这个需求我向来是不慌的——不就是调一次系统分享面板吗文本、图片、链接各来一发Android 上 Intent 一拉iOS 上 UIActivityViewController 一弹完事。直到我真正开始把 Flutter 应用往 OpenHarmony 设备上移植才发现这套经验完全不够用。OpenHarmony 的分享机制底层不叫 Intent叫 Want。它有一套独立的 Ability 拉起规则和参数体系fltter engine 跑在上面时Dart 侧调系统分享能力是没有现成路径的。更麻烦的是Flutter 生态里成熟的分享插件比如 share、share_plus默认只实现了 Android 和 iOS 的平台代码OpenHarmony 设备上是拿不到 SystemShare 的。所以拿到系统分享功能这个需求时我的第一反应是不能等插件作者适配得自己动手。这条路走下来核心工作其实就两件事第一是找到 API 设计和扩展性都合适的 Flutter 第三方库第二是通过 Flutter 的 MethodChannel 机制把分享请求从 Dart 侧送进 OpenHarmony 的系统 Ability。这一整套做完share_extend 这个名字才真正从一个候选插件变成了鸿蒙设备上的可用分享能力。1.1 先弄清楚 OpenHarmony 的 Want 到底想干什么想要在 OpenHarmony 上做系统分享必须先理解它和 Android 的差异。Android 的分享 Intent 核心是ACTION_SENDtypeEXTRA_TEXT/EXTRA_STREAM系统根据 MIME 类型匹配能处理这条 Intent 的应用。OpenHarmony 把这套东西换了个实现Action字段负责描述意图例如分享文本通常用系统预置的ohos.want.action.sendToDataUri和type负责描述要分享的数据type是 MIME 类型parameters是额外参数比如ability.params.stream用来传文件流。这意味着 Flutter 插件层要做的事其实就是把 Dart 侧给的字符串、文件路径、图片字节流翻译成 Want 参数再通过context.abilityContext.startAbility(want)或者startAbilityForResult把系统分享面板拉起来。对 Flutter 开发者来说这段代码不会出现在 Dart 侧而是在插件的 OpenHarmony 平台实现里。所以选哪个插件做基底决定了这个翻译层要自己写的部分有多少。1.2 share / share_plus / share_extend 三条路线的取舍当时我对比了三个插件插件Android 支持iOS 支持OpenHarmony 支持分享图片方式API 灵活度share完整完整无只支持 XFile 路径中share_plus完整完整社区有 PR 但未合入XFile 路径 base64中高share_extend完整完整无路径 base64 多类型混合高share_extend 的 API 设计是我见过最贴近跨端系统分享场景的。它不要求你必须把图片放在某个临时文件里而是可以直接传 base64 字符串这对 OpenHarmony 适配是一个极大的便利——鸿蒙的图片分享能力本身就对内存字节流接受度很高少一层文件落地就少很多权限和路径问题。而且 share_extend 允许一次调用同时分享 text filePaths 或 text imageBase64这个混合分享能力在处理一段文字配一张截图这种高频场景时非常有用。share_plus 想做到这个就得自己拼多个 channel 调用逻辑就散了。1.3 share_extend 源码里的插件扩展点在哪里选定 share_extend 之后我第一步是翻它的源码结构。Flutter 插件的标准结构是lib/放 Dart APIandroid/和ios/放平台实现share_extend 也不例外。关键文件是share_extend.dart里暴露的三个方法ShareExtend.share()文本、shareExtend.shareImage()图片、shareExtend.shareFile()文件。这三个方法最终都会汇聚到一个私有方法里通往下层MethodChannel.invokeMethod。让我觉得这个插件适配 OpenHarmony 成本可控的核心原因就在这里它所有平台相关逻辑都收口在ShareExtendPlugin的onMethodCall分发里方法名就三个参数结构也清晰。适配时只要在工程里新增一个 OpenHarmony 的 har 包在这个插件类里处理shareText、shareImage、shareFile三种 case剩下的 Dart 侧 API 完全不用动。2. 环境准备Flutter SDK 与 OpenHarmony 工具链的前置组合正式开始之前环境是最容易卡人的环节。OpenHarmony 上的 Flutter 开发不是装个 Flutter SDK 就能跑的它需要一整套版本咬合正确的工具链。我的建议是先花半天把所有版本确定下来再动手写代码不然中途升版本会搞得你怀疑人生。2.1 版本组合怎么选OpenHarmony 的 Flutter 适配走的是 OpenHarmony SIG 维护的分支发布节奏和 Google 主仓有偏移。我实测下来比较稳的一套组合是OpenHarmony SDKAPI 104.0 Release 及以上Flutter SDKOpenHarmony 4.0 适配分支基于 Flutter 3.7 至 3.10 之间DevEco Studio4.0 Release配套的 hvigor 版本Flutter SDK 的 path 里要能看到flutter_ohos相关目录这套组合的验证标准很简单用 DevEco Studio 新建一个 OpenHarmony 空工程然后在同一 IDE 里打开 Flutter 插件的 ohos 示例工程如果能跑起来说明工具链基本咬合了。注意不要手痒去把 Flutter SDK 升到 3.16 再用在 OpenHarmony 4.0 上。OpenHarmony 的 Flutter engine 还在持续适配版本跳太大容易出现 Dart AOT 产物格式不兼容运行时报错查起来非常费劲。2.2 配置文件里藏着的是权限不是代码OpenHarmony 工程里有一个很容易忽略的文件ohos/module.json5它管理着模块的权限声明。分享功能至少要声明ohos.permission.READ_IMAGEVIDEO读图库图片和ohos.permission.READ_MEDIA读媒体文件如果还要分享下载目录里的文件ohos.permission.READ_DOCUMENT也得加上。这段配置往往被放在最后才处理但缺了它你会发现一个诡异现象分享面板能弹出来但图片输出到微信或其他应用后是空的或者在相册选图阶段直接崩溃。因为权限缺失导致文件读取失败系统分享面板拿不到数据不会报错只会安静地失败。2.3 环境变量和 IDE 识别的经典问题我踩过的第一个坑是DevEco Studio 识别不到 OpenHarmony 签名设备但 adb 能看到。原因通常是 IDE 用的hdcHarmonyOS Device Connector路径没有配置到系统环境变量里。解决方式是在.bashrc或 Windows 环境变量里加入 DevEco SDK 自带的toolchains目录例如export OHOS_SDK_HOME/path/to/ohos-sdk export PATH$PATH:$OHOS_SDK_HOME/9/toolchains配好之后在终端跑hdc list targets能列出设备就说明 IDE 也能识别了。3. share_extend 接入实战从依赖声明到分享面板首次调起环境就绪之后进入正题。这里我会把从零接入的完整路径走一遍包括依赖声明、权限、Dart 侧调用以及 OpenHarmony 平台侧的关键实现。3.1 依赖声明和版本锁定share_extend 在 pub.dev 上的最新版本是 2.0.3我用的版本再往上的版本我没有实测过但直接把它加进 OpenHarmony 工程的pubspec.yaml还不行因为 pub 默认拉到的插件包不含 ohos 实现。实操做法是先把 share_extend 源码 fork 或直接下载到本地third_party目录在pubspec.yaml里用 path 引用dependencies: flutter: sdk: flutter share_extend: path: ./third_party/share_extend然后在 share_extend 源码工程里新增ohos目录把插件的 OpenHarmony 实现放进去。这一步做完flutter pub get 才会把 ohos 的 har 一起打进去。为什么我不直接用 pub 的线上版本而是用本地路径因为插件在 pub 上架后OpenHarmony 分支的代码不会自动同步只有源码里真正含ohos目录的版本才能被 Flutter 的 ohos 构建链识别。用 path 依赖可以确保你改的是那份带鸿蒙实现的代码排查问题的时候路径也是可控的。3.2 Dart 侧最小可行代码依赖加好之后Dart 侧其实不需要做任何针对 OpenHarmony 的特殊处理这个体验对 Flutter 开发者来说很舒服import package:share_extend/share_extend.dart; // 分享纯文本 ShareExtend.share(来自 OpenHarmony 的分享测试, text); // 分享单张图片 ShareExtend.shareImage(/data/storage/el2/base/files/share_test.png, image); // 分享 base64 图片 String base64Str iVBORw0KGgo...; // 自己生成或从网络获取 ShareExtend.shareImageBase64(base64Str, image, share_test.png);这三行代码覆盖了系统分享最常见的三个入口。实战中分享文本和分享图片是最高频的base64 那条路径在分享网络图片这个场景特别好用因为不用先把字节流写成临时文件。首次跑到ShareExtend.share()的时候我的预期是弹一个系统分享面板。但真相是第一次我拿到了一个空的分享面板里面列了很多应用但点开目标应用后没有任何内容。这个坑留在第五节细说先继续讲平台侧的实现。3.3 OpenHarmony 平台实现把 Dart 参数翻译成 Want这是本篇文章最核心的代码。share_extend 插件在 OpenHarmony 侧的实现本质是一个ShareExtendPlugin它继承 FlutterPlugin 并注册 MethodChannelimport { FlutterPlugin } from ohos/flutter_ohos; import { MethodCall, MethodChannel } from ohos/flutter_ohos; export class ShareExtendPlugin implements FlutterPlugin { private methodChannel: MethodChannel; onAttachedToEngine(binding: PluginBinding): void { this.methodChannel new MethodChannel(binding.getBinaryMessenger(), share_extend); this.methodChannel.setMethodCallHandler((call: MethodCall) { return this.handleMethodCall(call); }); } private async handleMethodCall(call: MethodCall): Promiseany { switch (call.method) { case shareText: return this.shareText(call.arguments as Mapstring, string); case shareImage: return this.shareImage(call.arguments as Mapstring, string); case shareFile: return this.shareFile(call.arguments as Mapstring, string); default: throw new Error(Unknown method: ${call.method}); } } }shareText的完整实现要点是构造 Want 并指定 actionasync shareText(args: Mapstring, string): Promisenumber { let text args[text] || args[texts] || ; let want: Want { action: ohos.want.action.sendToData, type: text/plain, parameters: { ability.params.stream: text, ability.params.title: 分享文本 } }; // context 是通过 UIAbilityContext 获取的 await this.context.startAbility(want); return 0; }这里有个细节要提醒大家ability.params.stream在 OpenHarmony 里可以被塞字符串也可以被塞文件流。分享纯文本时直接塞字符串是最省事的分享图片时优先塞fileUri经fs.open拿到的流再把流塞进ability.params.stream。3.4 图片分享的两种实现路径图片分享是 share_extend 相对其他插件最有优势的场景。在 OpenHarmony 上它有两条实现路线路径分享如果图片已经落在本地例如下载到了files目录直接取文件路径通过fileUri.getUri拿到的串传给 Want 的uri字段base64 分享网络图片或者内存缓存图片不落盘直接用 base64 字符串。此时 Want 的 type 是image/*同时需要把 base64 解码成ArrayBuffer放进parameters[ability.params.stream]。async shareImage(args: Mapstring, string): Promisenumber { let path args[path] || args[paths] || ; let type args[type]; let name args[name] || share_image.png; let fileUriObj fileUri.getUriFromPath(path); let stream await fs.open(fileUriObj.path, fs.OpenMode.READ_ONLY); let want: Want { action: ohos.want.action.sendToData, type: type || image/*, uri: fileUriObj.toString(), parameters: { ability.params.stream: stream, ability.params.title: name } }; await this.context.startAbility(want); return 0; }base64 路径几乎一模一样只是把uri换成解码后的字节流async shareImageBase64(args: Mapstring, string): Promisenumber { let base64Str args[base64]; let type args[type]; let name args[name] || share_image.png; // base64 转 ArrayBuffer let buffer base64ToArrayBuffer(base64Str); let want: Want { action: ohos.want.action.sendToData, type: type || image/*, parameters: { ability.params.stream: buffer, ability.params.title: name } }; await this.context.startAbility(want); return 0; }两条路径实测都能在微信、邮箱、信息应用里正常唤起分享草稿。会写到这里基本说明 share_extend 在 OpenHarmony 上的核心能力已经打通了。4. 拆穿这层壳MethodChannel 如何把 Flutter 分享请求送进系统 Want分享功能跑通之后我反而更想把中间那层桥接彻底讲清楚。因为 OpenHarmony 的 Flutter 插件机制和 Android 有差异很多问题如果不理解这层壳排查起来就是黑盒乱猜。4.1 一次分享请求的完整调用链从 Dart 侧的ShareExtend.share()到 OpenHarmony 系统分享面板全程经过五个环节Dart 侧调用ShareExtend.share()它内部走platformChannel.invokeMethod(shareText, args)Flutter engine 把 MethodCall 编码成二进制消息经由BinaryMessenger发送到 OpenHarmony native 侧ShareExtendPlugin的MethodChannel.setMethodCallHandler收到消息解析 method 名和参数插件代码构造 Want 对象调用context.startAbility(want)OpenHarmony 系统根据 Want 匹配可接收的应用弹出分享面板。在整个链路里Dart 和 TSArkTS两侧通过字符串方法名做契约。任何一侧方法名对不上结果是 Dart 侧收到MissingPluginException这也是 OpenHarmony 插件适配最常见的启动期问题。// Dart 侧调用后可以捕获异常判断插件是否注册成功 try { await ShareExtend.share(测试内容, text); } on MissingPluginException catch (e) { print(插件未注册: ${e.message}); }如果你的应用在 OpenHarmony 真机上出现这个异常九成是插件类没有在EntryAbility的onCreate里调用ShareExtendPlugin.register()。Flutter 的 ohos 适配要求所有原生插件在 Ability 启动阶段手动注册这个动作和 Android 自动注册不太一样。4.2 Want 构造的几个核心参数是怎么确定的很多人遇到的问题是照着官方样例写了 Want但分享面板不弹或者弹出来是空的。我帮同事排查时发现大多数时候是 Want 参数填错了。OpenHarmony 的 Want 结构有几个关键字段分享场景下尤其注意字段分享文本分享图片分享文件actionohos.want.action.sendToData同左ohos.want.action.sendToDatatypetext/plainimage/png或image/*与文件后缀匹配的 MIMEuri可不填文件 uri 或流文件 uriparameters.stream文本内容图片字节流文件流parameters.title分享标题分享标题分享标题action 不必多说sendToData对应的是发送数据这个系统能力。type 很关键它是系统做应用匹配的过滤条件填image/*能匹配所有支持图片分享的应用填image/png则只匹配对 png 有明确声明的应用实测微信在image/png下也还能匹配到但部分邮件应用可能匹配不到。parameters.title比较容易忽略它会在分享面板顶部标题栏显示。不传也不会崩但很多系统应用会把 title 作为分享内容的一部分带入草稿例如邮件会把 title 当作邮件标题所以建议都带上。4.3 为什么说 MethodChannel 参数结构要简单share_extend 的 Dart 侧代码在传参时做了一件很聪明的事情所有分享类型最终都用MapString, String传递。static Futurevoid share(String text, String type) async { var args String, String{ text: text, }; await _channel.invokeMethod(shareText, args); }参数结构保持扁平对 OpenHarmony 侧的类型转换压力最小。ArkTS 侧的call.arguments拿到的就是Mapstring, string不需要再嵌套解包。别小看这个设计有些插件在传文件列表时用ListMapOpenHarmony 侧解析 json 数组再转 ArrayBufferAPI 10 上容易踩序列化转换的坑。如果你要自己适配插件建议严格遵守Dart 侧只传扁平 Map值为 String 或 int这个原则。复杂结构走 JSON 序列化反而更容易出问题。5. 踩坑实录分享面板不弹出、图片不展示、结果回调失效任何第三方库适配到新平台不可能一遍跑通。这里我记录三个最典型的坑以及完整排查链路希望帮你省下两三个通宵。5.1 错误一分享面板弹出但内容是空的这个现象最迷惑人因为你看到的是系统分享面板正常工作但目标应用收到的内容为空。我第一次遇到时排查链路是这样的先看 Dart 侧有没有异常——没有shareText返回正常再看 OpenHarmony 侧日志——用hdc shell hilog | grep ShareExtend没发现报错单独测试 shareText 的文本内容——发现问题出在parameters的 key 写错了。OpenHarmony 的系统应用约定使用ability.params.stream取值但部分系统应用版本还认ability.params.text。我一开始只写了ability.params.stream在部分版本上内容丢失。修复方式是同时塞两份parameters: { ability.params.stream: text, ability.params.text: text, ability.params.title: 分享文本 }这个兼容性写法不仅适用于文本图片分享里的 base64 数据同时塞一份到ability.params.stream和ability.params.fileUriList也能提升兼容面。代价仅是内存多一点可接受。5.2 错误二图片分享到微信后只有文件名没有图这个问题定位起来比第一个快因为它和权限强相关。现象是分享面板正常微信也弹出了但聊天框里只有一个share_image.png的文件名点击没有预览图发送后对方看到的是空文件。定位过程先确认图片源文件是否存在——用hdc file read /data/storage/el2/base/files/share_test.png文件存在且大小正常再查应用是否申请了媒体读取权限——module.json5里漏了ohos.permission.READ_MEDIA补上权限后重新安装——问题消失。这里有个细节OpenHarmony 的权限分为system_grant安装时授权和user_grant运行时动态弹窗确认两类。READ_MEDIA属于 user_grant所以不只要在module.json5声明还要在代码里动态申请import { abilityAccessCtrl, Permissions } from ohos.abilityAccessCtrl; async function requestPermission(): Promisevoid { let atManager abilityAccessCtrl.createAtManager(); let permissions: Permissions [ ohos.permission.READ_MEDIA, ohos.permission.READ_IMAGEVIDEO ]; let result await atManager.requestPermissionsFromUser(context, permissions); if (result.authResults.some(r r ! 0)) { // 有权限被拒绝需引导用户手动开启 } }分享图片前先跑这段权限请求比直接读取文件再失败重试要合理得多。5.3 错误三分享完成后 Dart 侧拿不到结果share_extend 的 API 设计里share()返回值是 Future但没有设计分享完成/取消的回调。这在 Android 上可以接受因为 Activity 的onActivityResult需要插件层单独实现。但到了 OpenHarmony如果希望感知分享结果比如用户取消分享用来做埋点就需要额外处理。问题是startAbility()是拉起即返回的它不代表分享完成。真正能感知结果的是startAbilityForResult()它会在目标应用处理完成后回调。适配思路是在 ArkTS 插件里实现abilityContext.startAbilityForResult(want, callback)把结果码回传到 Dart 侧async shareTextWithResult(args: Mapstring, string): Promisenumber { let want: Want { /* 构造同上 */ }; let result await this.context.startAbilityForResult(want); // result.resultCode 0 表示成功-1 表示用户取消 return result.resultCode; }Dart 侧相应增加一个方法static Futureint shareWithResult(String text, String type) async { return await _channel.invokeMethod(shareTextWithResult, { text: text, }); }数据回传链路变成分享面板关闭 →startAbilityForResult回调 → 插件层返回 resultCode → MethodChannel 回传 Dart 侧。这比单纯startAbility的体验完整很多建议在正式产品里采用。但这里也顺便提醒startAbilityForResult在部分 OpenHarmony 系统应用如某些第三方输入法上会有超时需要设定ONLY_IF_NEEDED之类的超时参数或者容忍它 3 到 5 秒不回调。不要因为一次回调失败就认为用户取消了分享否则埋点数据会很脏。5.4 排查日志的实用命令OpenHarmony 真机调试时最有效的日志查看命令是hdc shell hilog | grep -E ShareExtend|FlutterJNI|MethodChannelhilog默认日志量很大务必 grep 过滤。定位 plugin 注册问题看FlutterJNI定位分享调用链看ShareExtend。另外注意hdc shell在 API 10 上的部分版本需要加-t指定目标设备多设备连接时尤其容易踩。6. 覆盖更全的使用场景与进阶扩展思路share_extend 在 OpenHarmony 上跑通基础分享后你会发现思路一下就打开了。很多原生能力都能照这个路径接入不必等官方适配。6.1 文件分享与多文件混合分享share_extend 原生 API 里没有多文件分享但它允许传paths数组。在 OpenHarmony 侧多文件的处理方式是把每个文件流放进ability.params.streamsasync shareFiles(args: Mapstring, string): Promisenumber { let paths: Arraystring JSON.parse(args[paths] || []); let streams []; for (let path of paths) { let fileUriObj fileUri.getUriFromPath(path); let file await fs.open(fileUriObj.path, fs.OpenMode.READ_ONLY); streams.push(file); } let want: Want { action: ohos.want.action.sendToData, type: */*, parameters: { ability.params.streams: streams, ability.params.title: 多文件分享 } }; await this.context.startAbility(want); return 0; }实测在文件管理器里选中多个文件再点分享能正常唤起微信的多文件发送。这个能力在业务场景里做导出多个报表附件特别实用。6.2 EventChannel 扩展分享状态的主动通知如果你需要更精细的分享状态管理比如分享成功、取消、失败而不是在startAbilityForResult里被动等待可以引入 EventChannel。思路是插件在启动时注册一个 EventChannel分享事件发生后主动向 Dart 侧推消息。// 插件内注册 EventChannel private eventChannel: EventChannel; private eventSink: EventSink; onAttachedToEngine(binding: PluginBinding): void { this.eventChannel new EventChannel( binding.getBinaryMessenger(), share_extend_events ); this.eventChannel.setStreamHandler({ onListen: (args, sink) { this.eventSink sink; }, onCancel: () { this.eventSink null; } }); } // 分享完成后主动推状态 private notifyShareResult(code: number): void { if (this.eventSink) { this.eventSink.success({ resultCode: code }); } }Dart 侧监听final _eventChannel EventChannel(share_extend_events); _eventChannel.receiveBroadcastStream().listen((event) { int resultCode (event as Map)[resultCode] as int; if (resultCode 0) { // 分享成功 } else { // 分享取消 } });这个方案的优点是不需要每个分享方法都等回调分享面板一关结果立刻推送适合做分享成功打点和分享失败后重试提示的交互逻辑。6.3 其他插件适配的通用套路把 share_extend 适配到 OpenHarmony 的经验完全可以复制到其他 Flutter 插件上先看插件的 MethodChannel 方法名和参数结构绘制出方法名 → 参数格式 → 预期返回清单对应到 OpenHarmony 的能力映射查 API 文档确认底层用哪个 Ability 或 API 能实现在插件工程里新增 ohos 目录新建同名 plugin 类注册到 EntryAbility先用最简单的方法调用跑通链路再逐步添加复杂参数多设备真机测试特别关注不同系统版本在parameters字段兼容性上的差异。这套流程走完你会发现 OpenHarmony 上的 Flutter 插件开发并没有想象中吓人核心就是 Want MethodChannel 权限管理三板斧。7. 验证清单发布前你至少要确认这几件事功能写完只是开始。我在交付前会跑一个固定的验证清单每一项都直接关系最终体验检查项验证方法通过标准文本分享分享到备忘录、邮件、微信纯文本无乱码无内容缺失单图分享分享到微信、相册图片预览正常发送后对方可见base64 分享分享网路图片不发原图字节流能正常展示多文件分享分享 2 个以上 PDF目标应用能收到多个附件取消行为打开分享面板后返回应用无白屏Dart 侧无异常权限拒绝拒绝媒体权限后分享有引导弹窗不崩溃重复分享连续分享 5 次无内存泄漏无崩溃前四项偏功能正确性后三项偏健壮性。第六项特别重要因为用户很容易误点不允许一个权限被拒后就静默失败的分享功能在用户那里就是这个 App 分享坏了。另外一个体验细节是分享面板的 title。不要用 分享 这种通用文案最好能带上业务上下文比如分享订单 #1024 详情。在 OpenHarmony 上这个 title 会直接显示在分享面板顶部也会被部分应用作为默认文件名或邮件标题带业务信息能让接收方省很多判断成本。我实际使用下来的体会是OpenHarmony 的 Flutter 生态正在快速补课插件适配的思路其实比想象中统一。share_extend 这套扁平参数 Want 构造 权限管理的三板斧不仅解决了系统分享的问题更是一个完整的插件迁移范本。后续遇到其他 Flutter 插件在 OpenHarmony 上不支持翻一翻插件的 MethodChannel 定义照这个路子写一版 openharmony 实现大多数常见能力都能自己搞定。
返回列表