Flutter 三方库 share_handler 的鸿蒙适配教程

本文配套仓库:https://atomgit.com/oh-flutter/share_handler(TAG:0.0.25-ohos-1.0.0-beta.1,分支:feat/ohos_share_handler_0.0.25)。本文解决的是另一件事:从上游 GitHub 仓库开始,把 share_handler 完整适配到 OpenHarmony / HarmonyOS 平台,并在模拟器上用真实的系统分享链路验证。

share_handler 是 pub.dev 上的一个"接收系统分享"插件(作者 AboutShout,MIT 协议,0.0.25)。它解决的是社交类应用的经典需求:用户在图库里选中一张图、在浏览器里选中一段文字,点系统分享面板里的"我的应用",应用要能拿到这份内容——无论是应用根本没启动(冷启动被分享拉起),还是已经在前台运行。对外的 Dart API 只有四个:getInitialSharedMedia(取冷启动分享)、sharedMediaStream(监听运行中的分享)、recordSentMessage(记录"发送消息"上下文供分享建议使用)、resetInitialSharedMedia(清空冷启动缓存)。

上游是一个标准的 federated plugin(联邦插件):主包 share_handler 按平台装配端实现,share_handler_platform_interface 定义 SharedMedia/SharedAttachment 模型与平台接口,share_handler_android、share_handler_ios、share_handler_macos 等各自实现。上游支持 Android、iOS、macOS、Linux、Web,唯独没有鸿蒙。本次适配新增 share_handler_ohos endorser 包:ArkTS 侧基于鸿蒙系统分享框架 @kit.ShareKit(systemShare.getSharedData / getContactInfo)实现接收,通过 flutter_ohos 框架的 AbilityAware 机制在插件内部接管 want 处理——宿主工程的 EntryAbility 保持模板原样即可,一行都不用改。

本文完整走一遍社区三方库适配的标准流程:把上游仓库同步到 AtomGit,拉到宿主机,建适配分支,补全 ohos 平台注册与 endorser 包,实现 ArkTS 原生层,补齐适配说明文件后提交分支与 TAG,最后用仓库自带的 example 在 DevEco 模拟器上验证冷启动接收、类型识别、持久化与热启动推送。中间会重点讲两个只有真正动手才会踩到的坑:hvigor 对含 utd 的 skills 条目强制要求 scheme 导致的构建失败,以及 UTD(Uniform Type of Data)类型判断不能靠字符串匹配的层级归属问题。

一、环境搭建

鸿蒙 Flutter 开发环境(ohos 版 SDK、DevEco Studio、签名配置)的完整搭建步骤,官方指南已经写得很细,直接照做即可:

Flutter OHOS 开发环境搭建指南

适配工作比单纯使用多一项要求:终端里 flutter 命令必须指向 ohos 版 SDK,因为后文自动补全 ohos 目录靠的是它提供的 flutter create --platforms ohos 能力。环境装好后用 flutter devices 确认能识别鸿蒙设备。本文实测使用的环境:

项版本
Flutter(ohos 版)3.41.10-ohos-1.0.1
DevEco Studio26.0.0.821
编译 SDKHarmonyOS 26.0.0(OpenHarmony API 26)
实测设备DevEco 模拟器 emulator 7.0.0.105(OpenHarmony API 26)

接收分享验证还需要一个能作为分享来源的系统应用,图库(照片)是最好用的一个:选图 → 分享 → 分享面板里应该出现我们的 example。

二、适配过程

2.1 将上游仓库同步到 AtomGit

鸿蒙 Flutter 三方库社区(oh-flutter 组织)托管在 AtomGit,上游项目在 GitHub,适配的第一步是把上游代码完整迁入 AtomGit 上为它新建的目标仓库 oh-flutter/share_handler。做法是把上游克隆下来,添加 AtomGit 远端后整库推送,保留全部 commit 历史与 TAG,后续上游发新版时也能用同样的方式增量同步:

# 克隆上游仓库,目录名与目标仓库保持一致
git clone https://github.com/AboutShout/share_handler.git share_handler
cd share_handler

