Flutter 3.47 鸿蒙三方库适配实战:file_picker 文件选择兼容层
Flutter 3.47 鸿蒙三方库适配实战:file_picker 文件选择兼容层
一、项目信息
| 项目 | 内容 |
|---|---|
| 适配仓库名称 | file_picker |
| 原始包名 | file_picker |
| 适配版本 | 13.1.0-ohos.1 |
| 上游项目 | vicajilau/flutter_file_picker |
| 方法通道 | miguelruivo.flutter.plugins.filepicker |
| OHOS 原生入口 | FilePickerPlugin |
| Dart OHOS 注册 | FilePickerOhos |
| 构建产物 | entry-default-unsigned.hap |
文件选择器的适配目标不是自己实现一个文件浏览页面,而是把 FilePicker 的 Dart API 接到 OHOS 系统的 DocumentViewPicker。这样用户仍然使用系统文件提供方、权限模型和保存流程,业务代码也无需改变。
二、保留 FilePicker 静态 API
上游包在 v13 中将 FilePicker 设计为静态入口,业务代码只需要调用:
final PlatformFile? file = await FilePicker.pickFile();
final List<PlatformFile> files = await FilePicker.pickFiles(
type: FileType.image,
);
final String? directory = await FilePicker.getDirectoryPath();
final Uri? uri = await FilePicker.saveFile(
fileName: 'ohos-demo.txt',
bytes: Uint8List.fromList('hello'.codeUnits),
);
根 pubspec.yaml 增加 OHOS 声明:
version: 13.1.0-ohos.1
flutter:
plugin:
platforms:
ohos:
pluginClass: FilePickerPlugin
dartPluginClass: FilePickerOhos
FilePickerOhos 注册到 FilePickerPlatform.instance,因此 FilePicker 静态方法不需要增加 OHOS 专用分支。
三、原生选择器实现
插件在 onAttachedToEngine 时创建通道,在 Ability 绑定时保存 UIAbilityContext:
const CHANNEL = 'miguelruivo.flutter.plugins.filepicker';
export default class FilePickerPlugin
implements FlutterPlugin, MethodCallHandler, AbilityAware {
private channel: MethodChannel | null = null;
private context: common.UIAbilityContext | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL);
this.channel.setMethodCallHandler(this);
}
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.context = binding.getAbility().context;
}
}
选择文件时使用 DocumentSelectOptions:
const target = new picker.DocumentViewPicker(this.context);
const options = new picker.DocumentSelectOptions();
const multiple = directory ? false : Boolean(call.argument('multiple'));
options.maxSelectNumber = multiple ? 50 : 1;
if (directory) {
options.selectMode = picker.DocumentSelectMode.FOLDER;
}
const filters = this.suffixFilters(call);
if (filters.length > 0) {
options.fileSuffixFilters = filters;
}
return await target.select(options);
单文件选择使用 maxSelectNumber = 1,多文件选择最多允许 50 个文件。FileType.image、FileType.video、FileType.audio 分别映射为系统选择器的后缀过滤;FileType.custom 使用业务传入的扩展名。
四、保存文件的完整链路
保存文件先创建 DocumentSaveOptions,再由系统选择器返回 URI:
const options = new picker.DocumentSaveOptions();
options.newFileNames = [fileName];
const target = new picker.DocumentViewPicker(this.context);
const uris = await target.save(options);
if (uris.length === 0) {
return null;
}
const uri = uris[0];
const bytes = call.argument('bytes') as Uint8Array;
const file = fs.openSync(
this.pathFromUri(uri),
fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC,
);
try {
fs.writeSync(file.fd, bytes.buffer as ArrayBuffer);
} finally {
fs.closeSync(file);
}
return uri;
这里必须区分“用户取消”和“写入失败”:取消返回 null,写入失败通过 MethodChannel error 返回,不能把空 URI 当成保存成功。
五、PlatformFile 的 OHOS 表示
OHOS 返回的 URI 不一定都是本地 file:// 路径,因此适配层实现了 OhosPlatformFile:
base class OhosPlatformFile extends PlatformFile {
OhosPlatformFile(this.uri, {int? size});
final Uri uri;
String? get path => uri.scheme == 'file' ? uri.toFilePath() : null;
Future<Uint8List> readAsBytes() =>
File(path ?? uri.toString()).readAsBytes();
}
业务读取内容前应优先使用 readAsBytes() 或 readAsByteStream(),不要假设所有系统 URI 都能直接用 dart:io.File 打开。
六、业务示例
选择图片:
final List<PlatformFile> images = await FilePicker.pickFiles(
type: FileType.custom,
allowedExtensions: <String>['png', 'jpg', 'jpeg'],
);
for (final PlatformFile image in images) {
debugPrint('${image.name}: ${await image.length()} bytes');
}
选择目录:
final String? directory = await FilePicker.getDirectoryPath();
if (directory != null) {
debugPrint('selected directory: $directory');
}
保存文本:
import 'dart:typed_data';
final Uint8List bytes = Uint8List.fromList(
'OpenHarmony file picker'.codeUnits,
);
final Uri? saved = await FilePicker.saveFile(
fileName: 'ohos-demo.txt',
bytes: bytes,
mimeType: 'text/plain',
);
七、能力边界
| 能力 | OHOS 结果 |
|---|---|
| 单文件选择 | 支持 |
| 多文件选择 | 支持,最多 50 个 |
| 图片、视频、音频过滤 | 支持 |
| 自定义扩展名过滤 | 支持 |
| 目录选择 | 支持 |
| 保存文件 | 支持 |
clearTemporaryFiles() | 空实现,返回成功 |
skipEntitlementsChecks() | 空实现,返回成功 |
pickFileAndDirectoryPaths() | 部分支持,返回选择器 URI |
八、构建验证
验证命令:
flutter pub get
flutter analyze
cd example
flutter pub get
flutter analyze
flutter build hap --debug --no-codesign
当前结果:
| 检查项 | 结果 |
|---|---|
根目录 flutter analyze | 通过 |
示例目录 flutter analyze | 通过 |
| OHOS ArkTS 编译 | 通过 |
| HAP 产物 | entry-default-unsigned.hap |
| release HAP 大小 | 约 21 MB |
![]() |
九、把“选择器返回 URI”当作真实边界
1. 为什么不能把所有结果都当成普通文件路径
OHOS 系统文件选择器返回的是 URI,可能是 file:// 形式,也可能带有其他 authority。Dart 层的 OhosPlatformFile 因此保留原始 Uri,只在 URI 确实是本地文件时才提供 path:
base class OhosPlatformFile extends PlatformFile {
OhosPlatformFile(this.uri, {int? size}) : _size = size;
final Uri uri;
String? get path =>
uri.scheme == 'file' ? uri.toFilePath() : null;
Future<Uint8List> readAsBytes() =>
File(path ?? uri.toString()).readAsBytes();
}
这解释了为什么业务代码不应无条件写 File(platformFile.path!)。如果系统返回非 file:// URI,path 可能为 null,此时应该使用 URI、平台文件接口或专门的 OHOS 文件访问能力,而不是假设一个本地路径。
2. FileType 到系统后缀过滤器的映射
DocumentSelectOptions.fileSuffixFilters 不理解 Dart 的 FileType.image 枚举,所以 ArkTS 侧先把类型转换为后缀列表:
| Dart 类型 | OHOS 后缀过滤 |
|---|---|
FileType.any | 不设置过滤器 |
FileType.media | 图片、视频常用后缀 |
FileType.image | .jpg、.jpeg、.png、.gif、.bmp、.webp |
FileType.video | .mp4、.mov、.mkv、.webm、.avi |
FileType.audio | .mp3、.aac、.flac、.wav、.ogg、.m4a |
FileType.custom | 业务传入的 allowedExtensions |
如果业务传入自定义扩展名,适配层会自动补上前导点:
private withDot(value: string): string {
return value.startsWith('.') ? value : `.${value}`;
}
这样 Dart 层既可以传 png,也可以传 .png,不会因为一个字符差异导致过滤器失效。
3. 目录模式必须显式设置
目录选择是这轮适配中最容易漏掉的分支。DocumentSelectOptions 默认选择文件,即使把 maxSelectNumber 设为 1,也只会限制“只选一个文件”,不会切换成目录选择器:
const options = new picker.DocumentSelectOptions();
const multiple = directory ? false : Boolean(call.argument('multiple'));
options.maxSelectNumber = multiple ? 50 : 1;
if (directory) {
options.selectMode = picker.DocumentSelectMode.FOLDER;
}
DocumentSelectMode.FOLDER 从 OHOS API 11 开始提供,且目录一次只能选择一个。当前实现据此把 getDirectoryPath() 映射为目录 URI;不支持目录能力的设备应让系统选择器返回错误,而不是把文件 URI 伪装成目录路径。
4. 保存文件不是“选择器返回就结束”
保存流程分为四步:
Dart FilePicker.saveFile()
|
v
DocumentSaveOptions.newFileNames
|
v
DocumentViewPicker.save() 返回 URI
|
v
ArkTS fs.openSync() + fs.writeSync()
|
v
返回 URI 给 Dart
用户取消时系统返回空 URI,适配层返回 null;写入失败时通过 MethodChannel error 传播。两种情况不能合并成同一个“保存失败”,否则业务无法区分用户主动取消和文件权限、磁盘空间等真实异常。
5. 保存路径和读取路径的示例
保存一个 UTF-8 文本文件:
final Uint8List bytes = Uint8List.fromList(
utf8.encode('OpenHarmony file picker'),
);
final Uri? uri = await FilePicker.saveFile(
fileName: 'ohos-demo.txt',
bytes: bytes,
mimeType: 'text/plain',
);
读取选中文件时,先判断是否能拿到本地路径:
Future<void> inspectFile(PlatformFile file) async {
final String? path = file.path;
if (path != null) {
final File localFile = File(path);
debugPrint('local size: ${await localFile.length()}');
return;
}
final Uint8List bytes = await file.readAsBytes();
debugPrint('uri size: ${bytes.length}');
}
这段代码比直接使用 File(file.path!) 更接近 OHOS 的真实文件模型。
6. 测试矩阵
文件选择器同时涉及 Flutter Dart 契约、ArkTS 系统 API 和真实 UI,因此测试要分层:
| 层次 | 验证内容 |
|---|---|
| Dart 单元测试 | FilePicker 静态 API 能委托到平台实例 |
| Dart 单元测试 | 取消选择返回 null 或空列表 |
| ArkTS 编译 | DocumentSelectMode.FOLDER 和 DocumentSaveOptions 类型可用 |
| HAP 构建 | dartPluginClass 和 pluginClass 都被示例工程识别 |
7. 常见故障排查
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 目录按钮却选择了一个文件 | 未设置 DocumentSelectMode.FOLDER | 在目录分支设置 FOLDER 模式 |
| 图片过滤器无效 | 业务传入的扩展名没有统一格式 | 使用 withDot() 归一化 |
| 保存后文件为空 | 只打开了文件但没有写入 | 检查 fs.writeSync() 和 finally 关闭逻辑 |
file.path 为 null | URI 不是本地 file:// | 使用 URI 或平台文件读取接口 |
| 多文件选择数量异常 | 只设置单选模式却调用 pickFiles() | 确认 Dart 层和 ArkTS 层的 multiple 一致 |
| 示例点击无反应 | UIAbility Context 未绑定 | 在 Ability attached 后再调用选择器 |
十、总结
file_picker 的 OHOS 适配遵循三个原则:
- 保留上游静态 API 和
PlatformFile模型。 - 通过系统
DocumentViewPicker选择和保存文件。 - 对 URI、临时文件和目录混合选择保持明确限制。
欢迎加入 CPF-Flutter OpenHarmony 社区,交流 Flutter 三方库适配、系统文件验证经验。
更多推荐




所有评论(0)