第70篇 | HarmonyOS nearbyShareCallback:附近设备发起分享时应用做什么

第 70 篇继续 ShareKit,但重点从“注册能力”切到“设备真正触发分享时应用做什么”。在双镜记忆相机里,附近设备发起碰一碰或隔空抓取时,应用不能盲目把所有照片都发出去,而是要先判断当前页面有没有可分享记录,再把当前记录转换成系统可理解的分享数据。

nearbyShareCallback 是这条链路的入口。它的职责很克制:拿到当前可分享对象,没有内容就拒绝,有内容就构造 SharedData 并交给 sharableTarget.share。这篇会沿着回调向下追踪到记录选择、文件去重和 SharedRecord 构建,理解一个可靠近场分享回调应该包含哪些边界。

本篇目标

  • 理解 SharableTarget 回调里为什么要先判断可分享内容。
  • 掌握当前页面如何决定近场分享的目标记录。
  • 理解双镜照片为什么要去空路径和去重。
  • 把回调状态文案、拒绝原因和成功分享数量串起来。

对应源码位置

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

从相册当前记录发起近场分享

近场分享最怕“用户以为分享 A,结果发出去 B”。所以项目没有从全局列表随便取第一张,而是根据当前 Tab 和精选记录来决定目标。用户在相册详情页,分享的是当前相册记录;用户在保险箱并且已处于可见状态,才会进入私密记录的路径。

这张运行页面展示的是相册维度的分享场景。文章后面的代码会看到,真正回调触发时仍然会再检查一次数据是否为空。UI 上看似只是一个状态卡片,底层其实已经准备好“选择目标、构造数据、失败拒绝”的完整链路。

相册页面承接附近设备触发后的分享目标

相册页面承接附近设备触发后的分享目标

回调入口只做三件事

nearbyShareCallback 的结构非常清晰:先调用 getNearbyShareItems,如果数组为空就更新状态并 reject;如果有内容,就调用 buildSharedData 后交给系统分享目标;如果过程中发生异常,捕获错误并展示失败信息。

这种写法有两个优点。第一,回调不会散落复杂业务,只保留入口编排。第二,失败路径是显式的,附近设备不会一直等待一个没有内容的应用。实际项目里,近场分享体验是否顺滑,很大程度取决于这种回调是否能快速给出确定结果。

nearbyShareCallback 负责内容检查、分享调用和失败状态

nearbyShareCallback 负责内容检查、分享调用和失败状态

  private readonly nearbyShareCallback = async (sharableTarget: harmonyShare.SharableTarget): Promise<void> => {
    const shareItems = this.getNearbyShareItems();
    if (shareItems.length === 0) {
      this.nearbyShareStatusText = '还没有可分享的公开照片';
      await sharableTarget.reject(harmonyShare.SharableErrorCode.NO_CONTENT_ERROR);
      return;
    }

    try {
      await sharableTarget.share(this.buildSharedData(shareItems));
      this.nearbyShareStatusText = `\u5df2\u5206\u4eab ${shareItems.length} \u5f20\u7167\u7247`;
    } catch (error) {
      const message = error instanceof Error ? error.message : JSON.stringify(error);
      this.nearbyShareStatusText = `\u5206\u4eab\u5931\u8d25\uff1a${message}`;
    }
  };

分享目标来自当前可见上下文

getNearbyShareRecord 根据 activeTab 决定取哪个精选记录。相册页取 getFeaturedGalleryRecord,保险箱页取 getFeaturedVaultRecord,其他页面默认回到相册记录。这样近场分享不会依赖一个额外的全局变量,而是直接绑定用户当前正在看的内容。

这里要注意,能拿到保险箱记录并不等于任何时候都能分享私密内容。保险箱 UI 本身有解锁状态和详情打开限制,近场回调只是沿用当前可见上下文。下一篇隐私文章会继续把这条边界讲细。

getNearbyShareItems 通过当前 Tab 找到目标照片记录

getNearbyShareItems 通过当前 Tab 找到目标照片记录

  private getNearbyShareItems(): Array<LocalShareItem> {
    const shareRecord = this.getNearbyShareRecord();
    if (!shareRecord) {
      return [];
    }
    return this.buildRecordShareItems(shareRecord);
  }

  private getNearbyShareTargetText(): string {
    const shareRecord = this.getNearbyShareRecord();
    if (!shareRecord) {
      return '';
    }
    return `${shareRecord.place} / ${shareRecord.memoryTitle}`;
  }

  private getNearbyShareRecord(): GalleryMoment | undefined {
    if (this.activeTab === 'vault') {
      return this.getFeaturedVaultRecord();
    }
    if (this.activeTab === 'gallery') {
      return this.getFeaturedGalleryRecord();
    }
    return this.getFeaturedGalleryRecord();
  }

