Flutter 3.47 鸿蒙三方库适配实战:path_provider 应用目录兼容层
Flutter 3.47 鸿蒙三方库适配实战:path_provider 应用目录兼容层
一、项目信息
| 项目 | 内容 |
|---|---|
| 适配仓库名称 | path_provider |
| 原始包名 | path_provider |
| 适配版本 | 2.1.6-ohos.1 |
| 上游项目 | flutter/packages path_provider |
| 方法通道 | plugins.flutter.io/path_provider |
| OHOS 原生入口 | PathProviderPlugin |
| 构建产物 | entry-default-unsigned.hap |
path_provider 解决的是应用私有目录获取问题:缓存文件放哪里、用户文档放哪里、应用支持文件放哪里。它的 API 很小,但不同平台的目录语义并不完全相同,因此 OHOS 适配最重要的是把“可用目录”和“不存在等价概念的目录”区分清楚。
二、原库接口与 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.1
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 层。
四、为什么不模拟 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 示例首屏正常显示 |

截图来自 emulator 7.0.0.107,系统版本为 OHOS OpenHarmony-7.0.0.105。示例页面展示临时目录、文档目录、支持目录、库目录和缓存目录入口,说明通道已经注册;点击库目录等不支持入口时应由 Dart 层捕获平台错误。
七、把目录语义讲清楚,避免“能返回路径”就认为适配完成
目录 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。
欢迎加入 CPF-Flutter OpenHarmony 社区,交流 Flutter 三方库适配经验。
更多推荐



所有评论(0)