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 APIOHOS 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 APIOHOS 当前实现业务含义是否适合长期保存
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 三方库适配经验。

Logo

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

更多推荐