仓颉实战:构建高效、类型安全的鸿蒙相册选择器
目录
-
引言:超越“打开相册”——状态与URI的挑战
-
鸿蒙相册API与权限模型
-
架构设计:
AlbumViewModel的职责与状态 -
仓颉的异步之美:
async/await处理选择流 -
核心实战(一):单张图片选择与UI绑定
-
核心实战(二):多选与
Array<T>的状态管理 -
【深度实践】URI的“临时陷阱”与持久化策略
-
总结:从API调用到健壮的组件
一、引言:超越“打开相册”——状态与URI的挑战
在鸿蒙应用开发中,“相册选择”是一个基础且高频的功能。表面上看,这只是一个“拉起系统界面 -> 获取图片”的简单调用。然而,一个真正健壮的相册功能,背后隐藏着一系列不容忽视的工程挑战:
-
异步流:拉起相册是一个异步操作。用户可能在1秒内完成选择,也可能在30秒后才取消。如何管理这个异步流,避免“回调地狱”?
-
状态管理:选择图片后,UI如何响应?加载中(Loading)状态、选择结果(
Array<String>)、取消(Cancelled)状态、失败(Error)状态,如何与UI解耦? -
URI生命周期:相册返回的是一个
content://URI,它通常只带有一个临时访问权限。如果App重启,这个URI可能就失效了。如何将这个临时资源持久化?
仓颉语言凭借其现代化的并发模型(async/await)、强大的类型系统和声明式的UI状态管理(@State, @Observable),为我们提供了一套优雅的解决方案。本文将从零开始,构建一个解耦、可测试、可复用的相册选择功能,重点展示仓颉在处理异步I/O和状态管理方面的架构优势。
二、鸿蒙相册API与权限模型
在仓颉中调用相册能力,我们首先需要与鸿蒙的底层API交互。
2.1 权限声明
首先2.1 权限声明
首先,必须在 module.json5 中声明读取媒体的权限:
{
"module": {
// ...
"reqPermissions": [
{
// 读取媒体权限
"name": "ohos.permission.READ_MEDIA"
}
]
}
}
2.2 相册API(假设)
鸿蒙提供了相册选择器(`PhotoViewPicker 或 filepicker)的能力。在仓颉中,我们期望这是一个返回 Promise 的现代化API。
// 假设的鸿蒙相册API封装
import { PhotoViewPicker, PickOptions, PhotoResult } from 'harmony.media.PhotoPicker'
// PhotoResult 可能的结构
struct PhotoResult {
var uri: String // 临时内容URI, e.g., "content://..."
var mimeType: String
}
// 权限服务
import { PermissionService } from 'harmony.security.Permission'
class AlbumAPI {
// 请求权限
static func requestReadPermission(): Promise<Bool> {
return PermissionService.request(["ohos.permission.READ_MEDIA"])
.then { grants => grants.get("ohos.permission.READ_MEDIA") == PermissionStatus.Granted }
}
// 拉起单选
static func pickSingle(): Promise<PhotoResult?> {
let picker = PhotoViewPicker()
return picker.pick(PickOptions(maxSelection: 1))
.then { results => results.first() } // 返回第一个,或None
}
// 拉起多选
static func pickMultiple(max: Int32 = 9): Promise<Array<PhotoResult>> {
let picker = PhotoViewPicker()
return picker.pick(PickOptions(maxSelection: max))
}
}
三、架构设计:AlbumViewModel 的职责与状态
为了实现“高内聚、低耦合”,我们坚决反对在@Component(View层)中直接调用API和处理业务逻辑。我们构建一个 AlbumViewModel 来承担所有“脏活累活”。
@Observable
class AlbumViewModel {
// --- 状态 (State) ---
// 单选结果
@State var selectedImage: PhotoResult? = None
// 多选结果
@State var selectedImages: Array<PhotoResult> = []
// 加载状态
@State var isLoading: Bool = false
// 错误信息
@State var errorMessage: String? = None
// --- 逻辑 (Logic) ---
// 检查权限
private func checkPermission(): Promise<Bool> { ... }
// 执行单选
func selectSinglePhoto() { ... }
// 执行多选
func selectMultiplePhotos() { ... }
// 清空选择
func clearSelection() {
this.selectedImage = None
this.selectedImages.clear()
}
}
这个 ViewModel 是一个纯粹的仓颉 class,它不依赖任何UI组件,具有极高的可测试性。`View 层只需要“订阅”它的状态并“调用”它的方法。
四、仓颉的异步之美:async/await 处理选择流
这是仓颉并发模型的核心优势。ViewModel 中的异步逻辑不再需要层层嵌套的 .then 回调,而是使用 async/await 编写“同步风格”的异步代码。
@Observable
class AlbumViewModel {
// ...
// 使用 Task 启动异步任务
func selectSinglePhoto() {
// 1. 立即更新UI状态
this.isLoading = true
this.errorMessage = None
Task {
try {
// 2. 检查权限
let hasPermission = await AlbumAPI.requestReadPermission()
if (!hasPermission) {
throw Error("未授予相册读取权限")
}
// 3. 异步等待用户选择
// (主线程不会阻塞)
let result = await AlbumAPI.pickSingle()
// 4. 更新状态
this.selectedImage = result
} catch (e: CancellationError) {
// 4.1. 用户取消了操作
Logger.info("用户取消了选择")
} catch (e: Error) {
// 4.2. 发生错误
this.errorMessage = e.message
} finally {
// 5. 无论成功、失败还是取消,都结束加载
this.isLoading = false
}
}
}
}
try/catch/finally 的组合拳,完美地覆盖了成功、失败和取消三种异步场景,finally 确保了 isLoading 状态不会被“卡住”。
五、核心实战(一):单张图片选择与UI绑定
在 View 层,我们只负责声明UI如何响应 ViewModel 的状态。
@Component
struct ProfileAvatarUploader {
// 1. 持有 ViewModel 实例
@State var viewModel: AlbumViewModel = AlbumViewModel()
func build() -> View {
Column(spacing: 16.0) {
// 2. 绑定单选结果
if (let photo = this.viewModel.selectedImage) {
Image(src: photo.uri)
.size(width: 120.0, height: 120.0)
.cornerRadius(60.0)
.objectFit(ObjectFit.Cover)
} else {
// 默认占位图
Image(src: "resource:media.default_avatar")
.size(width: 120.0, height: 120.0)
}
// 3. 绑定错误状态
if (let error = this.viewModel.errorMessage) {
Text(error).color(Color.Red).fontSize(12.0)
}
// 4. 触发逻辑并绑定加载状态
Button("更换头像") {
this.viewModel.selectSinglePhoto()
}
.loading(this.viewModel.isLoading)
}
}
}
UI (ProfileAvatarUploader) 完全不知道API、权限和异步的存在。它只负责响应 viewModel 的状态,实现了彻底的职责分离。
六、核心实战(二):多选与Array<T>的状态管理
多选的挑战在于管理一个列表状态。得益于仓颉的响应式系统,当 ViewModel 中的 @State 数组发生变化时,UI会自动更新。
// --- In AlbumViewModel ---
func selectMultiplePhotos() {
this.isLoading = true
this.errorMessage = None
Task {
try {
let hasPermission = await AlbumAPI.requestReadPermission()
if (!hasPermission) { throw Error("未授予相册读取权限") }
let results = await AlbumAPI.pickMultiple(9 - this.selectedImages.size())
if (!results.isEmpty) {
// 仓颉的响应式系统会检测到 Array 的变化
// 并自动通知UI重渲染
this.selectedImages.appendAll(results)
}
} catch (e: CancellationError) {
Logger.info("用户取消了选择")
} catch (e: Error) {
this.errorMessage = e.message
} finally {
this.isLoading = false
}
}
}
// --- In View Component ---
@Component
struct PostCreator {
@State var viewModel: AlbumViewModel = AlbumViewModel()
func build() -> View {
Column {
// 1. GridView 自动响应 selectedImages 的变化
GridView(items: this.viewModel.selectedImages) { photo =>
Image(src: photo.uri)
.aspectRatio(1.0)
.objectFit(ObjectFit.Cover)
}
// 2. 添加按钮
Button("添加图片 (${this.viewModel.selectedImages.size()} / 9)") {
this.viewModel.selectMultiplePhotos()
}
.loading(this.viewModel.isLoading)
.disabled(this.viewModel.selectedImages.size() >= 9)
}
}
}
七、【深度实践】URI的“临时陷阱”与持久化策略
这是本文最具深度的实践点,也是新手最容易忽略的“陷阱”。
相册返回的 content://... URI 携带的访问权限是临时的。当你的应用进程被系统回收(例如切到后台过久)后,这个URI就会失效。如果你将这个URI字符串保存到数据库或 SharedPreference 中,下次启动应用时会发现图片无法加载。
**正确的做法是:在拿到I后,立即将其复制(转储)到应用自己的私有沙盒目录中,转而存储这个“永久”的内部文件路径。**
// 假设 FileService API
import { FileService } from 'harmony.io.File'
import { AppContext } from 'harmony.app.Context'
@Observable
class AlbumViewModel {
// 状态应存储持久化路径
@State var permanentImagePath: String? = None
// ...
func selectAndPersistSinglePhoto() {
this.isLoading = true
this.errorMessage = None
Task {
try {
let hasPermission = await AlbumAPI.requestReadPermission()
if (!hasPermission) { throw Error("未授予相册读取权限") }
let result = await AlbumAPI.pickSingle()
if (let photo = result) {
// 关键步骤:转储文件
let permanentPath = await this.saveToAppSandbox(photo.uri)
// 最终更新状态
this.permanentImagePath = permanentPath
}
} catch (e: Error) {
this.errorMessage = e.message
} finally {
this.isLoading = false
}
}
}
/**
* 深度实践:将临时URI复制到应用沙盒
* @param tempUri 临时的 content:// URI
* @return 永久的内部文件路径
*/
private func saveToAppSandbox(tempUri: String): Promise<String> {
// 1. 获取沙盒路径
let sandboxDir = AppContext.getCacheDir() + "/images/"
let fileName = "PICKED_${Date.now().timestamp}.jpg"
let permanentPath = sandboxDir + fileName
// 2. 确保目录存在
await FileService.makeDir(sandboxDir, { recursive: true })
// 3. 异步执行文件I/O
// (await 自动将 FileService.copy 的 Promise 解包)
try {
await FileService.copy(from: tempUri, to: permanentPath)
Logger.info("图片已复制到沙盒: ${permanentPath}")
return permanentPath
} catch (e: Error) {
Logger.error("文件复制失败: ${e.message}")
throw Error("保存图片失败")
}
}
}
现在,`permanentmagePath` 是一个指向你应用私有目录的文件路径,它在应用重启后依然有效,可以被安全地保存到数据库或用于上传。
八、总结:从API调用到健壮的组件
本文我们解构了“相册选择”功能。它远不止是 `picker.pick() 一行代码,而是一个涉及权限、异步、状态、I/O的完整工程链路。
通过仓颉的现代特性,我们构建了一个健壮的解决方案:
-
MVVM架构:使用
@Observableclass 将所有逻辑与@Component视图彻底解耦。 -
异步处理:使用
async/await和Task,将复杂的回调流转变为清晰的“同步”逻辑,并用try/catch/finally完美处理了所有分支。 -
状态管理:
@State自动将ViewModel的状态(如isLoading,selectedImages)绑定到UI,无需手动操作。 -
深度实践:我们解决了临时URI的“陷阱”,通过异步I/O将其持久化到沙盒,确保了功能的长期健壮性。
更多推荐
所有评论(0)