第72篇 | HarmonyOS 分享降级:近场能力不可用时回到系统分享
第72篇 | HarmonyOS 分享降级:近场能力不可用时回到系统分享
第 72 篇讲分享降级。真实设备环境很复杂:有的设备支持碰一碰但不支持隔空抓取,有的系统防护能力未开启,有的分享面板可能被用户取消。一个训练营项目想写得像完整作品,不能只展示“成功路径”,还要让能力不可用时有可继续操作的出口。
双镜记忆相机的降级策略比较清晰:ShareKit 注册失败不会阻塞相册;近场分享没有内容时主动拒绝;系统分享作为通用出口保留在详情页和视频管理页;分享过程用 busy 状态防重复点击。这篇我们把这些兜底点串起来看。
本篇目标
- 理解 ShareKit 部分能力失败时为什么不应该阻断页面。
- 掌握系统分享面板作为通用降级出口的写法。
- 理解
systemShareBusy如何避免重复拉起分享。 - 学会把近场分享、一键成片和普通系统分享放进同一套兜底策略。
对应源码位置
superImage/entry/src/main/ets/pages/Index.ets
降级体验要让用户还有路可走
从运行效果看,用户并不会感知到底是 ShareKit 近场分享还是 SystemShare 面板;用户只需要知道照片能不能发出去。项目在详情页保留了系统分享按钮,在视频管理页也能把素材交给系统能力生成或分享,这些都是降级体验的一部分。
降级不是“失败后弹一句话”这么简单,而是要在功能设计阶段就提供第二条路径。近场能力不可用时,相册仍然可浏览;隔空抓取注册失败时,碰一碰可以继续;两者都不可用时,系统分享按钮仍然可以完成文件流转。

地图和相册流程之外仍然保留系统级分享出口
注册失败要区分可忽略和需要提示
在注册 ShareKit 能力时,项目把碰一碰和隔空抓取分开处理。隔空抓取注册失败后,如果碰一碰已经成功,就不更新错误文案;如果两个都失败,再根据错误码决定是否提示。错误码 801 这类“不支持”场景会安静处理,避免页面反复出现无意义提示。
这类处理对真实设备非常重要。训练营文章写到这里时,可以提醒读者:不是所有错误都应该以弹窗形式展示。设备不支持某个增强能力时,保持主流程可用,才是面向用户的降级。

注册 knockShare 和 gesturesShare 时区分部分成功和全部失败
private async registerNearbyShareListeners(): Promise<void> {
let ready = this.knockShareRegistered || this.gesturesShareRegistered;
if (!this.knockShareRegistered) {
try {
harmonyShare.on('knockShare', this.nearbyShareCallback);
this.knockShareRegistered = true;
ready = true;
} catch (error) {
}
}
if (!this.gesturesShareRegistered) {
try {
const registry = await this.createSendCapabilityRegistry();
harmonyShare.on('gesturesShare', registry, this.nearbyShareCallback);
this.gesturesShareRegistry = registry;
this.gesturesShareRegistered = true;
ready = true;
} catch (error) {
if (!this.knockShareRegistered) {
const err = error as BusinessError;
this.nearbyShareStatusText = err.code === 801
? ''
: `附近分享初始化失败:${err.message ?? err.code ?? -1}`;
}
}
}
this.nearbyShareReady = ready;
if (ready) {
this.nearbyShareStatusText = '';
}
}
系统分享面板是最稳的通用出口
系统分享面板不依赖附近设备和手势能力,只要文件路径和媒体类型准备正确,就可以作为普通分享出口。项目里 showSystemSharePanel 把 SharedData 交给 ShareController,并设置单选和详情预览模式。
当 ShareKit 能力不可用时,用户仍然可以通过这个入口把照片发到其他应用、保存到系统能力或继续进入后续流程。对课程文章来说,这一节应该强调“降级不是另写一套数据”,而是复用 buildSharedData。

