☰
Flutter 3.47 鸿蒙三方库适配实战:file_picker 文件选择兼容层
2026/10/25 1:54:58 网站建设 项目流程

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 OH3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco Studio26.0.0
HarmonyOS SDKAPI 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:FilePickerOhos

FilePickerOhos注册到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
当前已发布 TAG13.1.0-ohos.2
适配分支main
目标最新 TAG13.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为nullURI 不是本地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 三方库适配、系统文件选择器和虚拟机验证经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询