第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 名词,读者也能沿着真实工程把功能跑通。