# 关联 AtomGit 目标仓库
git remote add atomgit https://atomgit.com/oh-flutter/share_handler.git

# 推送全部分支与 TAG
git push atomgit --all
git push atomgit --tags

推送完成后,打开 AtomGit 上目标仓库的页面,能看到与上游一致的提交历史和源码目录:

在这里插入图片描述

图一:同步完成后 AtomGit 目标仓库的代码页

2.2 拉取代码到宿主机并认清联邦结构

从目标仓库把代码拉到本地,后续所有操作都在这份代码上进行:

git clone https://atomgit.com/oh-flutter/share_handler.git
cd share_handler

与 charset_converter 那类"单文件插件"不同,share_handler 上游是 monorepo 组织的联邦插件,仓库根下并列着主包与各端实现包:

share_handler/                              # 仓库根
├── share_handler/                          # 主包(app-facing package)
│   ├── lib/
│   │   ├── share_handler.dart              # 导出模型与 ShareHandler.instance
│   │   └── src/share_handler.dart          # 按平台选择 default_package(本次在此加 ohos 分支)
│   ├── pubspec.yaml                        # plugin.platforms 注册各端实现包
│   └── example/                            # 上游自带示例(接收分享演示页)
├── share_handler_platform_interface/       # 平台接口层:SharedMedia / SharedAttachment 模型
├── share_handler_android/                  # Android 端(pigeon 生成的强类型通道)
├── share_handler_ios/                      # iOS 端(pigeon)
├── share_handler_macos/  share_handler_linux/  share_handler_web/
├── share_handler_ohos/                     # 本次新增:鸿蒙 endorser 包
└── ...

两个决定后续做法的结构特征:其一,平台选择的注册点在主包 pubspec.yaml 的 plugin.platforms 段,每端一个 default_package,鸿蒙要加一行 ohos: default_package: share_handler_ohos;其二,Android/iOS 端的 Dart 层是 pigeon 生成的强类型通道(自定义消息编解码器),这套东西在鸿蒙端没有对应物,所以鸿蒙端不能复用上游的通道协议,要按 platform_interface 的标准方法自己实现一层(见 2.4 节)。

2.3 创建适配分支并补全 ohos 结构

先建适配分支。命名规则是 feat/ohos_<库名>_<版本号>,版本号取自上游 pubspec.yaml 的 version 字段(本库为 0.0.25):

git checkout -b feat/ohos_share_handler_0.0.25

适配涉及三处 ohos 化,逐个说明。

第一处,主包平台注册。在 share_handler/pubspec.yaml 的 plugin.platforms 段追加(dependencies 里同步追加 share_handler_ohos,仓库内用相对 path 依赖,与 android/ios 的写法一致):

plugin:
  platforms:
    android:
      default_package: share_handler_android
    ios:
      default_package: share_handler_ios
    ohos:
      default_package: share_handler_ohos   # 新增

dependencies:
  share_handler_ohos:
    path: ../share_handler_ohos

第二处,新建 share_handler_ohos endorser 包。结构上模仿 share_handler_android:包根放 pubspec.yaml(声明对 share_handler_platform_interface 与 flutter_ohos 的依赖)、lib/share_handler_ohos.dart(Dart 平台实现,2.4 节),然后在包根执行 flutter create --platforms ohos . 生成 ohos/ 原生骨架:

mkdir share_handler_ohos
cd share_handler_ohos
# 手写 pubspec.yaml 与 lib/share_handler_ohos.dart 后:
flutter create --platforms ohos .

生成物中与插件直接相关的文件及各自职责:

文件职责
ohos/index.ets模块入口,导出插件类供引擎注册
ohos/oh-package.json5模块包描述,声明对 @ohos/flutter_ohos 的依赖
ohos/src/main/module.json5模块配置(纯 endorser 包不需要动它)
ohos/src/main/ets/components/plugin/ShareHandlerOhosPlugin.ets插件模板:生命周期接口 + 空 onMethodCall,2.4 节要补的就是它

