Flutter 三方库 flutter_file_dialog 的 OpenHarmony 适配实战
Flutter 社区地址: https://atomgit.com/CPF-Flutter/flutter_flutter
适配后仓库地址:https://atomgit.com/oh-flutter/flutter_file_dialog
本文记录了将开源 Flutter 三方库
flutter_file_dialog适配到 OpenHarmony / HarmonyOS 平台的完整过程,
包含适配思路、代码改动对照、关键决策和踩坑复盘(含真机验证时发现并修复的两个真实问题)。
一、背景
1.1 三方库简介
flutter_file_dialog 是一个 Flutter 社区广泛使用的文件对话框三方库,提供以下能力:
- 选择文件:通过系统文件管理器选择单个文件,支持文件扩展名过滤;
- 选择目录:通过系统文件管理器选择目录,返回目录 URI;
- 保存文件:弹出系统保存对话框,将文件保存到用户选择的位置;
- 保存到指定目录:将文件保存到
pickDirectory选择的目录,无需再次弹出对话框。
该三方库最初支持 Android 与 iOS 两个平台,本次任务将其适配到 OpenHarmony / HarmonyOS 平台。
1.2 适配目标
| 维度 | 要求 |
|---|---|
| 功能一致性 | pickFile / pickDirectory / saveFile / saveFileToDirectory / isPickDirectorySupported 五个方法全部可用 |
| Dart 层零改动 | Dart API、方法签名、返回值结构保持不变,只新增 ohos/ 平台目录 |
| 性能 | 复用系统 FilePicker(DocumentViewPicker),零额外内存开销 |
| 工程规范 | 遵循 CPF-Flutter 三方库鸿蒙化规范(README.OpenHarmony_CN/EN、CHANGELOG.OpenHarmony) |
二、适配路线图
整个适配分为 5 个阶段:
第 1 阶段:项目初始化 ── flutter create 生成 ohos 平台模板骨架
第 2 阶段:原生实现 ── 对照 Android Kotlin 实现翻译为 ArkTS(核心)
第 3 阶段:三方库注册 ── pubspec.yaml 声明 ohos 平台 pluginClass
第 4 阶段:示例验证 ── 生成 example/ohos 宿主工程,签名后在真机验证
第 5 阶段:构建验证 ── flutter build hap 产出 signed HAP 并安装运行
三、逐步适配过程
第 1 阶段:项目初始化
使用 Flutter 命令行生成 OHOS 模板:
flutter create . --template=plugin --platforms=ohos
该命令会自动生成 ohos/ 目录的标准模板结构,包含必要的构建配置和入口文件。
目录结构:
ohos/
├── index.ets # 模块入口,导出三方库类
├── oh-package.json5 # 包配置
├── build-profile.json5 # 构建配置
├── src/main/
│ ├── module.json5 # HAR 模块配置
│ └── ets/components/plugin/
│ └── FlutterFileDialogPlugin.ets # 原生三方库实现(核心)
关键配置文件:
index.ets(入口导出文件)
import FlutterFileDialogPlugin from './src/main/ets/components/plugin/FlutterFileDialogPlugin';
export default FlutterFileDialogPlugin;
oh-package.json5(包配置)
{
"name": "flutter_file_dialog",
"version": "1.0.0",
"main": "index.ets",
"license": "Apache-2.0",
"dependencies": {}
}
注:
@ohos/flutter_ohos由 Flutter 引擎在构建时自动链接,无需在dependencies中显式声明。
module.json5(HAR 模块配置)
{
"module": {
"name": "flutter_file_dialog",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
第 2 阶段:原生实现(核心)
这是适配的核心工作。将 Android 平台的 Kotlin 实现逐一翻译为 ArkTS。
2.1 整体架构对比
flutter_file_dialog 属于方法调用型插件(Dart 主动调用原生,MethodChannel + MethodCallHandler),同时需要访问 UIAbility 上下文来拉起系统 FilePicker,因此还需实现 AbilityAware 接口:
Android (Kotlin) OHOS (ArkTS)
──────────────────── ────────────────────
class FlutterFileDialogPlugin class FlutterFileDialogPlugin
implements FlutterPlugin, implements FlutterPlugin,
ActivityAware, MethodCallHandler,
MethodCallHandler AbilityAware
import { FlutterPlugin,
import io.flutter... FlutterPluginBinding,
import android.app.Activity MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
AbilityAware,
AbilityPluginBinding
} from '@ohos/flutter_ohos'
Activity / ActivityAware UIAbility / AbilityAware
Intent ACTION_OPEN_DOCUMENT picker.DocumentViewPicker
2.2 通道注册
| 平台 | 代码 |
|---|---|
| Android | MethodChannel(messenger, "flutter_file_dialog") |
| OHOS | new MethodChannel(binding.getBinaryMessenger(), "flutter_file_dialog") |
差异:OHOS 使用
getBinaryMessenger(),接口与 Android 基本一一对应;通道名必须与 Dart 侧MethodChannel('flutter_file_dialog')完全一致。
2.3 能力绑定(AbilityAware)
系统 FilePicker(DocumentViewPicker)必须在 UIAbility 上下文中才能拉起,因此插件实现了 AbilityAware 接口,对应 Android 侧的 ActivityAware:
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.ability = binding.getAbility();
}
onDetachedFromAbility(): void {
this.ability = null;
}
2.4 原生方法实现对照
五个核心方法逐一对照:
| 方法 | Android 实现 | OHOS 实现 |
|---|---|---|
pickFile | Intent(ACTION_OPEN_DOCUMENT) + 扩展名过滤 + 复制到缓存目录 | DocumentViewPicker.select() + fileSuffixFilters + 复制到缓存目录 |
pickDirectory | Intent(ACTION_OPEN_DOCUMENT_TREE) | DocumentSelectOptions.selectMode = FOLDER |
isPickDirectorySupported | Build.VERSION.SDK_INT >= LOLLIPOP | 返回 true |
saveFile | Intent(ACTION_CREATE_DOCUMENT) + 后台复制 | DocumentViewPicker.save() + writeSync |
saveFileToDirectory | DocumentFile.createFile() 直接写入已授权目录 | 系统保存对话框预填所选目录(见第八章踩坑 #1) |
OHOS 实现(ArkTS)核心片段:
onMethodCall(call: MethodCall, result: MethodResult): void {
if (this.ability == null) {
result.error("init_failed", "Not attached to ability", null);
return;
}
try {
switch (call.method) {
case 'getPlatformVersion':
result.success("OpenHarmony");
break;
case 'isPickDirectorySupported':
result.success(true);
break;
case 'pickDirectory':
this.pickDirectory(result);
break;
case 'pickFile':
this.pickFile(call, result);
break;
case 'saveFile':
this.saveFile(call, result);
break;
case 'saveFileToDirectory':
this.saveFileToDirectory(call, result);
break;
default:
result.notImplemented();
}
} catch (err) {
result.error("internal_error", `Error: ${(err as Error).message}`, null);
}
}
pickFile 实现(含扩展名过滤与复制到缓存):
private async pickFile(call: MethodCall, result: MethodResult): Promise<void> {
try {
const context = this.ability?.context as common.UIAbilityContext;
const fileExtensionsFilter = call.argument('fileExtensionsFilter') as Array<string> | null;
const copyFileToCacheDir = (call.argument('copyFileToCacheDir') as boolean | null) ?? true;
const documentPicker = new picker.DocumentViewPicker(context);
const options = new picker.DocumentSelectOptions();
options.maxSelectNumber = 1;
if (fileExtensionsFilter != null && fileExtensionsFilter.length > 0) {
const filters: Array<string> = new Array<string>();
fileExtensionsFilter.forEach((ext: string) => {
filters.push(`.${ext}`);
});
options.fileSuffixFilters = filters;
}
const uris: Array<string> = await documentPicker.select(options);
if (uris == null || uris.length == 0) {
result.success(null);
return;
}
const uri = uris[0];
if (copyFileToCacheDir) {
const path = await this.copyFileToCacheDir(context, uri);
result.success(path);
} else {
result.success(uri);
}
} catch (err) {
const e = err as BusinessError;
if (e.code == 13900042) {
// user cancelled
result.success(null);
return;
}
result.error("pick_file_failed", e.message, null);
}
}
关键差异点:
- Android 用
Intent+startActivityForResult+onActivityResult回调,状态机复杂;OHOS 用DocumentViewPicker.select()/save()的 Promise 直接返回 URI 数组,代码大幅简化;- 取消操作:Android 在
onActivityResult中判resultCode,OHOS 捕获BusinessError(code13900042)视为取消,统一返回null;fileExtensionsFilter(如["pdf"])需转换为鸿蒙fileSuffixFilters的".pdf"格式。
第 3 阶段:三方库注册
在 pubspec.yaml 中添加 OHOS 平台注册:
flutter:
plugin:
platforms:
android:
package: com.kineapps.flutter_file_dialog
pluginClass: FlutterFileDialogPlugin
ios:
pluginClass: FlutterFileDialogPlugin
ohos: # ← 新增
pluginClass: FlutterFileDialogPlugin # ← 对应 index.ets 默认导出
Flutter 的 OHOS 引擎在构建时会读取 pubspec.yaml 中的 ohos 配置,自动加载 ohos/index.ets 中导出的三方库类。
注意:
pluginClass必须与 ArkTS 实现类的getUniqueClassName()返回值完全一致(区分大小写)。
第 4 阶段:示例应用创建
flutter create . --template=plugin --platforms=ohos 会自动在 example/ 下生成 OHOS 宿主工程:
example/ohos/
├── AppScope/app.json5 # 应用配置
├── build-profile.json5 # 项目构建配置(含 signingConfigs、SDK 版本)
├── hvigor/hvigor-config.json5 # 构建工具配置
├── oh-package.json5 # 顶层包配置
├── hvigorfile.ts # 构建入口
├── entry/
│ ├── build-profile.json5
│ ├── oh-package.json5
│ ├── src/main/
│ │ ├── module.json5 # entry 模块配置
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # Ability 生命周期
│ │ │ └── pages/
│ │ │ └── Index.ets # UI 页面(Flutter 容器)
│ │ └── resources/
│ │ └── rawfile/flutter_assets/ # Flutter 运行时资源
│ └── src/ohosTest/ # 测试目录
说明:
GeneratedPluginRegistrant.ets由 Flutter 工具根据pubspec.yaml的ohos配置自动生成,会import FlutterFileDialogPlugin并注册到 FlutterEngine,无需手写。
第 5 阶段:构建验证(HAP)
用 Flutter 工具链构建签名 HAP:
cd example
flutter build hap --debug
成功产出:build/ohos/hap/entry-default-signed.hap。
说明:签名配置在 DevEco Studio 中配置(本机
~/.ohos/config下的自动签名证书),构建时自动注入signingConfigs。
四、完整代码对照
4.1 Android vs OHOS 完整实现对照
| 维度 | Android (Kotlin) | OHOS (ArkTS) |
|---|---|---|
| 语言 | Kotlin | ArkTS (TypeScript 语法) |
| 插件接口 | FlutterPlugin + ActivityAware + MethodCallHandler | FlutterPlugin + AbilityAware + MethodCallHandler |
| 通道注册 | MethodChannel(messenger, "flutter_file_dialog") | new MethodChannel(binding.getBinaryMessenger(), "flutter_file_dialog") |
| 拉起选择器 | Intent(ACTION_OPEN_DOCUMENT) + startActivityForResult | DocumentViewPicker.select(options) |
| 拉起保存器 | Intent(ACTION_CREATE_DOCUMENT) | DocumentViewPicker.save(options) |
| 目录选择 | Intent(ACTION_OPEN_DOCUMENT_TREE) | selectMode = DocumentSelectMode.FOLDER |
| 取消判定 | onActivityResult 判 resultCode | 捕获 BusinessError code 13900042 |
| 写文件 | contentResolver.openOutputStream | fileIo.openSync + writeSync |
4.2 关键 ArkTS 语法差异
| Android 语法 | ArkTS 语法 | 备注 |
|---|---|---|
import io.flutter.plugin.common.MethodChannel | import { MethodChannel } from '@ohos/flutter_ohos' | OHOS 使用模块化导入 |
binding.activity | binding.getAbility() | Activity → UIAbility |
call.argument("key") | call.argument('key') | 参数读取方式一致 |
result.success/error/notImplemented | result.success/error/notImplemented | 结果回调 API 一致 |
ByteArray | Uint8Array | 二进制数据表示(Dart Uint8List) |
Array<String> | Array<string> | 泛型语法 |
五、关键决策说明
决策 1:保持通道名与方法契约不变
Dart 层 MethodChannel('flutter_file_dialog') 与五个方法名(pickFile / pickDirectory / isPickDirectorySupported / saveFile / saveFileToDirectory)是 Dart 与原生之间的通信契约,OHOS 侧必须完全一致,任何改动都会破坏 Dart 层兼容性。
维护策略:通道名与方法签名以 Dart 层定义为准,后续上游新增方法时在 onMethodCall 中同步补充 case 分支。
决策 2:Dart 层零改动,仅新增 ohos 平台目录
适配只在仓库中新增 ohos/、example/ohos/、README.OpenHarmony_CN/EN 与 CHANGELOG.OpenHarmony,lib/ 与 Android/iOS 代码零改动,保证上游可无冲突合入。
维护策略:保持 lib/ 与上游同步,OHOS 实现独立演进。
决策 3:用系统 DocumentViewPicker 替代手写文件对话框
Android 通过 Intent 拉起系统文档选择器,OHOS 的等价物就是 picker.DocumentViewPicker(select/save),同样是系统级对话框,无需申请任何权限(Picker 授权机制自带)。
维护策略:优先复用系统能力,避免自绘 UI 导致体验割裂。
决策 4:saveFileToDirectory 改用系统保存对话框预填所选目录
初版直接对目录 URI 拼接子路径写入,真机验证发现 select() 返回的目录 URI 仅具临时只读权限,写入报 OpenFile err: 5。最终改用 DocumentViewPicker.save() 并设置 defaultFilePathUri 预填 pickDirectory 所选目录(详见第八章踩坑 #1)。
维护策略:后续若鸿蒙开放目录写授权能力(如 FolderAuthorization 系统能力),可回退为静默写入以对齐 Android 行为。
决策 5:writeSync 前将 Uint8Array 转为 ArrayBuffer
fileIo.writeSync 只接受 ArrayBuffer | string,Dart 侧 Uint8List 经通道解码为 Uint8Array,直接传入报 Failed to resolve buf and options。新增 toArrayBuffer() 工具方法按 byteOffset/byteLength 精确切片转换(详见第八章踩坑 #2)。
维护策略:所有二进制写入路径统一走 toArrayBuffer(),避免同类问题复发。
六、测试与验证
测试环境
| 项目 | 版本 |
|---|---|
| Flutter | 3.41.10-ohos-1.0.0 |
| Dart | 3.11.5 |
| HarmonyOS SDK | 5.1.0(18)(targetSdkVersion 26.0.0) |
| IDE | DevEco Studio(内置 SDK 26.0.0) |
| 设备 ROM | ALN-AL00 7.0.0.105(SP6C00E105R4P3),OpenHarmony 7.0.0.105(API 26) |
验证要点
- 构建验证 —
flutter build hap --debug成功产出entry-default-signed.hap,flutter analyze无错误; - pickFile — 真机点击 Pick file,系统文件管理器弹出,选择中文名 PDF,返回缓存路径
/data/storage/el2/base/haps/entry/cache/file_dialog/兼职老师.pdf(279.6 KB),扩展名过滤、复制到缓存、中文文件名解码均正常; - pickDirectory — 真机点击 Pick directory,目录选择器弹出,选中 Download,返回
file://docs/storage/Users/currentUser/Download; - saveFile — 真机点击 Save file,系统保存对话框弹出(“将文件保存至 Download”);
- saveFileToDirectory — 修复后保存对话框预填所选目录,确认保存返回
errorcode: 0; - 取消场景 — 选择器返回空数组 /
13900042时统一返回null,Dart 侧pickFile等返回null不抛异常。
七、运行效果
真机(ALN-AL00,OpenHarmony 7.0.0.105)实测截图:
1. 选择文件后返回缓存路径

2. 目录选择器(selectMode = FOLDER)

第一张截图展示
pickFile选择中文名 PDF 后返回的缓存路径(/data/storage/el2/base/haps/entry/cache/file_dialog/兼职老师.pdf);
第二张展示pickDirectory拉起的系统目录选择器("选择路径"界面)。
八、遗留问题与改进方向
踩坑复盘
适配中遇到的实际问题最有价值,本次真机验证发现并修复了两个真实 bug:
| 踩坑点 | 现象 / 报错 | 根因与解法 |
|---|---|---|
| 坑 #1:saveFileToDirectory 写入失败 | 点击保存后无文件生成,日志报 OpenFileInner: OpenFile fail to SendRequest. err: 5、OpenByFileDataUri Access return false and failed to open file by Datashare error -1 | 根因:DocumentViewPicker.select() 返回的目录 URI 仅具临时只读权限,直接拼接 ${directory}/${fileName} 后 openSync 创建子文件不被授权。解法:改用 DocumentViewPicker.save() 系统保存对话框,并通过 DocumentSaveOptions.defaultFilePathUri 预填 pickDirectory 所选目录(对齐 Android ACTION_CREATE_DOCUMENT 流程) |
| 坑 #2:writeSync 报类型解析失败 | 日志报 file_api: [prop_n_exporter.cpp:802->WriteSync] Failed to resolve buf and options | 根因:Dart Uint8List 经通道解码为 Uint8Array,而 fileIo.writeSync 只接受 ArrayBuffer | string,typed array 被拒绝。解法:新增 toArrayBuffer(),按 byteOffset/byteLength 切片为独立 ArrayBuffer 再写入 |
已知问题
- iOS 专属参数不生效 —
OpenFileDialogParams中的dialogType、sourceType、allowEditing、allowedUtiTypes为 iOS 平台参数,OpenHarmony 平台忽略; - Android 专属参数不生效 —
mimeTypesFilter、localOnly为 Android 平台参数,OpenHarmony 平台忽略; - pickDirectory 依赖设备系统能力 — 目录选择依赖
SystemCapability.FileManagement.UserFileService.FolderSelection,仅具备该能力的设备(如二合一设备)支持;手机等设备上可能降级为文件选择(API 26 起手机已支持 FOLDER 模式,实测 ALN-AL00 可用); - saveFileToDirectory 会弹出确认对话框 — 与 Android/iOS 静默写入所选目录的行为不同,鸿蒙受限于目录 URI 只读授权,需用户确认保存位置。
未来优化
- 持久化授权 — 引入
fileShare.persistPermission(ohos.permission.FILE_ACCESS_PERSIST),在支持FolderAuthorization系统能力的设备上实现对所选目录的静默写入,完全对齐 Android 行为; - saveFile 大文件流式写入 — 当前用
writeSync一次性写入,超大文件可改为createStream流式写入避免内存峰值; - 文件重名处理 — 对齐 Android 的自动重命名与 iOS 的
onFileExists回调语义,完善文件已存在时的交互。
九、总结
将一个 Flutter 三方库适配到 OHOS 平台,核心路径可以概括为 三步走:
1. 找对应 ── 找到 OHOS 对每个 Android 原生 API 的等价实现(Intent → DocumentViewPicker,Activity → UIAbility)
2. 保契约 ── 确保方法通道名、方法名、返回值结构完全一致(flutter_file_dialog + 五个方法零改动)
3. 补缺口 ── 对于 OHOS 不提供的 API,用合理方案弥补(目录只读 → 保存对话框预填,Uint8Array → ArrayBuffer)
对于 flutter_file_dialog 三方库,适配新增 ohos/(7 个文件,核心实现 264 行 ArkTS)与 example/ohos/(39 个文件)工程,pubspec.yaml 仅增加 2 行 ohos 平台声明,Dart 层与其他平台代码完全不受影响——这正是 Flutter 跨平台三方库生态的魅力所在。
真机验证让适配质量有了质的提升:pickFile、pickDirectory、saveFile、saveFileToDirectory 四个核心路径全部在 OpenHarmony 7.0 真机上跑通,并发现、修复了两个模拟器/静态分析发现不了的运行时问题(目录 URI 只读授权、writeSync 类型解析),验证结论已同步至 README.OpenHarmony_CN/EN。
参考文档
更多推荐
所有评论(0)