Flutter 三方库 share_handler 的鸿蒙适配教程
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 版 SDK,因为后文自动补全 ohos 目录靠的是它提供的 flutter create --platforms ohos 能力。环境装好后用 flutter devices 确认能识别鸿蒙设备。本文实测使用的环境:
| 项 | 版本 |
|---|---|
| Flutter(ohos 版) | 3.41.10-ohos-1.0.1 |
| DevEco Studio | 26.0.0.821 |
| 编译 SDK | HarmonyOS 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 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:
- CPF-Flutter 鸿蒙社区
- Flutter OHOS 开发环境搭建指南
- 鸿蒙版仓库
- 本文 TAG(分支
feat/ohos_share_handler_0.0.25) - example 源码目录
- 上游仓库
- pub.dev 包页
更多推荐

所有评论(0)