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

上图为本文原创生成的技术插画,不是应用截图。核心关系是:应用只接收用户主动选择的图片 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;
这些参数分别表达清晰的产品约束:
- 只接收图片,不把视频混入失物发布;
- 调用方即使误传 20,最终也会被压到最多 3 张;
- 当前入口关闭拍照能力,避免同时扩展到相机权限与拍摄生命周期;
- 保留系统搜索,方便用户从较多图片中定位目标。
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。否则,用户已经选好的图片可能因为第二次打开后取消而被意外清空。
正确的取消体验是:
- 返回原表单;
- 已填写文本和已选图片保持不变;
- 显示“未选择照片,可继续填写”这类中性说明;
- 发布按钮继续由完整表单规则决定,而不是强制要求图片。

上图把三条结果路径分开:选择 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 读取、文件复制、磁盘空间和冷启动恢复仍是下一层必须验证的问题。
九、测试要覆盖边界值,而不只点一次按钮
建议至少覆盖以下用例:
maxSelectNumber传入 0、1、3、20,最终范围保持 1—3;- 选择 1 张与 3 张,页面顺序和移除操作正确;
- 打开后取消,原表单文本和原图片不丢失;
- 返回空 URI 数组时使用成功语义,不显示异常;
- 系统选择器抛错时返回
ErrorCategory.PLATFORM; - 连续点击时不会叠加多个选择器或覆盖 loading;
- 大字体、小屏和底部安全区下,3 个图片槽位仍可操作;
- 图片缺失或无法解码时有占位,不让页面崩溃。
当前项目已有证据边界需要诚实说明:系统选择器的拉起与取消路径已经验证;真实选择用户图片、复制到应用沙箱并在冷启动后重新展示,还需要新的真机证据。代码存在不能替代运行证明。
十、一次选图功能变更需要做影响分析
如果未来从 3 张扩展到 6 张,不能只改 maxSelectNumber。还要同步检查:
- 表单图片区域布局与可访问性;
ReportDraft和正式记录的序列化上限;- 图片复制失败时的回滚成本;
- 列表与详情的加载性能;
- 删除记录时的引用感知清理;
- 上传后端时的并发、重试和流量;
- CSDN 或演示文档中的能力说明。
上限是跨层契约,不是页面常量。修改前先列影响面,修改后用同一组边界用例回归,才能避免多个层级各自理解一个“最多数量”。
十一、从用户动作到长期记录的责任边界
选图完成只是发布链路中的一个节点。把各层责任写清楚,可以避免后续开发把临时 URI、权限和数据库逻辑重新塞回页面:
| 阶段 | 输入 | 输出 | 失败后保留什么 |
|---|---|---|---|
| ArkUI 表单 | 用户点击与当前草稿 | 发起选图请求 | 全部已填字段 |
| PhotoPickerService | 最大数量 | 0—3 个选中 URI 或平台错误 | 原表单状态 |
| ReportService | 完整 ReportDraft | 校验通过的业务动作 | 可修改草稿 |
| Photo Repository | 临时/受管 URI | 应用沙箱 URI | 原 URI 与文本 |
| Report Repository | 规范化业务模型 | 权威本地记录 | 不写入半成品 |
这张表还能用于定位问题:系统面板没出现属于能力层,取消后图片被清空属于页面状态,重启后图片失效属于物化或持久化,不能统称为“Photo Picker 有 bug”。
十二、交付记录要能回答四个问题
一次选图功能验收不应只留下“按钮能点”的结论。更有用的记录需要回答:
- 使用了哪个入口和参数,是否明确最多 3 张、只选图片;
- 取消、空结果和平台异常分别呈现什么用户状态;
- 真实图片是否已物化到应用目录,并在冷启动后可再次显示;
- 哪些项目没有执行,是否仍需要特定设备、媒体样本或异常环境。
对于隐私说明,还要核对应用实际没有为了当前流程新增相册整库权限。若未来加入拍照、图片编辑或后台上传,应重新评估权限、失败降级和用户告知,不能沿用本文结论直接扩大能力范围。
维护者看到这样的记录,可以区分代码契约、系统面板、文件持久化和用户可见结果,不会把某一层通过错误扩写成端到端完成。
十三、本文小结
“寻迹校园”通过系统 Photo Picker 完成少量图片选择:Service 固定图片类型、把数量限制在 1—3、关闭当前入口的拍照能力,并把选择、取消和平台异常映射成不同结果。
页面只持有草稿 URI,不申请无关的整库访问,也不承担文件生命周期。下一篇将继续解决最容易被忽略的问题:Photo Picker 返回的临时 URI 为什么不能直接当成长期业务数据,以及 ReportPhotoRepository.materialize() 如何把它们复制到应用沙箱。
系列导航:第 11 篇 / 共 50 篇。上一篇:《ReportService 的隐私友好校验策略》;下一篇:《把临时 URI 复制到应用沙箱》。
更多推荐


所有评论(0)