第三处,example 的 ohos 宿主。进入 example/ 执行一次 flutter create --platforms ohos .,模板生成 example/ohos/ 宿主工程。这里有一个值得强调的点:宿主的 EntryAbility.ets 保持模板默认即可——很多"鸿蒙接收分享"的资料会指导你重写 EntryAbility 的 onCreate/onNewWant 去解析 want,那是对普通应用的做法;Flutter 插件有更优雅的通道,见 2.4 节的 AbilityAware。

2.4 在插件文件中补全 ohos 实现

先看改动全景。原则仍是"只做加法",上游各端与接口层一行不动:

文件改动
share_handler/pubspec.yaml修改:注册 ohos 平台的 default_package
share_handler_ohos/lib/share_handler_ohos.dart新增:Dart 平台实现 ShareHandlerOhosPlatform
share_handler_ohos/ohos/src/main/ets/components/plugin/ShareHandlerOhosPlugin.ets新增:ArkTS 原生实现(want 解析、ShareKit 读取、UTD 分类、持久化)
example/ohos/entry/src/main/module.json5修改:ability 声明 ohos.want.action.sendData 分享接收 skills
example/ohos/entry/src/main/ets/entryability/EntryAbility.ets一行不动(保持模板)

Dart 端:为什么不用上游的 pigeon 通道。 Android/iOS 端的 Dart 实现由 pigeon 生成,走的是 pigeon 自定义的消息编解码器;鸿蒙端引擎没有这套编解码器的实现,强行对通道名只会收到解码错误。正确做法是开一条专属 MethodChannel,用引擎默认的 StandardMessageCodec 传 Map,拿到后在 Dart 侧调用接口层的 SharedMedia.decode 还原成强类型模型。事件流沿用上游的 EventChannel 通道名,消费方无感知:

/// 专属 MethodChannel:StandardMessageCodec 传 Map,不用上游 pigeon 的自定义编解码器
const MethodChannel shareHandlerOhosMethodChannel =
    MethodChannel('com.shoutsocial.share_handler/ohos');

/// 事件流通道名与上游实现保持一致,消费方代码不变
const EventChannel shareHandlerOhosEventChannel =
    EventChannel('com.shoutsocial.share_handler/sharedMediaStream');

class ShareHandlerOhosPlatform extends ShareHandlerPlatform {
  
  Future<SharedMedia?> getInitialSharedMedia() async {
    final Object? result =
        await shareHandlerOhosMethodChannel.invokeMethod<Object?>(
            'getInitialSharedMedia');
    if (result == null) return null;
    return SharedMedia.decode(result as Map<Object?, Object?>);   // Map → 强类型模型
  }

  
  Future<void> recordSentMessage({
    required String conversationIdentifier,
    required String conversationName,
    String? conversationImageFilePath,
    String? serviceName,
  }) {
    return shareHandlerOhosMethodChannel.invokeMethod<void>(
      'recordSentMessage',
      <String, Object?>{ /* 四个参数逐个放入 */ },
    );
  }

  
  Future<void> resetInitialSharedMedia() { /* invokeMethod 即可 */ }

  
  Stream<SharedMedia> get sharedMediaStream {
    _sharedMediaStream ??=
        shareHandlerOhosEventChannel.receiveBroadcastStream().map<SharedMedia>(
      (dynamic event) => SharedMedia.decode(event as Map<dynamic, dynamic>),
    );
    return _sharedMediaStream!;
  }
}

ArkTS 端:AbilityAware 让宿主零改造。 flutter_ohos 框架为插件提供了 AbilityAware 生命周期:插件实现该接口后,引擎会注入 AbilityPluginBinding,通过 addOnNewWantListener 就能收到热启动的 want 回调,冷启动的 launchWant 则从 ability 上直接取。这就把"解析分享 want"完整地收进了插件内部:

