Flutter 3.47 鸿蒙三方库适配实战:path_provider 应用目录兼容层
Flutter 3.47 鸿蒙三方库适配实战:path_provider 应用目录兼容层
一、项目信息
| 项目 | 内容 |
|---|---|
| 适配仓库名称 | path_provider |
| 适配仓库地址 | https://atomgit.com/oh-flutter/path_provider |
| 原始包名 | path_provider |
| 适配版本 | 2.1.6-ohos.2 |
| 上游项目 | flutter/packages path_provider |
| 方法通道 | plugins.flutter.io/path_provider |
| OHOS 原生入口 | PathProviderPlugin |
| 构建产物 | entry-default-unsigned.hap |
path_provider 解决的是应用私有目录获取问题:缓存文件放哪里、用户文档放哪里、应用支持文件放哪里。它的 API 很小,但不同平台的目录语义并不完全相同,因此 OHOS 适配最重要的是把“可用目录”和“不存在等价概念的目录”区分清楚。

环境搭建与版本
下面是本次适配实际使用的环境。环境搭建过程这里不再展开,直接看 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 |
| 目标仓库 | path_provider |
二、原库接口与 OHOS 映射
顶层包继续导出原有函数:
Future<Directory> getTemporaryDirectory();
Future<Directory> getApplicationCacheDirectory();
Future<Directory> getApplicationSupportDirectory();
Future<Directory> getApplicationDocumentsDirectory();
Future<Directory> getLibraryDirectory();
Future<Directory?> getExternalStorageDirectory();
OHOS 插件只实现有明确系统对应关系的目录:
| Dart API | OHOS Context API | 结果 |
|---|---|---|
getTemporaryDirectory() | context.cacheDir | 返回临时目录 |
getApplicationCacheDirectory() | context.cacheDir | 返回应用缓存目录 |
getApplicationSupportDirectory() | context.filesDir | 返回应用支持目录 |
getApplicationDocumentsDirectory() | context.filesDir | 返回应用文档目录 |
getLibraryDirectory() | 无等价 API | 返回不支持 |
getExternalStorageDirectory() | 无 Android 风格公共目录 | 返回不支持 |
这里的 cacheDir 和 filesDir 是当前 SDK 中 common.Context 的属性,早期代码中使用的 getCacheDir()、getFilesDir() 在 ArkTS 编译阶段会直接报错。适配时应以 SDK 类型定义为准,而不是照搬 Android Java API 的方法名。
三、插件入口与生命周期
根 pubspec.yaml 增加 OHOS 声明:
version: 2.1.6-ohos.2
flutter:
plugin:
platforms:
ohos:
pluginClass: PathProviderPlugin
ohos/index.ets 导出 ArkTS 插件:
import PathProviderPlugin
from './src/main/ets/components/plugin/PathProviderPlugin';
export default PathProviderPlugin;
原生插件在 Engine 和 Ability 生命周期中维护 Context:
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.context = binding.getApplicationContext();
this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL);
this.channel.setMethodCallHandler(this);
}
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.context = binding.getAbility().context;
}
onDetachedFromAbility(): void {
this.context = null;
}
这种设计的好处是:目录查询发生在 Flutter 页面运行期间,插件始终使用当前 Ability 的应用 Context,而不会把失效的 Context 长期缓存在 Dart 层。
OHOS 工程目录创建
本次适配前,上游仓库没有 ohos/ 目录。适配分支上按 Flutter OH 的插件工程约束创建平台目录,再把 ArkTS 入口接入 pubspec.yaml:
git switch -c <your-ohos-adaptation-branch>
flutter create --platforms ohos .
工程生成后至少保留以下边界:
ohos/
├── index.ets
├── oh-package.json5
└── src/main/
├── module.json5
└── ets/components/plugin/
示例工程还需要生成 example/ohos/,并在插件 pubspec.yaml 的 flutter.plugin.platforms.ohos 中声明 pluginClass;只创建目录、没有注册入口,Dart 方法通道仍然不会工作。
四、为什么不模拟 Android 外部目录
Android 的 getExternalStorageDirectory() 通常返回应用专属外部目录,但 OHOS 的媒体文件、用户文件和应用沙箱采用不同的授权模型。把 filesDir 直接伪装成 getExternalStorageDirectory() 会让业务误以为拿到了用户可见的公共目录。
因此当前插件对这些方法返回明确错误:
case 'getStorageDirectory':
case 'getExternalCacheDirectories':
case 'getExternalStorageDirectories':
result.error(
'unsupported_on_ohos',
'OHOS does not expose Android-style external storage paths',
null,
);
return;
业务需要访问用户选择的文件时,应该使用 DocumentViewPicker 等系统选择器,而不是猜测外部目录路径。
五、业务代码示例
读取应用目录:
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
Future<void> printApplicationDirectories() async {
final Directory cache = await getApplicationCacheDirectory();
final Directory documents = await getApplicationDocumentsDirectory();
debugPrint('cache: ${cache.path}');
debugPrint('documents: ${documents.path}');
}
缓存下载文件:
import 'dart:io';
import 'package:path_provider/path_provider.dart';
Future<File> saveCacheFile(String content) async {
final Directory directory = await getTemporaryDirectory();
final File file = File('${directory.path}/download.tmp');
return file.writeAsString(content);
}
业务代码不需要增加 OHOS 分支。只要调用的是支持的 API,函数签名和返回的 Directory 对象与上游保持一致。
六、验证记录
验证命令:
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 |
| 模拟器安装启动 | 通过 |
| 页面状态 | Path Provider 示例首屏正常显示 |
版本与提交基线
| 项目 | 版本/地址 |
|---|---|
| 上游包 | path_provider |
| 上游最新版本 | 2.1.6 |
| 文章目标基线 | 2.1.6-ohos.2 |
| 当前已发布 TAG | 2.1.6-ohos.2 |
| 适配分支 | main |
| 目标最新 TAG | 2.1.6-ohos.2 |
| 最终提交 | 3905829 |
版本号更新只代表文章和适配分支的基线已对齐;涉及上游 API 变更时,仍需在该 TAG 上重新运行 Dart 测试、ArkTS 编译、HAP 安装和真机回归。
参考资料和仓库地址
- 适配仓库主页
- 适配分支 main
- 当前已发布 TAG 2.1.6-ohos.2
- 目标最新 TAG 2.1.6-ohos.2
- 最终提交 3905829
- ArkTS 通道实现
- 鸿蒙适配说明
- pub.dev 包页面
- pub.dev 2.1.6 版本页
- 上游仓库
- Flutter 官方插件与平台通道文档
- Flutter OH 鸿蒙 SDK 仓库
- HarmonyOS 开发者官方文档
欢迎加入 CPF-Flutter 鸿蒙社区。
适配过程 FAQ 与解决方案
为什么要区分文件、缓存和文档目录?
解决方案: OHOS 的应用沙箱、临时文件和用户文档有不同的生命周期及访问范围。统一返回一个路径会让业务误以为缓存不会被清理,或把私有数据暴露到用户文档区。
为什么不能把 Android 的路径规则套到 OHOS?
解决方案: OHOS 的目录命名和权限模型不同,部分 Android 习惯路径甚至不存在。适配层应通过 OHOS 公共能力获取目录,再把结果转换成 Dart 需要的字符串。
权限和沙箱边界如何验证?
解决方案: 分别验证应用私有目录读写、缓存清理、文档选择和重启后的路径稳定性。不能只看首次写入成功,还要确认文件在进程重启后仍能读取。
升级 SDK 后目录 API 变化怎么排查?
解决方案: 先对比 OHOS SDK 文档和 oh-package.json5 兼容版本,再检查插件 HAR 是否被宿主工程重新编译。只更新文章版本而不重新生成 HAR,运行时仍可能使用旧实现。
七、把目录语义讲清楚,避免“能返回路径”就认为适配完成
目录 API 看起来只是一个字符串返回值,但它背后包含三个容易混淆的问题:目录是否属于应用沙箱、系统是否允许清理、业务是否能把它当作用户可见目录。OHOS 适配必须把这三件事写进文档,而不是只展示一个路径字符串。
| Dart API | OHOS 当前实现 | 业务含义 | 是否适合长期保存 |
|---|---|---|---|
getTemporaryDirectory() | cacheDir | 可被系统清理的临时文件目录 | 否 |
getApplicationCacheDirectory() | cacheDir | 应用缓存目录 | 否,数据应可重建 |
getApplicationSupportDirectory() | filesDir | 应用内部支持文件 | 是,但属于应用私有数据 |
getApplicationDocumentsDirectory() | filesDir | 应用内部文档目录 | 是,但不等价于用户公共文档 |
getLibraryDirectory() | 未实现 | 无稳定等价目录 | 否 |
getExternalStorageDirectory() | 明确报错 | 无 Android 风格公共外部目录 | 否 |
getExternalCacheDirectories() | 明确报错 | 无对应多卷缓存目录 | 否 |
getExternalStorageDirectories() | 明确报错 | 无对应多卷外部目录 | 否 |
getDownloadsDirectory() | 未实现 | OHOS 公共下载目录需要另一套授权模型 | 否 |
1. 为什么 filesDir 不能直接当成“用户文档”
getApplicationDocumentsDirectory() 在 Android、iOS 和 OHOS 上都可以返回一个应用可写目录,但“应用可写”不等于“用户可以在文件管理器中看到”。如果业务把用户拍摄的照片、合同或录音直接写入 filesDir,用户可能无法通过系统文件应用找到它,也无法使用其他应用的分享入口。
因此,适合放入 filesDir 的内容是数据库、配置、离线缓存索引和应用生成的中间文件。真正需要用户可见、可分享的文件,应通过 DocumentViewPicker 让用户选择保存位置,或接入 OHOS 媒体文件能力。
2. 失败结果必须让 Dart 层看懂
OHOS 插件对不支持的 Android 风格目录返回统一错误码:
result.error(
'unsupported_on_ohos',
'OHOS does not expose Android-style external storage paths',
null,
);
Dart 侧不会把它伪装成 null,而是让调用方捕获平台异常。业务代码可以这样处理:
Future<void> loadExternalDirectory() async {
try {
final Directory? directory = await getExternalStorageDirectory();
debugPrint(directory?.path ?? 'unavailable');
} on MissingPlatformDirectoryException catch (error) {
debugPrint('directory unavailable: $error');
} on PlatformException catch (error) {
debugPrint('OHOS does not provide this directory: ${error.code}');
}
}
这里保留错误而不是返回一个伪造的路径,是为了让上层可以切换到系统文件选择器或显示明确提示。
3. 目录不存在时不要盲目创建
path_provider 返回的是系统目录,不负责创建业务子目录。业务层应该在需要时创建自己的文件夹,并处理权限和异常:
Future<File> writeReport(String content) async {
final Directory root = await getApplicationSupportDirectory();
final Directory reports = Directory('${root.path}/reports');
if (!await reports.exists()) {
await reports.create(recursive: true);
}
final File file = File('${reports.path}/latest.txt');
return file.writeAsString(content, flush: true);
}
使用 flush: true 可以让文件写入在返回前落盘,适合用户刚点击“保存”后立即退出应用的场景。对于大文件,应改为流式写入并把进度暴露给 UI,避免阻塞主线程。
4. 生命周期和 Context 的选择
插件在 Engine 绑定时先保存 Application Context,在 Ability 绑定时再替换为当前 Ability Context。这样即使 Flutter Engine 比某个 Ability 活得更久,也不会永久持有一个已经销毁的 UIAbility。
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.context = binding.getApplicationContext();
this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL);
this.channel.setMethodCallHandler(this);
}
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.context = binding.getAbility().context;
}
onDetachedFromAbility(): void {
this.context = null;
}
实际查询时先取出局部变量,再判断是否为空,避免异步过程中 Context 被置空:
private async handle(call: MethodCall, result: MethodResult): Promise<void> {
const context = this.context;
if (context === null) {
result.error(
'context_unavailable',
'No application context is attached',
null,
);
return;
}
// ...
}
5. 测试矩阵
目录适配不能只看 flutter test。建议至少覆盖以下层次:
| 层次 | 验证内容 |
|---|---|
| Dart 单元测试 | 平台接口返回路径时能包装为 Directory |
| Dart 单元测试 | 平台接口返回 null 时抛出 MissingPlatformDirectoryException |
| ArkTS 编译 | cacheDir / filesDir 属性在当前 SDK 中存在 |
| HAP 构建 | HAR 能被示例工程生成插件注册代码 |
| 模拟器 | 四个支持目录都能返回非空路径 |
| 模拟器 | 外部目录入口显示明确错误,而不是错误路径 |
| 真实设备 | 重启应用后业务子目录中的文件仍然存在 |
6. 常见排错
| 现象 | 原因 | 处理方式 |
|---|---|---|
getCacheDir is not a function | 使用了旧 SDK 方法名 | 改为 context.cacheDir |
getFilesDir is not a function | 使用了旧 SDK 方法名 | 改为 context.filesDir |
getLibraryDirectory 报错 | OHOS 无稳定等价 API | 业务改用 filesDir 或自定义目录策略 |
| 路径返回成功但文件管理器看不到 | 目录属于应用沙箱 | 使用系统选择器处理用户可见文件 |
| 重启后临时文件消失 | cacheDir 可能被系统清理 | 重要数据迁移到 filesDir 或业务数据库 |
| 示例外部目录按钮不可用 | 示例按 Platform.isAndroid 屏蔽 | OHOS 业务应明确捕获平台错误 |
八、限制与后续方向
- 当前只返回应用沙箱目录,不提供 Android 风格公共外部目录。
getLibraryDirectory()暂不实现,避免把filesDir错误映射为库目录。- 当前模拟器验证了 HAP 构建、安装和示例首屏;真实设备上的系统清理、备份策略和用户目录授权仍需业务项目验证。
- 如果后续 OHOS 提供稳定的用户目录 API,可以在原生插件中增加独立方法,不修改已有 Dart API。
仓库名称:path_provider
仓库地址:https://atomgit.com/oh-flutter/path_provider
欢迎加入 CPF-Flutter OpenHarmony 社区,交流 Flutter 三方库适配、应用目录和虚拟机验证经验。
更多推荐

所有评论(0)