【寻迹校园 HarmonyOS NEXT 实战 11】HarmonyOS Photo Picker 实战:不申请相册整库权限也能选图

这是“寻迹校园 HarmonyOS NEXT 实战”系列第 11 篇。本文结合项目中的 PhotoPickerService.etsPublishFormPage.ets,拆解如何通过系统 Photo Picker 完成“最多选择 3 张失物照片”,并把取消选择、平台异常和权限边界分别处理。

Photo Picker 最小权限原创封面图

上图为本文原创生成的技术插画,不是应用截图。核心关系是:应用只接收用户主动选择的图片 URI,不把“发布一条失物信息”扩大成“读取整个相册”。

一、失物发布为什么不需要整库相册权限

校园失物招领的图片需求很窄:用户在发布丢失或拾得信息时,主动挑选少量照片作为辅助证据。页面不需要扫描全部照片、不需要建立相册索引,也不需要在后台持续读取媒体库。

如果为了这个入口申请更宽的相册访问能力,会同时带来三个问题:

  • 权限目的与实际功能不匹配,用户难以理解为什么发布物品需要查看全部照片;
  • 拒绝权限后,原本可以继续填写文字的核心路径可能被错误阻断;
  • 页面层不得不处理权限申请、拒绝、再次申请和系统设置跳转,状态复杂度明显上升。

项目选择系统 Photo Picker:用户在系统界面中明确选择哪些图片,应用只拿到本次选择结果。这符合“功能需要多少,就访问多少”的最小化思路。

二、把系统能力封装在 PhotoPickerService

页面不应该直接 import MediaLibrary Kit 后拼装所有参数。项目把选图能力收敛到 PhotoPickerService,对页面暴露一个稳定方法:

async selectImages(maxSelectNumber: number = 3): Promise<OperationResult<string[]>>

返回值不是裸数组,而是统一的 OperationResult<string[]>。页面可以同时获得:

  • success:本次操作是否正常结束;
  • userMessage:适合展示给用户的说明;
  • data:成功时的 URI 数组;
  • ErrorCategory:平台异常时的稳定错误分类。

这样,ArkUI 页面只消费业务语义,不直接理解系统异常对象。

三、PhotoSelectOptions 如何限制能力范围

项目中的核心配置如下:

const options: photoAccessHelper.PhotoSelectOptions =
  new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = Math.max(1, Math.min(maxSelectNumber, 3));
options.isPhotoTakingSupported = false;
options.isSearchSupported = true;

这些参数分别表达清晰的产品约束:

  1. 只接收图片,不把视频混入失物发布;
  2. 调用方即使误传 20,最终也会被压到最多 3 张;
  3. 当前入口关闭拍照能力,避免同时扩展到相机权限与拍摄生命周期;
  4. 保留系统搜索,方便用户从较多图片中定位目标。

Math.max(1, Math.min(maxSelectNumber, 3)) 还承担防御性边界:选图上限不完全信任页面参数。即使未来出现第二个调用入口,Service 仍然守住 1—3 张的契约。

四、系统选择结果还要再次截断

系统返回 PhotoSelectResult 后,项目仍然执行一次 slice(0, 3)

const result: photoAccessHelper.PhotoSelectResult = await picker.select(options);
const selected: string[] = result.photoUris.slice(0, 3);

配置限制和结果限制不是重复劳动。前者约束系统交互,后者保护应用内部数据。外部能力、测试替身或未来平台行为发生变化时,业务层依然只接收最多 3 个 URI。

同一条上限还应出现在后续草稿序列化、图片物化和正式记录写入中。多层都使用同一个数字边界,才能避免“页面显示 3 张,数据库却存了 4 张”的不一致。

五、用户取消不是异常

选图器返回空数组时,项目这样处理:

if (selected.length === 0) {
  return new OperationResult<string[]>(true, '未选择照片,可继续填写', []);
}

这是一个重要的交互判断。用户可能只是临时查看、没有合适照片、误触入口,或者决定只发文字。取消不代表系统失败,也不应出现红色错误提示。

PublishFormPage 还需要守住原有选择:只有返回非空数组时才替换当前 imageUris。否则,用户已经选好的图片可能因为第二次打开后取消而被意外清空。

正确的取消体验是:

  • 返回原表单;
  • 已填写文本和已选图片保持不变;
  • 显示“未选择照片,可继续填写”这类中性说明;
  • 发布按钮继续由完整表单规则决定,而不是强制要求图片。

Photo Picker 选择、取消与异常原创流程图

上图把三条结果路径分开:选择 1—3 张进入 URI 列表,取消回到表单继续填写,平台异常进入可重试错误态。它们不能共用一个“选图失败”提示。

六、平台异常要映射,不能把原始错误直接抛给页面

系统选择器无法拉起时,Service 捕获异常并返回稳定分类:

catch (error) {
  return new OperationResult<string[]>(false,
    '系统图片选择暂不可用,请稍后重试',
    undefined,
    ErrorCategory.PLATFORM);
}

页面不需要展示堆栈、系统路径或底层错误文本。用户只需要知道当前能力暂不可用,并且文字发布仍然可以继续。

开发日志若要记录,也应只保留受控错误分类和必要上下文,不能输出用户图片 URI 或其他隐私数据。对当前项目而言,图片是可选项,平台异常不应让整张表单进入不可恢复状态。