export default class ShareHandlerOhosPlugin
    implements FlutterPlugin, MethodCallHandler, AbilityAware, StreamHandler {

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.abilityBinding = binding;
    // 热启动路径:应用已在前台,系统再次投递分享
    const listener: NewWantListener = {
      onNewWant: (want: Want, launchParams: AbilityConstant.LaunchParam): void => {
        this.handleWant(want, false);
      }
    };
    binding.addOnNewWantListener(listener);

    // 冷启动路径:应用从分享面板被拉起,onCreate 的 want 里带着分享内容
    const ability = binding.getAbility() as FlutterAbility;
    const launchWant: Want = ability.getWant();
    if (launchWant != null) {
      this.handleWant(launchWant, true);
    }
  }

  private handleWant(want: Want, initial: boolean): void {
    const action: string = want.action ?? '';
    const uri: string = want.uri ?? '';
    // 普通启动不带 sendData action 也没有 uri,直接跳过
    if (action !== SEND_ACTION && uri.length === 0) {
      return;
    }
    this.processSharedWant(want, initial);
  }
}

ShareKit 读取分享内容。 processSharedWant 是整个插件的核心:通过 systemShare.getSharedData(want) 拿到 SharedData,遍历 SharedRecord,按记录的 UTD 类型分流——文本类(general.text/general.hyperlink/general.plain-text)取 content 拼成分享文本,其余带 uri 的记录当附件处理;分享来源的 contactId 映射为 conversationIdentifier:

private async processSharedWant(want: Want, initial: boolean): Promise<void> {
  const sharedData: systemShare.SharedData = await systemShare.getSharedData(want);
  const records: systemShare.SharedRecord[] = sharedData.getRecords();

  let content: string | null = null;
  const attachments: Array<Record<string, Object>> = [];
  for (const record of records) {
    const utd: string = record.utd ?? '';
    if (utd === UTD_TEXT || utd === UTD_HYPERLINK || utd === UTD_PLAIN_TEXT) {
      if (content == null) {
        content = record.content ?? null;   // 文本类:取内容
      }
      continue;
    }
    const recordUri: string | null = record.uri ?? null;
    if (recordUri != null && recordUri.length > 0) {
      let type: number = TYPE_FILE;         // 默认按文件处理
      if (utdBelongsTo(utd, UTD_IMAGE)) {
        type = TYPE_IMAGE;                  // 0,与 SharedAttachmentType 枚举序号一致
      } else if (utdBelongsTo(utd, UTD_VIDEO)) {
        type = TYPE_VIDEO;
      } else if (utdBelongsTo(utd, UTD_AUDIO)) {
        type = TYPE_AUDIO;
      }
      const path: string = this.copyAttachmentToCache(recordUri, utd);
      attachments.push({ 'path': path, 'type': type } as Record<string, Object>);
    }
  }

  try {
    const contactInfo: systemShare.ContactInfo = await systemShare.getContactInfo(want);
    const contactId: string = contactInfo.contactId ?? '';
    if (contactId.length > 0) {
      conversationIdentifier = contactId;
    }
  } catch (e) { /* contact info 可选,失败忽略 */ }

  const mediaMap: Record<string, Object> = {};
  mediaMap['attachments'] = attachments;
  // ArkTS 下逐键构建,无值键直接省略;Dart 侧 decode 缺键为 null,
  // 与上游 Android 端 putNull() 的行为对齐
  if (conversationIdentifier != null) {
    mediaMap['conversationIdentifier'] = conversationIdentifier;
  }
  if (content != null) {
    mediaMap['content'] = content;
  }

  this.initialMedia = mediaMap;             // 冷启动:存起来等 getInitialSharedMedia 来取
  if (this.eventSink != null) {
    this.eventSink.success(mediaMap);       // 有监听者时同步广播(含冷启动这次),对齐 Android
  }
}

附件落盘采用"只读授权打开后拷贝":鸿蒙分享面板给的是只读的文件 URI,插件把文件拷到应用自己的缓存目录 cacheDir/share_handler/,Dart 侧拿到的就是这个稳定路径。文件名取源文件名,源文件没有扩展名时按 UTD 推断补全(图片 .jpg、视频 .mp4、音频 .mp3,兜底 .dat)。

recordSentMessage:持久化到 Preferences。 Android 端这个接口创建桌面分享快捷方式,iOS 端写入系统分享建议;鸿蒙没有与两者等价的公开 API,所以鸿蒙侧语义是"持久化到应用 Preferences"(share_handler_preferences),四个参数原样写入 dataPreferences,供宿主后续读取(做自己的分享建议、桌面卡片等)。接口签名与返回值与其他平台完全一致,业务代码不需要平台分支。

