第72篇 | HarmonyOS 分享降级:近场能力不可用时回到系统分享

第 72 篇讲分享降级。真实设备环境很复杂:有的设备支持碰一碰但不支持隔空抓取,有的系统防护能力未开启,有的分享面板可能被用户取消。一个训练营项目想写得像完整作品,不能只展示“成功路径”,还要让能力不可用时有可继续操作的出口。

双镜记忆相机的降级策略比较清晰:ShareKit 注册失败不会阻塞相册;近场分享没有内容时主动拒绝;系统分享作为通用出口保留在详情页和视频管理页;分享过程用 busy 状态防重复点击。这篇我们把这些兜底点串起来看。

本篇目标

  • 理解 ShareKit 部分能力失败时为什么不应该阻断页面。
  • 掌握系统分享面板作为通用降级出口的写法。
  • 理解 systemShareBusy 如何避免重复拉起分享。
  • 学会把近场分享、一键成片和普通系统分享放进同一套兜底策略。

对应源码位置

  • superImage/entry/src/main/ets/pages/Index.ets

降级体验要让用户还有路可走

从运行效果看,用户并不会感知到底是 ShareKit 近场分享还是 SystemShare 面板;用户只需要知道照片能不能发出去。项目在详情页保留了系统分享按钮,在视频管理页也能把素材交给系统能力生成或分享,这些都是降级体验的一部分。

降级不是“失败后弹一句话”这么简单,而是要在功能设计阶段就提供第二条路径。近场能力不可用时,相册仍然可浏览;隔空抓取注册失败时,碰一碰可以继续;两者都不可用时,系统分享按钮仍然可以完成文件流转。

地图和相册流程之外仍然保留系统级分享出口

地图和相册流程之外仍然保留系统级分享出口

注册失败要区分可忽略和需要提示

在注册 ShareKit 能力时,项目把碰一碰和隔空抓取分开处理。隔空抓取注册失败后,如果碰一碰已经成功,就不更新错误文案;如果两个都失败,再根据错误码决定是否提示。错误码 801 这类“不支持”场景会安静处理,避免页面反复出现无意义提示。

这类处理对真实设备非常重要。训练营文章写到这里时,可以提醒读者:不是所有错误都应该以弹窗形式展示。设备不支持某个增强能力时,保持主流程可用,才是面向用户的降级。

注册 knockShare 和 gesturesShare 时区分部分成功和全部失败

注册 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 构建结果

系统分享面板复用 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 状态保护系统分享流程

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

一键成片入口同样复用 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 恢复状态,不能让按钮永久不可用。

今日练习

  1. 模拟 gesturesShare 抛出 801,检查页面是否保持安静。
  2. 在系统分享失败分支里输出错误信息,观察状态是否恢复。
  3. 把视频素材数量改成 1,确认一键成片不会拉起分享面板。

训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。

Logo

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

更多推荐