Flutter 3.47 鸿蒙三方库适配实战:file_picker 文件选择兼容层
一、项目信息
| 项目 | 内容 |
|---|---|
| 适配仓库名称 | file_picker |
| 适配仓库地址 | https://atomgit.com/oh-flutter/file_picker |
| 原始包名 | file_picker |
| 适配版本 | 13.1.0-ohos.2 |
| 上游项目 | vicajilau/flutter_file_picker |
| 方法通道 | miguelruivo.flutter.plugins.filepicker |
| OHOS 原生入口 | FilePickerPlugin |
| Dart OHOS 注册 | FilePickerOhos |
| 构建产物 | entry-default-unsigned.hap |
文件选择器的适配目标不是自己实现一个文件浏览页面,而是把FilePicker的 Dart API 接到 OHOS 系统的DocumentViewPicker。这样用户仍然使用系统文件提供方、权限模型和保存流程,业务代码也无需改变。
环境搭建与版本
下面是本次适配实际使用的环境。环境搭建过程这里不再展开,直接看 Flutter OH 环境搭建指南。
| 项目 | 版本/说明 |
|---|---|
| Flutter OH | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.0.0 |
| HarmonyOS SDK | API 26 |
| 目标仓库 | file_picker |
二、保留FilePicker静态 API
上游包在 v13 中将FilePicker设计为静态入口,业务代码只需要调用:
finalPlatformFile?file=awaitFilePicker.pickFile();finalList<PlatformFile>files=awaitFilePicker.pickFiles(type:FileType.image,);finalString?directory=awaitFilePicker.getDirectoryPath();finalUri?uri=awaitFilePicker.saveFile(fileName:'ohos-demo.txt',bytes:Uint8List.fromList('hello'.codeUnits),);根pubspec.yaml增加 OHOS 声明:
version:13.1.0-ohos.2flutter:plugin:platforms:ohos:pluginClass:FilePickerPlugindartPluginClass:FilePickerOhosFilePickerOhos注册到FilePickerPlatform.instance,因此FilePicker静态方法不需要增加 OHOS 专用分支。
OHOS 工程目录创建
本次适配前,上游仓库没有ohos/目录。适配分支上按 Flutter OH 的插件工程约束创建平台目录,再把 ArkTS 入口接入pubspec.yaml:
gitswitch-c<your-ohos-adaptation-branch>flutter create--platformsohos.工程生成后至少保留以下边界:
ohos/ ├── index.ets ├── oh-package.json5 └── src/main/ ├── module.json5 └── ets/components/plugin/示例工程还需要生成example/ohos/,并在插件pubspec.yaml的flutter.plugin.platforms.ohos中声明pluginClass;只创建目录、没有注册入口,Dart 方法通道仍然不会工作。
三、原生选择器实现
插件在onAttachedToEngine时创建通道,在 Ability 绑定时保存UIAbilityContext:
constCHANNEL='miguelruivo.flutter.plugins.filepicker';exportdefaultclassFilePickerPluginimplementsFlutterPlugin,MethodCallHandler,AbilityAware{privatechannel:MethodChannel|null=null;privatecontext:common.UIAbilityContext|null=null;onAttachedToEngine(binding:FlutterPluginBinding):void{this.channel=newMethodChannel(binding.getBinaryMessenger(),CHANNEL);this.channel.setMethodCallHandler(this);}onAttachedToAbility(binding:AbilityPluginBinding):void{this.context=binding.getAbility().context;}}选择文件时使用DocumentSelectOptions:
consttarget=newpicker.DocumentViewPicker(this.context);constoptions=newpicker.DocumentSelectOptions();constmultiple=directory?false:Boolean(call.argument('multiple'));options.maxSelectNumber=multiple?50:1;if(directory){options.selectMode=picker.DocumentSelectMode.FOLDER;}constfilters=this.suffixFilters(call);if(filters.length>0){options.fileSuffixFilters=filters;}returnawaittarget.select(options);单文件选择使用maxSelectNumber = 1,多文件选择最多允许 50 个文件。FileType.image、FileType.video、FileType.audio分别映射为系统选择器的后缀过滤;FileType.custom使用业务传入的扩展名。
四、保存文件的完整链路
保存文件先创建DocumentSaveOptions,再由系统选择器返回 URI:
constoptions=newpicker.DocumentSaveOptions();options.newFileNames=[fileName];consttarget=newpicker.DocumentViewPicker(this.context);consturis=awaittarget.save(options);if(uris.length===0){returnnull;}consturi=uris[0];constbytes=call.argument('bytes')asUint8Array;constfile=fs.openSync(this.pathFromUri(uri),fs.OpenMode.READ_WRITE|fs.OpenMode.TRUNC,);try{fs.writeSync(file.fd,bytes.bufferasArrayBuffer);}finally{fs.closeSync(file);}returnuri;这里必须区分“用户取消”和“写入失败”:取消返回null,写入失败通过 MethodChannel error 返回,不能把空 URI 当成保存成功。
五、PlatformFile的 OHOS 表示
OHOS 返回的 URI 不一定都是本地file://路径,因此适配层实现了OhosPlatformFile:
baseclassOhosPlatformFileextendsPlatformFile{OhosPlatformFile(this.uri,{int?size});@overridefinalUriuri;String?getpath=>uri.scheme=='file'?uri.toFilePath():null;@overrideFuture<Uint8List>readAsBytes()=>File(path??uri.toString()).readAsBytes();}业务读取内容前应优先使用readAsBytes()或readAsByteStream(),不要假设所有系统 URI 都能直接用dart:io.File打开。
六、业务示例
选择图片:
finalList<PlatformFile>images=awaitFilePicker.pickFiles(type:FileType.custom,allowedExtensions:<String>['png','jpg','jpeg'],);for(finalPlatformFileimageinimages){debugPrint('${image.name}:${awaitimage.length()}bytes');}选择目录:
finalString?directory=awaitFilePicker.getDirectoryPath();if(directory!=null){debugPrint('selected directory:$directory');}保存文本:
import'dart:typed_data';finalUint8Listbytes=Uint8List.fromList('OpenHarmony file picker'.codeUnits,);finalUri?saved=awaitFilePicker.saveFile(fileName:'ohos-demo.txt',bytes:bytes,mimeType:'text/plain',);七、能力边界
| 能力 | OHOS 结果 |
|---|---|
| 单文件选择 | 支持 |
| 多文件选择 | 支持,最多 50 个 |
| 图片、视频、音频过滤 | 支持 |
| 自定义扩展名过滤 | 支持 |
| 目录选择 | 支持 |
| 保存文件 | 支持 |
clearTemporaryFiles() | 空实现,返回成功 |
skipEntitlementsChecks() | 空实现,返回成功 |
pickFileAndDirectoryPaths() | 部分支持,返回选择器 URI |
适配过程 FAQ 与解决方案
为什么不能把 OHOS 选择器返回值当成普通文件路径?
解决方案:OHOS 文件选择器返回的是 URI,应用只在当前授权范围内拥有读写能力。适配层应保留 URI,只有在确实需要真实路径时才通过系统能力复制或打开,不能用字符串拼接伪造路径。
FileType如何转换成系统过滤器?
解决方案:每种FileType都要落到文档选择器可识别的后缀或 MIME 条件;自定义类型要显式处理,否则系统会退回“全部文件”,业务看起来像是过滤器失效。
保存文件为什么容易误以为已经成功?
解决方案:选择器确认并不等于应用已经把内容写入目标 URI。保存流程要把“用户选择目标”和“应用完成写入”拆成两个状态,并在写入失败时把错误返回给 Dart。
目录模式为什么要单独测试?
解决方案:目录模式返回的是目录 URI,后续读取依赖目录权限和遍历能力。只测文件选择会漏掉目录 URI 无法读取、权限未继承和重复选择后 URI 失效等问题。
八、构建与模拟器验证
验证命令:
flutter pub get flutter analyzecdexample 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 |
| 模拟器安装启动 | 通过 |
| 页面状态 | file_picker OHOS示例首屏正常显示 |
版本与提交基线
| 项目 | 版本/地址 |
|---|---|
| 上游包 | file_picker |
| 上游最新版本 | 13.1.0 |
| 文章目标基线 | 13.1.0-ohos.2 |
| 当前已发布 TAG | 13.1.0-ohos.2 |
| 适配分支 | main |
| 目标最新 TAG | 13.1.0-ohos.2 |
| 最终提交 | 7c252ca |
版本号更新只代表文章和适配分支的基线已对齐;涉及上游 API 变更时,仍需在该 TAG 上重新运行 Dart 测试、ArkTS 编译、HAP 安装和真机回归。
参考资料和仓库地址
- 适配仓库主页
- 适配分支 main
- 当前已发布 TAG 13.1.0-ohos.2
- 目标最新 TAG 13.1.0-ohos.2
- 最终提交 7c252ca
- ArkTS 通道实现
- 鸿蒙适配说明
- pub.dev 包页面
- pub.dev 13.1.0 版本页
- 上游仓库
- Flutter 官方插件与平台通道文档
- Flutter OH 鸿蒙 SDK 仓库
欢迎加入 CPF-Flutter 鸿蒙社区。
九、把“选择器返回 URI”当作真实边界
1. 为什么不能把所有结果都当成普通文件路径
OHOS 系统文件选择器返回的是 URI,可能是file://形式,也可能带有其他 authority。Dart 层的OhosPlatformFile因此保留原始Uri,只在 URI 确实是本地文件时才提供path:
baseclassOhosPlatformFileextendsPlatformFile{OhosPlatformFile(this.uri,{int?size}):_size=size;@overridefinalUriuri;String?getpath=>uri.scheme=='file'?uri.toFilePath():null;@overrideFuture<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 |
如果业务传入自定义扩展名,适配层会自动补上前导点:
privatewithDot(value:string):string{returnvalue.startsWith('.')?value:`.${value}`;}这样 Dart 层既可以传png,也可以传.png,不会因为一个字符差异导致过滤器失效。
3. 目录模式必须显式设置
目录选择是这轮适配中最容易漏掉的分支。DocumentSelectOptions默认选择文件,即使把maxSelectNumber设为 1,也只会限制“只选一个文件”,不会切换成目录选择器:
constoptions=newpicker.DocumentSelectOptions();constmultiple=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 文本文件:
finalUint8Listbytes=Uint8List.fromList(utf8.encode('OpenHarmony file picker'),);finalUri?uri=awaitFilePicker.saveFile(fileName:'ohos-demo.txt',bytes:bytes,mimeType:'text/plain',);读取选中文件时,先判断是否能拿到本地路径:
Future<void>inspectFile(PlatformFilefile)async{finalString?path=file.path;if(path!=null){finalFilelocalFile=File(path);debugPrint('local size:${awaitlocalFile.length()}');return;}finalUint8Listbytes=awaitfile.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都被示例工程识别 |
| 模拟器 | 单文件、图片、目录、保存文本四个入口可打开系统 UI |
| 模拟器 | 自定义扩展名过滤生效,取消选择不写入文件 |
| 真实设备 | 非file://URI 不应被强行转成本地路径 |
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、临时文件和目录混合选择保持明确限制。
仓库名称:file_picker
仓库地址:https://atomgit.com/oh-flutter/file_picker
欢迎加入 CPF-Flutter OpenHarmony 社区,交流 Flutter 三方库适配、系统文件选择器和虚拟机验证经验。