UTD 类型判断:字符串匹配不生效的坑。 这是本次适配最隐蔽的问题。第一版实现用字符串比较把 record.utd 映射到附件类型,编译运行都正常,唯独从图库分享一张截图时,附件类型永远是 file 而不是 image。用 hilog 抓系统分享框架(tshare)的日志,真相是图库投递的 UTD 是 general.jpeg——它是 general.image 的子类型,但字符串上根本不是 general.image 的前缀,精确匹配与 startsWith 全部落空:

A07DFE/tshare:SystemShareModalAbility/HuaweiShare: [Thumbnails] fromUri, utd: general.jpeg, mainExt: jpg, subExt: null

UTD 是一棵层级树,分享方投递的是最具体的叶子类型(general.jpeg、general.png、general.mp4……),接收方要判断"是否属于某一类"必须沿层级向上查。API 26 SDK 的 @ohos.data.uniformTypeDescriptor 提供两个关键能力:getTypeDescriptor(typeId) 拿到 TypeDescriptor,其 belongingToTypes 属性给出直接父类型数组。于是判断逻辑写成沿 belongingToTypes 递归向上遍历:

/**
 * 判断 utdId 是否属于 category。字符串前缀匹配在这里不成立:
 * 子类型的 id 并不以父类型 id 开头(general.jpeg 之于 general.image)。
 */
function utdMatchesCategory(utdId: string, category: string, depth: number): boolean {
  if (utdId === category) {
    return true;
  }
  if (utdId.length === 0 || depth > 8) {
    return false;
  }
  try {
    const descriptor: uniformTypeDescriptor.TypeDescriptor =
      uniformTypeDescriptor.getTypeDescriptor(utdId);
    const parents: Array<string> = descriptor.belongingToTypes;
    for (const parent of parents) {
      if (utdMatchesCategory(parent, category, depth + 1)) {
        return true;
      }
    }
  } catch (e) {
    // 未知 UTD id,按不属于处理
  }
  return false;
}

两个实现细节:uniformTypeDescriptor 模块在这个 SDK 上是默认导出(import uniformTypeDescriptor from '@ohos.data.uniformTypeDescriptor'),按文档里的命名导入写会直接编译失败;标准文档里的 isBelongTo/getUniformDataType 是更高 API 版本的产物,API 26 SDK 的 d.ts 里没有,只能用 getTypeDescriptor + belongingToTypes 自己组合。这个函数同时服务两处:附件类型映射与无扩展名文件的扩展名推断,改一处全生效。

2.5 宿主声明分享接收 skills(含一个 schema 坑)

插件解决"怎么收",宿主还要解决"分享面板里凭什么出现我"。在 example/ohos/entry/src/main/module.json5 的 ability 里追加第二条 skills,对齐上游 Android 端 ACTION_SEND/ACTION_SEND_MULTIPLE 的 intent-filter 语义。这里踩到一个 hvigor 的坑:module.json5 的 schema 校验要求含 utd 的 uri 条目必须同时带 scheme,否则构建直接失败,报 Schema validate failed(错误码 00303038)。所以每条都要带 scheme,穷举本应用支持的数据类型:

{
  // 分享面板按 utd 匹配目标应用,需穷举支持的数据类型
  "actions": [
    "ohos.want.action.sendData"
  ],
  "uris": [
    { "scheme": "file", "utd": "general.plain-text", "maxFileSupported": 100 },
    { "scheme": "file", "utd": "general.text", "maxFileSupported": 100 },
    { "scheme": "file", "utd": "general.hyperlink", "maxFileSupported": 100 },
    { "scheme": "file", "utd": "general.image", "maxFileSupported": 100 },
    { "scheme": "file", "utd": "general.video", "maxFileSupported": 100 },
    { "scheme": "file", "utd": "general.audio", "maxFileSupported": 100 },
    { "scheme": "file", "utd": "general.file", "maxFileSupported": 100 }
  ]
}

maxFileSupported 对齐上游 Android 端支持多文件分享的能力(100 个)。

