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 平台。

项目地址https://atomgit.com/oh-flutter/flutter_file_dialog

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 通道注册
平台代码
AndroidMethodChannel(messenger, "flutter_file_dialog")
OHOSnew 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 实现
pickFileIntent(ACTION_OPEN_DOCUMENT) + 扩展名过滤 + 复制到缓存目录DocumentViewPicker.select() + fileSuffixFilters + 复制到缓存目录
pickDirectoryIntent(ACTION_OPEN_DOCUMENT_TREE)DocumentSelectOptions.selectMode = FOLDER
isPickDirectorySupportedBuild.VERSION.SDK_INT >= LOLLIPOP返回 true
saveFileIntent(ACTION_CREATE_DOCUMENT) + 后台复制DocumentViewPicker.save() + writeSync
saveFileToDirectoryDocumentFile.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(code 13900042)视为取消,统一返回 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.yamlohos 配置自动生成,会 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)
语言KotlinArkTS (TypeScript 语法)
插件接口FlutterPlugin + ActivityAware + MethodCallHandlerFlutterPlugin + AbilityAware + MethodCallHandler
通道注册MethodChannel(messenger, "flutter_file_dialog")new MethodChannel(binding.getBinaryMessenger(), "flutter_file_dialog")
拉起选择器Intent(ACTION_OPEN_DOCUMENT) + startActivityForResultDocumentViewPicker.select(options)
拉起保存器Intent(ACTION_CREATE_DOCUMENT)DocumentViewPicker.save(options)
目录选择Intent(ACTION_OPEN_DOCUMENT_TREE)selectMode = DocumentSelectMode.FOLDER
取消判定onActivityResultresultCode捕获 BusinessError code 13900042
写文件contentResolver.openOutputStreamfileIo.openSync + writeSync

4.2 关键 ArkTS 语法差异

Android 语法ArkTS 语法备注
import io.flutter.plugin.common.MethodChannelimport { MethodChannel } from '@ohos/flutter_ohos'OHOS 使用模块化导入
binding.activitybinding.getAbility()Activity → UIAbility
call.argument("key")call.argument('key')参数读取方式一致
result.success/error/notImplementedresult.success/error/notImplemented结果回调 API 一致
ByteArrayUint8Array二进制数据表示(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.DocumentViewPickerselect/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(),避免同类问题复发。


六、测试与验证

测试环境
项目版本
Flutter3.41.10-ohos-1.0.0
Dart3.11.5
HarmonyOS SDK5.1.0(18)(targetSdkVersion 26.0.0)
IDEDevEco Studio(内置 SDK 26.0.0)
设备 ROMALN-AL00 7.0.0.105(SP6C00E105R4P3),OpenHarmony 7.0.0.105(API 26)
验证要点
  1. 构建验证flutter build hap --debug 成功产出 entry-default-signed.hapflutter analyze 无错误;
  2. pickFile — 真机点击 Pick file,系统文件管理器弹出,选择中文名 PDF,返回缓存路径 /data/storage/el2/base/haps/entry/cache/file_dialog/兼职老师.pdf(279.6 KB),扩展名过滤、复制到缓存、中文文件名解码均正常;
  3. pickDirectory — 真机点击 Pick directory,目录选择器弹出,选中 Download,返回 file://docs/storage/Users/currentUser/Download
  4. saveFile — 真机点击 Save file,系统保存对话框弹出(“将文件保存至 Download”);
  5. saveFileToDirectory — 修复后保存对话框预填所选目录,确认保存返回 errorcode: 0
  6. 取消场景 — 选择器返回空数组 / 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: 5OpenByFileDataUri 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 再写入
已知问题
  1. iOS 专属参数不生效OpenFileDialogParams 中的 dialogTypesourceTypeallowEditingallowedUtiTypes 为 iOS 平台参数,OpenHarmony 平台忽略;
  2. Android 专属参数不生效mimeTypesFilterlocalOnly 为 Android 平台参数,OpenHarmony 平台忽略;
  3. pickDirectory 依赖设备系统能力 — 目录选择依赖 SystemCapability.FileManagement.UserFileService.FolderSelection,仅具备该能力的设备(如二合一设备)支持;手机等设备上可能降级为文件选择(API 26 起手机已支持 FOLDER 模式,实测 ALN-AL00 可用);
  4. saveFileToDirectory 会弹出确认对话框 — 与 Android/iOS 静默写入所选目录的行为不同,鸿蒙受限于目录 URI 只读授权,需用户确认保存位置。
未来优化
  • 持久化授权 — 引入 fileShare.persistPermissionohos.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 跨平台三方库生态的魅力所在。

真机验证让适配质量有了质的提升:pickFilepickDirectorysaveFilesaveFileToDirectory 四个核心路径全部在 OpenHarmony 7.0 真机上跑通,并发现、修复了两个模拟器/静态分析发现不了的运行时问题(目录 URI 只读授权、writeSync 类型解析),验证结论已同步至 README.OpenHarmony_CN/EN。


参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