系统分享面板复用 SharedData 构建结果
private async showSystemSharePanel(items: Array<LocalShareItem>): Promise<void> {
if (items.length === 0) {
throw new Error('');
}
try {
const sharedData = this.buildSharedData(items);
const controller: systemShare.ShareController = new systemShare.ShareController(sharedData);
await controller.show(this.getAbilityContext(), {
selectionMode: systemShare.SelectionMode.SINGLE,
previewMode: systemShare.SharePreviewMode.DETAIL
});
} catch (error) {
const err = error as BusinessError;
throw new Error(`拉起系统分享面板失败:${err.message ?? err.code ?? 'unknown'}`);
}
}
普通照片分享也要防重复点击
shareRecordWithSystemShare 的第一行就判断 systemShareBusy。这不是小细节,系统分享面板是异步拉起的,如果用户连续点击,很容易出现多个面板、状态错乱或二次失败。busy 状态让每次分享都有明确开始和结束。
函数还处理了空文件列表、分享中状态、成功清空状态和失败文案。这样即使系统面板失败,页面也能恢复按钮可点状态。降级链路最怕失败后卡死,finally 里恢复 busy 就是兜底闭环。

shareRecordWithSystemShare 用 busy 状态保护系统分享流程
private async shareRecordWithSystemShare(
record: GalleryMoment,
scope: 'gallery' | 'vault'
): Promise<void> {
if (this.systemShareBusy) {
return;
}
const shareItems = this.buildRecordShareItems(record);
if (shareItems.length === 0) {
this.updateRecordExportStatus(scope, '');
return;
}
this.systemShareBusy = true;
this.updateRecordExportStatus(scope, `正在分享 ${shareItems.length} 个文件...`);
try {
await this.showSystemSharePanel(shareItems);
this.updateRecordExportStatus(scope, '');
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
this.updateRecordExportStatus(scope, `系统分享失败:${message}`);
} finally {
this.systemShareBusy = false;
}
}
一键成片也复用分享兜底
视频管理页的 openHarmonyOneClickMovieForSelection 先检查素材数量,再把选中的照片转为分享项,最后调用同一个 showSystemSharePanel。这说明系统分享面板不仅是普通照片出口,也能承接“把照片交给系统能力继续处理”的场景。
这里的状态闭环同样完整:素材不足直接返回,分享中展示数量,成功后追加视频管理记录并跳转预览,失败时展示系统分享失败。一个高质量实战项目应该像这样,把降级出口和业务后续动作都写清楚。

一键成片入口同样复用 showSystemSharePanel
private async openHarmonyOneClickMovieForSelection(): Promise<void> {
if (this.systemShareBusy) {
return;
}
const sourceRecords = this.getSelectedVideoRecords();
if (sourceRecords.length < 2) {
this.harmonyMovieStatusText = '';
return;
}
const shareItems = this.buildVideoPhotoShareItems(sourceRecords);
if (shareItems.length < 2) {
this.harmonyMovieStatusText = '';
return;
}
this.systemShareBusy = true;
this.harmonyMovieStatusText = `正在分享 ${shareItems.length} 个文件...`;
try {
await this.showSystemSharePanel(shareItems);
this.harmonyMovieStatusText = '';
await this.appendSystemVideoManagerRecord(sourceRecords);
this.galleryMediaTab = 'video';
const latestVideoRecord = this.getVideoManagerRecordsForRender()[0];
if (latestVideoRecord && latestVideoRecord.mode === 'normal' && this.canPreviewVideoManagerRecord(latestVideoRecord)) {
this.selectedVideoManagerRecordId = latestVideoRecord.id;
this.videoPreviewFrameIndex = 0;
this.galleryViewMode = 'videoPreview';
} else {
this.galleryViewMode = 'album';
}
} catch (error) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
this.harmonyMovieStatusText = `系统分享失败:${message}`;
} finally {
this.systemShareBusy = false;
}
}
工程检查清单
- 部分能力注册失败时不要阻塞页面主流程。
- 系统分享面板要复用同一套 SharedData 构建逻辑。
- 分享按钮必须有 busy 状态防重复点击。
- 失败后通过
finally恢复状态,不能让按钮永久不可用。
今日练习
- 模拟
gesturesShare抛出 801,检查页面是否保持安静。 - 在系统分享失败分支里输出错误信息,观察状态是否恢复。
- 把视频素材数量改成 1,确认一键成片不会拉起分享面板。
训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。
更多推荐


所有评论(0)