2.6 补全适配说明文件并提交分支

四份适配说明文件各自的作用:

文件作用
README.OpenSource开源软件申报信息:名称、协议、版本、上游地址
README.OpenHarmony_CN.md中文说明:简介、下载安装、兼容性、接口说明、遗留问题
README.OpenHarmony.md英文说明,内容与中文版对应
CHANGELOG.OpenHarmony.md鸿蒙适配版本变更记录,TAG 之间变更的唯一事实来源

CHANGELOG.OpenHarmony.md 的实际内容:

## [0.0.25-ohos-1.0.0-beta.1]

- 适配 OpenHarmony 平台:新增 `share_handler_ohos` endorser 包(ArkTS 原生实现 + Dart 平台实现),主包注册 `ohos` 平台 default_package。
- 接收系统分享:文本 / 图片 / 视频 / 音频 / 文件,基于 `@kit.ShareKit`(`systemShare.getSharedData`),附件拷贝至应用缓存目录 `cacheDir/share_handler/`。
- `conversationIdentifier` 来自 `getContactInfo(want).contactId`;`getInitialSharedMedia` 读取即清(与 Android 端一致),`sharedMediaStream` 复用上游 EventChannel。
- `recordSentMessage` / `resetInitialSharedMedia`:持久化到应用 Preferences(`share_handler_preferences`)。
- example 新增 `ohos` 平台工程,entry 声明 `ohos.want.action.sendData` 分享接收 skills。
- 依赖 ohos 版 Flutter SDK `3.41.10-ohos-1.0.1`(Dart 3.11.5)。
- 验证环境:HarmonyOS 模拟器 `7.0.0.105`(DevEco Studio)。

提交时注意三件事:第一,构建过程中在 example/ohos/build-profile.json5 里生成过的签名配置要还原成空的 signingConfigs: [](签名材料属本机隐私,不入库);第二,flutter build hap 会把 entry/src/main/resources/base/profile/buildinfo.json5 迁移到 entry/src/main/resources/rawfile/buildinfo.json5,这是 ohos 工具链的固定行为,git 会识别成 rename,直接接受即可;第三,联邦插件的 git 依赖生效依赖 path 层级与仓库结构一致(主包在 share_handler/ 子目录、endorser 包在根下),不要挪动包目录。

git add -A
git commit -m "feat: adapt share_handler for the OpenHarmony platform"
git push atomgit feat/ohos_share_handler_0.0.25

# TAG 命名规则:原库版本-ohos-适配版本-beta.x(首个适配版 x=1)
git tag 0.0.25-ohos-1.0.0-beta.1
git push atomgit 0.0.25-ohos-1.0.0-beta.1

推送完成后在 AtomGit 仓库页面切换到分支与标签视图,能看到适配分支和 TAG:

在这里插入图片描述
在这里插入图片描述

图二:AtomGit 仓库页面的分支与 TAG

三、在 Demo 中验证适配效果

3.1 使用仓库自带的 example

优先改造仓库自带的 example/(path 依赖本地插件)。上游 example 本身就是一个接收分享的演示页:显示分享文本、来源标识与附件列表,图片附件带预览和 recordSentMessage 按钮——不需要新写页面,ohos 化之后正好覆盖四个接口里的三个。包名 com.shoutsocial.share_handler_example。

构建、安装、启动的完整命令:

cd example
flutter pub get
flutter build hap --debug

# 安装并启动(先用 hdc list targets 确认设备在线)
hdc install -r build/ohos/hap/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.shoutsocial.share_handler_example

如果首次构建只产出 intermediates 没有 HAP,是签名未配置:用 DevEco Studio 打开 example/ohos,在 File > Project Structure > Signing Configs 勾选 Automatically generate signature,之后重新 flutter build hap 即可。

3.2 冷启动接收:从图库分享一张截图

验证分享接收的标准动作:模拟器打开图库 → 选中一张截图 → 点分享 → 分享面板里应该能看到 example(skills 声明生效)→ 选中 example → 应用被拉起。

在这里插入图片描述

图三:example 冷启动后的初始界面(尚未收到分享)

