多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

鸿蒙Flutter插件适配实战:share_plus分享功能改造全记录

鸿蒙Flutter插件适配实战:share_plus分享功能改造全记录 从年初开始身边越来越多团队把鸿蒙端的Flutter适配提上日程。前阵子我们正好接到一个任务要在鸿蒙设备上跑通项目的分享功能。工程原本用的是Flutter生态里非常成熟的share_plus在Android和iOS上一直很稳结果一放到鸿蒙环境里调用分享后完全没反应。查了一圈发现社区里关于share_plus鸿蒙适配的实战资料少得可怜大部分帖子都停在“能不能用”的层面没有真正给出可落地的改造方案。这篇文章就把我们当时的适配过程、踩过的坑和最终跑通的实现方式完整记录下来给准备做Flutter跨平台分享功能鸿蒙适配的团队一个可直接参考的路线。这篇内容适合三类人看正在做鸿蒙Flutter应用移植的客户端开发、需要了解鸿蒙插件适配原理的进阶Flutter开发者以及准备在团队内推进跨端统一方案的架构师。我会从适配方案选型、Dart层改造、鸿蒙端原生实现到问题排查这条主线来写整个过程都在DevEco Studio和Flutter SDK的常规开发环境里完成不涉及任何特殊渠道或非公开工具。1. 适配前必须想清楚的事为什么share_plus在鸿蒙上“失灵”1.1 先搞清楚share_plus的工作机制share_plus是Flutter社区最常用的分享插件它做的事情本质上非常简单把Flutter层传入的文本、链接、文件路径等数据通过平台通道MethodChannel交给原生端由原生端拉起系统分享面板。在Android上原生端会调用Intent.createChooser在iOS上原生端会走到UIActivityViewController。整个流程是一条典型的单向调用链Dart侧发起请求、原生侧执行动作、返回结果给Dart侧。问题就出在这个“原生侧”上。share_plus官方目前并没有提供鸿蒙OpenHarmony的端侧实现它的MethodChannel在鸿蒙的Flutter引擎里找不到对应的处理函数调用链直接断掉。表现出来就是Dart侧调用Share.share没有报错但原生系统没有任何反应分享面板压根不会弹出来。1.2 鸿蒙Flutter引擎的特殊性鸿蒙的Flutter适配走的是OpenHarmony Flutter社区维护的分支底层引擎能力与标准Flutter一致但插件和原生的通道机制需要按照鸿蒙的开发范式重新实现。这意味着你可以继续在Dart层写MethodChannel但消息发到鸿蒙侧之后必须由鸿蒙的原生代码ArkTS来接收。这就给share_plus的适配留了一条清晰的技术路径保留Dart侧的调用API不变在鸿蒙端补齐原生实现。说得直白一点就是在鸿蒙工程里写一个和share_plus使用相同通道名和方法名的处理方法把系统分享面板拉起来。1.3 适配方案的选型思路当时我们评估了三种方案第一种是直接等官方支持风险在于时间不可控项目排期不允许。第二种是把share_plus的源码fork到本地在鸿蒙端添加原生实现这种方式最彻底但要维护一份私有插件代码后续同步官方更新比较费劲。第三种是做一个轻量级的“中间层适配”Dart侧保留share_plus的调用代码但通过自定义通道名在鸿蒙原生侧接管并实现分享逻辑。我们最后选了第三种因为它的改动面最小风险也最低。实际测试下来这个方案的切换成本确实低。Dart层原本写好的Share.share调用只改一个通道名或者加一个代理类就能兼容业务方不需要动任何分享逻辑代码。鸿蒙端只需要实现一个插件类注册到Flutter引擎上即可。2. 鸿蒙Flutter工程搭建与适配前置条件2.1 环境准备DevEco Studio Flutter SDK鸿蒙Flutter开发需要同时安装标准Flutter SDK和OpenHarmony版的Flutter SDK这个环节是整个适配工作的“地基”环境不对后面写的所有代码都没法跑。我们的实际环境组合是DevEco Studio 4.0及以上版本版本越高对ArkTS语法支持越好Flutter SDK标准版用于Dart层的编译和执行OpenHarmony的Flutter SDK分支用于生成鸿蒙端的Flutter引擎和插件模板标准版和鸿蒙分支的Flutter版本必须保持同期否则Dart层编译会报一些莫名其妙的运行时错误。建议先跑通一个空工程再用flutter doctor把环境检查项全部过一遍。2.2 在鸿蒙工程里引入Flutter模块如果你的项目原本就是Android/iOS双端现在要加鸿蒙端最直接的做法是在现有的HarmonyOS工程里创建一个Flutter模块然后把两边的工程目录放到同一个项目结构下。我们的目录结构大致是这样的project_root/ ├── harmony/ # HarmonyOS 原生工程 ├── flutter_module/ # Flutter 模块 │ ├── lib/ # Dart 代码 │ └── pubspec.yaml └── oh-package.json5 # 鸿蒙端依赖配置Flutter模块通过鸿蒙侧提供的一个FlutterPage或者FlutterAbility承载并且用标准的FlutterEngine来加载。我们之前踩过一次坑在鸿蒙架构里并不是所有页面都原生支持Flutter渲染必须通过特定的容器去加载。如果你发现鸿蒙原生页面上怎么都显示不出Flutter内容大概率是这一步没有配好。2.3 基础链路验证在鸿蒙上跑通Flutter页面不要把适配工作建立在“可能可以跑”的基础上。我们最先做的一件事是生成一个全新的Flutter工程在鸿蒙设备上跑通了一个最简单的页面再在里面显示一行文本和一个按钮。只有当你确认flutter run到鸿蒙设备上能正常显示Flutter UI并且按钮可以正常响应Dart代码才算是具备后续适配的前提。这个验证步骤看起来简单但能帮你省下很多排查时间。尤其是有些团队一上来就把老项目塞进鸿蒙工程结果分不清报错是环境问题还是业务代码问题排查起来特别痛苦。3. 核心适配方案Dart层如何穿透到鸿蒙原生3.1 阅读share_plus源码确认通道名和协议开始写任何代码之前先去找share_plus里Share.share的实现通常路径是packages/share_plus/lib/src/share_plus_linux.dart或是share_plus_platform_interface。我们重点看了MethodChannelShare这个类它的核心逻辑非常清晰const channel MethodChannel(dev.fluttercommunity.plus/share); const String _channelName dev.fluttercommunity.plus/share;这就是我们要对接的“电话号码”。鸿蒙端要做的就是注册一个同名的MethodChannel处理器。另外share_plus的share方法传参结构也很固定把文本、主题、sharePositionOrigin等参数包成一个Map或ShareParams对象。这个数据结构在后面鸿蒙原生解析的时候要用到。一个小技巧share_plus最新版本会把分享参数封装成ShareParams如果你在鸿蒙端解析Map时发现字段对不上可以先在Dart侧打一条日志把完整Map打印出来。我们就是这样定位到参数名差异的。3.2 修改Dart侧共享库替换与降级兜底既然官方share_plus在鸿蒙端没有原生实现最直接的思路是改造Dart层的调用路径但又不改动业务代码。我们做了一个轻量的封装叫ShareProxy内部维护一套方法名与share_plus完全一致的APIshare、shareUri、shareWithResult但底层会根据运行平台选择不同的通道。class ShareProxy { static const MethodChannel _harmonyChannel MethodChannel(harmony_share_plus); static FutureShareResult share(String text, {String? subject}) async { // 在鸿蒙平台走自研通道在其他平台仍然走 share_plus final result await _harmonyChannel.invokeMethod(share, { text: text, subject: subject, }); return ShareResult(result); } }这个方案的好处是业务侧只需要把Share.share替换成ShareProxy.share完全能够在鸿蒙上跑通分享。而且后续如果官方发布了鸿蒙支持版本只需要改动ShareProxy内部实现业务侧代码一行都不用动。如果你的项目里分享逻辑散落在多个地方这个代理层方案比直接替换share_plus更值得投入。3.3 通道名称冲突问题官方通道无法直接用吗你可能会问能不能让鸿蒙端直接注册dev.fluttercommunity.plus/share这个官方通道名这样Dart侧不改也能生效理论上可行但实际会有两个问题。一是share_plus插件本身如果还在依赖列表里它的Dart层会在初始化时尝试注册该通道虽然有同名通道更新机制setMethodCallHandler但时序上稍不注意就会互相覆盖。二是share_plus内部的参数结构、返回格式在不同版本之间有变化直接对接维护成本更高。我们最终决定用自定义通道名就是在权衡可维护性和改造量之后做的选择。4. 鸿蒙原生侧实现用ArkTS拉起系统分享能力4.1 新建鸿蒙端插件类注册MethodChannel鸿蒙原生端的实现分为两步注册通道、处理分享请求。我们用的是DevEco Studio里ArkTS的模块化写法新建一个SharePlugin.ets文件内部注册名为harmony_share_plus的通道。import { MethodChannel } from ohos/flutter_ohos; export class SharePlugin { static register(engine: FlutterEngine) { const channel new MethodChannel(engine, harmony_share_plus); channel.setMethodCallHandler((call, result) { if (call.method share) { try { const args call.arguments as Recordstring, Object; ShareHelper.showShareDialog(args[text] as string, args[subject] as string); result.success(true); } catch (e) { result.error(share_failed, e.toString(), null); } } else { result.notImplemented(); } }); } }需要注意鸿蒙Flutter SDK里的MethodChannel类名与标准Flutter引擎不完全一致早期版本甚至叫MethodChannel但后续版本可能有所调整。建议以你使用的OpenHarmony Flutter版本的实际API文档为准。4.2 拉起系统分享面板Want与Share Kit的选型鸿蒙系统提供了两种分享路径传统方式是通过Ability的Want机制将待分享内容封装成Want参数后启动系统侧的分享面板另外一种是使用系统提供的Share Kit功能更强但也更复杂。如果你的App只需要分享纯文本或单个链接Want机制完全够用。我们当时的实现是构建一个隐式Want指定action为系统分享对应的动作名并把文本内容放到parameters里。import { common, Want } from kit.AbilityKit; import { BusinessError } from kit.BasicServicesKit; export class ShareHelper { static async showShareDialog(text: string, subject: string) { const context getContext(this) as common.UIAbilityContext; const want: Want { action: ohos.want.action.sendData, parameters: { ability.params.stream: text, ability.params.additionalText: subject ?? , }, }; try { await context.startAbility(want); } catch (e) { const err e as BusinessError; console.error(ShareHelper startAbility error: ${err.code} ${err.message}); } } }注意startAbility是异步方法必须用await或者.then处理否则在部分鸿蒙版本上会静默失败表现就是点了按钮“没有反应”。我们当时调试了很久才定位到这个问题。如果你要分享文件Want机制也能做需要把文件Uri放到parameters里同时注意申请对应的存储权限。文件分享的权限检查比文本分享严格不少后面我会专门讲。4.3 从Share Kit到更成熟的分享体验如果产品需求不止于“发一段文本”还需要分享图片、多文件、甚至自定义分享面板UI那就得考虑接入鸿蒙的Share Kit。Share Kit提供了更完善的分享状态回调能告诉App用户是分享成功还是取消这一点是Want方案做不到的。我当时的建议是第一版先用Want机制快速打穿链路验证Flutter到鸿蒙原生通道的连通性第二版再根据产品需求决定是否迁移到Share Kit。因为Share Kit的集成复杂度高涉及资源校验、安全权限、UI配置等多方面不适合在适配初期就引入。4.4 注册插件到FlutterEngine写好SharePlugin之后必须在FlutterEngine创建时把它注册进去否则通道只会出现在Dart层原生侧永远不会收到消息。不同项目的注册时机略有不同但大流程是这样的let flutterEngine await FlutterEngine.create(context, { bundleName: com.example.myapp, moduleName: flutter_module, }); SharePlugin.register(flutterEngine);如果你的项目里已经有GeneratedPluginRegistrant机制也可以把插件类加进去统一管理但我们当时没有用这种方式而是直接在Ability的onCreate里手动注册。图省事的代价是后来加其他插件时都得手动注册一次所以还是建议尽早把插件注册表建立起来。5. 完整链路测试与踩坑实录5.1 真机调试从“没有反应”到弹出分享面板第一次在鸿蒙真机上测试时点击按钮没有任何反应。我们按照这个顺序排查第一步确认Dart侧有没有报错。结果invokeMethod正常返回了说明Dart侧没毛病。第二步在鸿蒙原生侧的setMethodCallHandler里加日志发现方法调用根本没进来。这就是典型的通道注册失败。第三步检查SharePlugin.register的调用时机发现FlutterEngine还没创建完成就去注册通道导致注册事件丢失。调整注册顺序后再次点击按钮日志显示原生侧收到请求但startAbility还是没效果。后来查了鸿蒙文档发现context需要是UIAbilityContext不能直接用getContext(this)拿到的上层上下文要做一个类型转换。改用getContext(this) as common.UIAbilityContext之后就正常弹出了分享面板。5.2 分享参数乱码与文本截断问题鸿蒙端的startAbility在传递文本参数时对长度和编码有一定限制。我们测试长文本分享时发现分享出去的文本被截断中文还出现乱码。这个问题的根源不在Flutter侧而在鸿蒙的Want参数序列化方式。解决办法是把文本内容先进行Base64编码再传给Want分享面板拿到后解码还原。虽然麻烦点但实测可以彻底解决中文和长文本问题。const encodedText Buffer.from(text, utf-8).toString(base64); const want: Want { action: ohos.want.action.sendData, parameters: { ability.params.stream: encodedText, }, };5.3 异步回调与生命周期风险鸿蒙的startAbility是异步的如果在分享结果返回之前Flutter页面被销毁原生侧的Promise可能会被提前回收导致回调不执行Dart侧的await一直挂着。我们当时的处理是在Dart侧为分享调用增加超时机制超时后自动返回失败避免用户无休止等待。try { final result await _harmonyChannel .invokeMethod(share, params) .timeout(const Duration(seconds: 5)); return ShareResult.success; } on TimeoutException { return ShareResult.unavailable; }这类问题其实在Android的高版本上也存在但在鸿蒙上更容易触发因为首版适配对系统能力边界还不熟悉。建议在项目初期就把超时机制加上否则上线后会出现大面积分享按钮卡住的情况。5.4 日志定位经验Dart和ArkTS日志时间戳对齐跨端问题最怕的就是不知道消息到底断在哪一侧。我的排查方式是在Dart侧invokeMethod前后各打印一条日志并在ArkTS侧setMethodCallHandler入口打印一条日志三个日志同时带上毫秒时间戳。如果Dart侧有前后两条日志、原生侧没有入口日志问题一定出在通道注册或Dart侧参数构建上如果原生侧有入口日志但分享面板没弹问题就出在原生实现上。这个“三段式日志”帮我们省了至少半天排查时间强烈建议直接复制使用。6. 适配之外的思考跨平台分享能力在鸿蒙上的后续演进6.1 文件分享与权限校验不可忽视如果分享功能后续要支持图片、文档等文件类型鸿蒙端的权限模型和Android差别很大。文件分享前必须确保目标文件对系统分享面板可见涉及文件Uri的授权、临时只读权限等。我们的经验是在分享前先调用文件管理模块校验文件存在性与可读性避免分享面板拉起后才发现文件打不开。可以做一个简单的封装把文件准备、Uri转换、授权逻辑放到原生侧Dart侧只传一个本地路径。这样业务方不用关心鸿蒙文件权限细节。6.2 推荐尽快建立鸿蒙插件自检清单这次适配完成后我们沉淀了一个鸿蒙端Flutter插件自检清单列了以下几项插件依赖是否同时兼容OpenHarmony Flutter SDKDart侧是否在通道调用时有try-catch和超时保护原生侧通道注册时机是否在FlutterEngine创建完成之后所有startAbility等异步方法是否有错误处理和用户提示原生侧日志是否能对应到Dart侧的实际调用参数以后每次加新插件先拿这份清单过一遍能省不少回归测试时间。6.3 抛开具体插件鸿蒙适配的核心方法论适配share_plus的过程本质上是一个通用问题的缩影如何让一个只面向Android/iOS的Flutter插件在鸿蒙上继续工作。方法论可以提炼为三步拆解插件的通道协议分析插件依赖的原生能力在鸿蒙端用对应系统API实现同等能力。这套方法同样适用于url_launcher、path_provider、shared_preferences等常用插件。在实际项目中我更建议团队做一个“鸿蒙适配中间层”把常用插件的鸿蒙端实现统一管理而不是每个业务团队各自造轮子。中间层的好处是统一处理通道注册、权限、日志、异常上报业务方调用的API保持一致切换平台时无感。这次share_plus的适配实践让我最大的感受是鸿蒙生态的发展速度比想象中快但很多成熟Flutter插件还停留在“能编译、不能跑”的状态。与其等着官方把每个插件都适配完不如掌握一套主动适配的方法遇到什么插件都能快速打通。如果你也在做类似的事情希望这篇文章能帮你少走几步弯路。后面我们还在做url_launcher和path_provider的鸿蒙适配等跑通了再继续给大家分享。
返回列表