双镜照片要先变成文件列表

一条双镜记忆记录通常有后置镜头照片和前置镜头照片。buildRecordShareItems 会把这两个路径变成 LocalShareItem 候选项,并给每个文件带上描述。随后它会过滤空路径和重复路径,避免 SharedData 中出现无效文件或同一张图被加入两次。

这一步看起来很普通,但决定了分享内容的质量。如果用户只拍到了单镜头,前置或后置路径可能为空;如果某些兜底逻辑让两张图指向同一文件,也不能重复塞给系统分享。去重后再分享,接收端拿到的才是干净文件列表。

buildRecordShareItems 过滤空路径和重复照片

buildRecordShareItems 过滤空路径和重复照片

  private buildRecordShareItems(record: GalleryMoment): Array<LocalShareItem> {
    const candidates: Array<LocalShareItem> = [
      {
        sourcePath: record.backPath,
        title: '',
        description: `${record.createdLabel} / ${record.place}`,
        baseType: utd.UniformDataType.IMAGE
      },
      {
        sourcePath: record.frontPath,
        title: '',
        description: `${record.createdLabel} / ${record.memoryTitle}`,
        baseType: utd.UniformDataType.IMAGE
      }
    ];
    const shareItems: Array<LocalShareItem> = [];
    const seenPaths: Array<string> = [];
    for (const candidate of candidates) {
      if (candidate.sourcePath.length === 0 || seenPaths.includes(candidate.sourcePath)) {
        continue;
      }
      seenPaths.push(candidate.sourcePath);
      shareItems.push(candidate);
    }
    return shareItems;

最后交给系统的必须是 SharedData

buildSharedData 先用第一条记录创建 SharedData,再把后续文件逐条 addRecordbuildSharedRecord 则负责把本地路径转换为 fileUri,并根据文件扩展名得到更精确的 UTD 类型。图片还会补上 thumbnailUri,让系统分享面板和接收端能更好地预览。

这也是为什么回调中没有直接传路径数组。ShareKit 和系统分享都需要结构化数据,里面包含类型、URI、标题、描述和缩略图。训练营写实战文章时,一定要把这层数据转换讲出来,读者才知道 API 背后真正需要准备什么。

SharedData 把本地照片路径封装为系统可分享对象

SharedData 把本地照片路径封装为系统可分享对象

  private buildSharedData(items: Array<LocalShareItem>): systemShare.SharedData {
    if (items.length === 0) {
      throw new Error('');
    }
    const sharedData: systemShare.SharedData = new systemShare.SharedData(this.buildSharedRecord(items[0]));
    for (let index = 1; index < items.length; index++) {
      try {
        sharedData.addRecord(this.buildSharedRecord(items[index]));
      } catch (error) {
        const message = error instanceof Error ? error.message : JSON.stringify(error);
        throw new Error(`添加分享文件失败:${message}`);
      }
    }
    return sharedData;
  }

  private buildSharedRecord(item: LocalShareItem): systemShare.SharedRecord {
    const extension = this.getShareFileExtension(item.sourcePath, item.baseType);
    const preciseType = utd.getUniformDataTypeByFilenameExtension(extension, item.baseType);
    const sharedRecord: systemShare.SharedRecord = {
      utd: preciseType,
      uri: fileUri.getUriFromPath(item.sourcePath),
      title: item.title,
      description: item.description
    };
    if (item.baseType === utd.UniformDataType.IMAGE) {
      const thumbnailPath = item.thumbnailPath ?? item.sourcePath;
      sharedRecord.thumbnailUri = fileUri.getUriFromPath(thumbnailPath);
    }
    return sharedRecord;
  }

工程检查清单

  • 回调中没有内容时必须主动 reject
  • 分享目标要从当前页面上下文读取,而不是硬编码第一条记录。
  • 双镜照片需要过滤空路径和重复路径。
  • SharedRecord 需要包含 UTD、URI、描述和图片缩略图。

今日练习

  1. 删除一条记录的前置路径,观察 buildRecordShareItems 是否只返回一张图。
  2. activeTab 切到 gallery 和 vault,分别跟踪 getNearbyShareRecord 的返回值。
  3. 在失败分支里打印错误码,比较没有内容和分享异常时的状态文案。

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

Logo

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

更多推荐