分享拉起后,example 显示接收到的内容:来源标识、分享文本、附件列表。图片附件正确渲染出预览,并且出现了 Record message 按钮(example 的逻辑是只对 SharedAttachmentType.image 渲染这两个控件):

在这里插入图片描述

图四:冷启动接收图库分享的截图,附件路径落在应用缓存目录

这张图同时验证了 2.4 节的 UTD 层级修复:图库投递的 UTD 是 general.jpeg,插件沿 belongingToTypes 归属到 general.image,类型映射成 image(枚举 0),Dart 侧才走到了图片渲染分支。修复前同一操作附件类型会被误判为 file,页面上只有一行路径文本。

在这里插入图片描述

图五:附件类型正确识别为 image,Record message 按钮与图片预览出现

3.3 recordSentMessage:验证持久化

点击 Record message 按钮,example 以图片路径为头像调用 recordSentMessage(conversationIdentifier/Name/serviceName 用示例值)。验证方法很直接——持久化文件立即出现在应用沙箱里,拉出来核对四个键的值与调用参数一致:

hdc file recv /data/app/el2/100/base/com.shoutsocial.share_handler_example/preferences/share_handler_preferences /tmp/prefs && cat /tmp/prefs
<?xml version="1.0" encoding="UTF-8"?>
<preferences version="1.0"><string key="conversationImageFilePath">/data/storage/el2/base/cache/share_handler/screenshot_20260914_220222_com.huawei.hmos.photos.jpg</string><string key="conversationIdentifier">custom-conversation-identifier</string><string key="serviceName">custom-service-name</string><string key="conversationName">John Doe</string></preferences>

在这里插入图片描述

图六:点击按钮调用 recordSentMessage,Preferences 四个键与调用参数一致

3.4 热启动推送:应用在前台再次分享

保持 example 在前台,回到图库再分享一次。这次应用不会被拉起(已经在运行),want 走 onNewWant 路径,内容通过 sharedMediaStream 事件流推给 Dart,UI 直接刷新为新内容。hilog 里能找到热启动的标志日志(isNewWant:1),证明确实走的是 onNewWant 而不是重新 onCreate:

09-14 22:47:38.325 ... C01332/...share_handler_example/UIAbility: name:EntryAbility,targeState:5,isNewWant:1

在这里插入图片描述

图七:应用在前台时再次收到分享,sharedMediaStream 推送并刷新界面

3.5 验证方法沉淀

分享链路横跨系统分享面板与插件两层,纯看应用日志经常定位不了问题,几个实测有效的手段:

看系统分享框架投递了什么。 hdc shell hilog > 文件 抓全量日志后操作一次分享,在日志里搜 tshare,能直接看到分享方投递的 UTD 与扩展名。判断类型识别问题时先看这里,确认系统给的原始值,再检查插件的映射逻辑——本次 general.jpeg 之谜就是靠它破案的(macOS 终端没有 timeout 命令,抓带时限的日志要用设备端 nohup hilog > /data/local/tmp/x.log 2>&1 & + 事后 pkill hilog)。

确认安装与进程确实是新的。 改完插件代码重新构建安装后,bm dump -n <bundleName> 的 updateTime(毫秒时间戳)与构建产物时间对得上才算覆盖安装成功;ps -ef | grep <bundleName> 看进程启动时间。排除"改了没生效"的怀疑时,先查这两处再怀疑代码。

区分冷启动与热启动。 hilog 里 UIAbility 的 isNewWant:1 标志是热启动(onNewWant 路径)的可靠证据;没有它就是冷启动走了 onCreate。验证 sharedMediaStream 时必须制造热启动场景(应用保持前台),否则验证的是 getInitialSharedMedia。

uitest 点击用设备像素坐标。 用 hdc shell uitest uiInput click x y 自动化点击时,坐标是设备分辨率坐标系(本文模拟器 1320×2232),不能直接拿模拟器截图(604×1050)里的像素位置用,要按比例换算,否则点击落空还不知道为什么。

四、常见问题

4.1 适配过程中的问题

Q1:flutter create --platforms ohos . 在联邦插件仓库里怎么用?