七、页面层只负责交互草稿

PublishFormPage 持有的是短生命周期状态:当前步骤、图片 URI、字段错误、loading 和页面提示。它不负责:

  • 申请整库媒体权限;
  • 判断 URI 能否跨重启长期使用;
  • 把图片复制到应用目录;
  • 直接更新 RelationalStore;
  • 在失败时遍历并删除文件。

这些职责分别属于系统 Picker、Service 和 Repository。页面只做四件事:发起选择、显示结果、允许移除单张、在构造 ReportDraft 时携带当前 URI。

这种分层让选图 UI 可以变化,而图片生命周期和持久化规则不被拖进 ArkUI 组件。

八、权限拒绝、取消和系统错误要分开建模

很多页面把所有失败都收敛成一个布尔值,最后只能显示“操作失败”。更可维护的状态至少应区分:

状态 是否业务错误 页面行为 用户下一步
选择 1—3 张 展示缩略项 继续填写或发布
取消/0 张 保留原草稿 可重新选择
平台能力异常 显示页面级提示 稍后重试或无图发布
图片后续复制失败 保留输入并退出 loading 重试保存

Photo Picker 方案减少了宽权限申请,但不等于“图片链路没有错误”。临时 URI 读取、文件复制、磁盘空间和冷启动恢复仍是下一层必须验证的问题。

九、测试要覆盖边界值,而不只点一次按钮

建议至少覆盖以下用例:

  1. maxSelectNumber 传入 0、1、3、20,最终范围保持 1—3;
  2. 选择 1 张与 3 张,页面顺序和移除操作正确;
  3. 打开后取消,原表单文本和原图片不丢失;
  4. 返回空 URI 数组时使用成功语义,不显示异常;
  5. 系统选择器抛错时返回 ErrorCategory.PLATFORM
  6. 连续点击时不会叠加多个选择器或覆盖 loading;
  7. 大字体、小屏和底部安全区下,3 个图片槽位仍可操作;
  8. 图片缺失或无法解码时有占位,不让页面崩溃。

当前项目已有证据边界需要诚实说明:系统选择器的拉起与取消路径已经验证;真实选择用户图片、复制到应用沙箱并在冷启动后重新展示,还需要新的真机证据。代码存在不能替代运行证明。

十、一次选图功能变更需要做影响分析

如果未来从 3 张扩展到 6 张,不能只改 maxSelectNumber。还要同步检查:

  • 表单图片区域布局与可访问性;
  • ReportDraft 和正式记录的序列化上限;
  • 图片复制失败时的回滚成本;
  • 列表与详情的加载性能;
  • 删除记录时的引用感知清理;
  • 上传后端时的并发、重试和流量;
  • CSDN 或演示文档中的能力说明。

上限是跨层契约,不是页面常量。修改前先列影响面,修改后用同一组边界用例回归,才能避免多个层级各自理解一个“最多数量”。

十一、从用户动作到长期记录的责任边界

选图完成只是发布链路中的一个节点。把各层责任写清楚,可以避免后续开发把临时 URI、权限和数据库逻辑重新塞回页面:

阶段 输入 输出 失败后保留什么
ArkUI 表单 用户点击与当前草稿 发起选图请求 全部已填字段
PhotoPickerService 最大数量 0—3 个选中 URI 或平台错误 原表单状态
ReportService 完整 ReportDraft 校验通过的业务动作 可修改草稿
Photo Repository 临时/受管 URI 应用沙箱 URI 原 URI 与文本
Report Repository 规范化业务模型 权威本地记录 不写入半成品

这张表还能用于定位问题:系统面板没出现属于能力层,取消后图片被清空属于页面状态,重启后图片失效属于物化或持久化,不能统称为“Photo Picker 有 bug”。

十二、交付记录要能回答四个问题

一次选图功能验收不应只留下“按钮能点”的结论。更有用的记录需要回答:

  1. 使用了哪个入口和参数,是否明确最多 3 张、只选图片;
  2. 取消、空结果和平台异常分别呈现什么用户状态;
  3. 真实图片是否已物化到应用目录,并在冷启动后可再次显示;
  4. 哪些项目没有执行,是否仍需要特定设备、媒体样本或异常环境。

对于隐私说明,还要核对应用实际没有为了当前流程新增相册整库权限。若未来加入拍照、图片编辑或后台上传,应重新评估权限、失败降级和用户告知,不能沿用本文结论直接扩大能力范围。

维护者看到这样的记录,可以区分代码契约、系统面板、文件持久化和用户可见结果,不会把某一层通过错误扩写成端到端完成。

十三、本文小结

“寻迹校园”通过系统 Photo Picker 完成少量图片选择:Service 固定图片类型、把数量限制在 1—3、关闭当前入口的拍照能力,并把选择、取消和平台异常映射成不同结果。

页面只持有草稿 URI,不申请无关的整库访问,也不承担文件生命周期。下一篇将继续解决最容易被忽略的问题:Photo Picker 返回的临时 URI 为什么不能直接当成长期业务数据,以及 ReportPhotoRepository.materialize() 如何把它们复制到应用沙箱。

系列导航:第 11 篇 / 共 50 篇。上一篇:《ReportService 的隐私友好校验策略》;下一篇:《把临时 URI 复制到应用沙箱》。

Logo

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

更多推荐