分清三个执行位置:endorser 包根(生成 share_handler_ohos/ohos/)、example 根(生成 example/ohos/)。不要在仓库根执行——仓库根不是包。另外端实现包是手写 pubspec.yaml + lib/ 之后再生成原生骨架,模板铺开的多余平台目录要对照 share_handler_android 的结构清掉。

Q2:为什么宿主的 EntryAbility 不用改?

flutter_ohos 框架的 AbilityAware/AbilityPluginBinding 机制:插件实现 AbilityAware 后由引擎注入 ability 绑定,addOnNewWantListener 收热启动回调、getAbility().getWant() 取冷启动 want。want 的生命周期事件在插件内部闭环,宿主保持模板。对比手动方案(宿主重写 onNewWant 再想办法传给插件),插件自持方案对使用方是零成本。

Q3:module.json5 里配置了 skills,构建报 Schema validate failed(00303038)?

hvigor 的 schema 校验要求 uris 数组里含 utd 字段的条目必须同时声明 scheme。每条写成 { "scheme": "file", "utd": "...", "maxFileSupported": N } 即可通过。这个约束文档里不明显,报错信息也不指向具体字段,容易卡住。

Q4:分享图片总是被识别成 file 类型?

UTD 是层级树,分享方投递的是具体叶子类型(图库截图是 general.jpeg),字符串上不是 general.image 的前缀也不是它的精确匹配。必须用 uniformTypeDescriptor.getTypeDescriptor(utd).belongingToTypes 沿层级向上递归判断归属(2.4 节完整实现)。判断不了时先抓 tshare 日志看系统投递的原始 UTD 值。

Q5:API 26 SDK 上照文档写 uniformTypeDescriptor 编译报错?

两处版本差异:模块是默认导出,import uniformTypeDescriptor from '@ohos.data.uniformTypeDescriptor'(命名导入编译失败);文档里的 getUniformDataType/isBelongTo 等 API 在该 SDK 的 d.ts 里不存在,用 getTypeDescriptor() + TypeDescriptor.belongingToTypes 组合实现等价逻辑。以本地 SDK 的 d.ts 为准,不要照抄在线文档签名。

Q6:签名配置要不要提交?

不要。example/ohos/build-profile.json5 的 signingConfigs 含证书路径与本机加密口令,提交前还原为空数组。构建时通过 DevEco 自动签名或本地临时注入解决。

4.2 使用过程中的问题

Q1:分享面板里看不到我的应用?

检查宿主 module.json5 的 ability 是否声明了 ohos.want.action.sendData skills,且 uris 覆盖了分享内容对应的 UTD 类别(只配 image 就只收得到图片)。改完配置要全量重新构建安装,热重载不更新 module 清单。

Q2:附件路径能用多久?

附件被拷到应用缓存目录 cacheDir/share_handler/,应用卸载或清理缓存后消失。业务上需要在会话间长期引用的图片,应在拿到路径后自行转存到应用沙箱的持久目录。

Q3:recordSentMessage 之后桌面/分享建议里怎么没变化?

鸿蒙侧该接口语义是持久化到应用 Preferences,不会创建 Android 意义上的分享快捷方式(鸿蒙无等价公开 API)。数据已可供宿主读取,做自定义的分享建议或卡片时从 share_handler_preferences 取。

Q4:getInitialSharedMedia 第二次调用返回 null?

读取即清空是该接口的既定语义(与 Android 端一致),冷启动数据只消费一次。需要持续监听就订阅 sharedMediaStream,运行中的每次分享都会推送。

五、结语

本次适配把 share_handler 的四个接口完整带到了 OpenHarmony:share_handler_ohos endorser 包基于 @kit.ShareKit 接收系统分享,AbilityAware 机制让宿主零改造,冷启动与热启动双路径实测可用,UTD 层级归属判断解决了具体子类型(general.jpeg)的识别问题。仓库托管在 oh-flutter/share_handler,适配层问题请到鸿蒙仓库 Issue 反馈,原库行为问题请到上游 Issue 反馈。接入方式、宿主配置与接口用法,见姊妹篇《share_handler 的鸿蒙使用指南》。

六、相关链接

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