给鸿蒙 App 增加用系统应用打开文件的能力 ------ open_app_file 的鸿蒙使用指南
应用内下载的 PDF、导出的报表、收到的日历邀请,用户点一下就该进对应的系统应用------自己实现文件预览既重又不专业。open_app_file 解决的就是这件事:给它一个应用沙箱内的文件路径,它负责把系统默认的查看应用拉起来。该库已完成 OpenHarmony 适配,仓库地址 https://atomgit.com/oh-flutter/open_app_file(TAG:4.0.5-ohos-1.0.0-beta.1)。读完本文,你能在鸿蒙 Flutter 应用里用一行调用打开图片、日历邀请等常见文件,并清楚每种返回结果的含义。
一、最终运行效果
先看结果。以下均来自 DevEco 模拟器(OpenHarmony 7.0.0.105 / API 26)实测截图。
调用 open() 打开一张 png 图片,系统图片预览器被拉起并显示图片:

图一:调用 open 后系统图片预览器显示 test.png
打开 .ics 日历邀请,系统日历应用被拉起(首次启动为隐私协议页):

图二:.ics 文件拉起系统日历应用
文件不存在时,不拉起任何应用,直接返回 fileNotFound:

图三:文件不存在时 Result: fileNotFound
扩展名无对应应用时(文件真实存在),系统弹出兜底对话框,返回值仍为 done:

图四:未知扩展名文件触发系统兜底对话框
二、open_app_file 是什么
open_app_file 是 pub.dev 上的一个 Flutter 插件(作者 yendoplan,BSD-3-Clause 协议,4.0.5),功能单一而专精:用系统默认应用打开应用有权限访问的本地文件。上游支持 Android、iOS、Web、Windows、Linux 与 macOS;鸿蒙版仓库已适配 OpenHarmony,接口签名、参数含义与返回值语义与 Android/iOS 完全一致,Dart API 无任何鸿蒙特有分支。
三、环境准备
鸿蒙 Flutter 开发环境(ohos 版 SDK、DevEco Studio、签名配置)的完整搭建步骤,直接照做官方指南:
本文实测环境:
| 项 | 版本 |
|---|---|
| Flutter(ohos 版) | 3.41.10-ohos-1.0.1 |
| 编译 SDK | 26.0.0(26) |
| DevEco Studio | 26.0.0(26.0.0.821) |
| 实测设备 | DevEco 模拟器(OpenHarmony 7.0.0.105 / API 26) |
运行示例工程需要一台已配置好调试签名的设备或模拟器(签名 profile 绑定 bundleName,示例工程为 com.example.demo)。
四、引入依赖
进入工程目录,在 pubspec.yaml 中添加 git 依赖:
yaml
dependencies:
open_app_file:
git:
url: https://atomgit.com/oh-flutter/open_app_file.git
# ref: 根据下方表格选择不同框架适配的 TAG 版本
ref: 4.0.5-ohos-1.0.0-beta.1
执行命令拉取依赖:
bash
flutter pub get
TAG 命名规则:原库版本-ohos-版本号-beta.x。
| Flutter 框架版本 | TAG 名称 | 分支名 |
|---|---|---|
| 3.41 | 4.0.5-ohos-1.0.0-beta.1 | feat/ohos_open_app_file_4.0.5 |
该 TAG 已在 3.41.10-ohos-1.0.1 搭配 DevEco 模拟器(OpenHarmony 7.0.0.105 / API 26)上实测通过。不同 TAG 之间的变更详见仓库中的 CHANGELOG.OpenHarmony.md。
五、代码接入
5.1 基础调用
场景:拿到一个沙箱内的本地文件路径,用系统默认应用打开它。
dart
import 'package:open_app_file/open_app_file.dart';
final result = await OpenAppFile.open('/path/to/file.png');
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
filePath |
String | 是 | 应用沙箱内的绝对路径 |
mimeType |
String? | 否 | 覆盖按扩展名推断的 MIME 类型(Android/鸿蒙生效) |
uti |
String? | 否 | iOS 专用 UTI,鸿蒙侧忽略 |
locate |
bool | 否 | macOS 在 Finder 中显示文件,鸿蒙侧忽略 |
返回值 OpenResult:
| ResultType | 值 | 语义 |
|---|---|---|
done |
0 | 已交由系统应用打开(或系统已接管) |
noAppToOpen |
-1 | 无应用可打开该文件 |
fileNotFound |
-2 | 文件不存在 |
permissionDenied |
-3 | 应用对该文件无读权限 |
error |
-4 | 其他错误,看 message |
message 字段携带人类可读的补充信息,失败场景先看它。
5.2 跨平台一套代码
鸿蒙侧的适配只做加法:通道名、方法名、参数、返回值与上游 Android/iOS 完全一致。你的业务代码不需要任何平台判断分支,同一份 OpenAppFile.open(filePath) 在 Android、iOS、鸿蒙上行为一致,在三端同时上架的应用里零成本复用。
5.3 实战场景:给下载详情页加一个「打开」按钮
一个贴近业务的完整片段:文件已下载到临时目录,详情页提供打开按钮,处理全部五种返回结果。
dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:open_app_file/open_app_file.dart';
import 'package:path_provider/path_provider.dart';
class DownloadDetailPage extends StatelessWidget {
final String fileName; // 例如 'report_2026q3.pdf'
const DownloadDetailPage({super.key, required this.fileName});
Future<void> _openFile(BuildContext context) async {
final dir = await getTemporaryDirectory();
final path = '${dir.path}/$fileName';
if (!await File(path).exists()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('文件不存在:$fileName')));
return;
}
final result = await OpenAppFile.open(path);
final String? tip;
switch (result.type) {
case ResultType.done:
tip = null; // 已拉起系统应用,无需提示
break;
case ResultType.fileNotFound:
case ResultType.permissionDenied:
tip = '无法访问文件,请重新下载';
break;
case ResultType.noAppToOpen:
tip = '没有找到可以打开此文件的应用';
break;
case ResultType.error:
tip = '打开失败:${result.message}';
break;
}
if (tip != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(tip)));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(fileName)),
body: Center(
child: ElevatedButton(
onPressed: () => _openFile(context),
child: const Text('用系统应用打开'),
),
),
);
}
}
两点提醒:文件必须先落到应用沙箱内(path_provider 的临时目录或文档目录都可以);鸿蒙上「扩展名无应用」的场景由系统兜底对话框接管并返回 done(见第七章),上面 noAppToOpen 分支更多覆盖权限与系统拦截场景,两者都处理才能把提示做全。
六、运行与验证
构建、安装、启动的完整命令序列(在 example 目录下):
bash
# 构建 hap(需在 example/ohos 工程配置好调试签名)
flutter build hap --debug
# 安装到模拟器(设备用 hdc list targets 确认)
hdc install -r build/ohos_app/outputs/default/entry-default-signed.hap
# 启动应用
hdc shell aa start -a EntryAbility -b com.example.demo
example 提供四个按钮与一个自由输入框,覆盖接口的全部返回路径。
验证 done :点击「Open sample image from assets」,图片复制进沙箱后调用 open(),系统图片预览器拉起并显示图片,返回应用后 Result 区显示 done:

图五:图片打开成功,Result: done
验证 fileNotFound :点击「Open non-existent file」,返回 fileNotFound,消息为 File asdf.qwert does not exist:

图六:文件不存在时 Result: fileNotFound
验证未知扩展名 :在输入框输入沙箱内真实存在的 .xyz 文件路径并点击「Open file」,系统弹出「暂无可用打开方式」兜底对话框,返回值 done:

图七:未知扩展名触发系统兜底后 Result: done
验证沙箱隔离 :输入框输入沙箱外路径(如 /data/local/tmp/foo.xyz,宿主侧真实存在),返回 fileNotFound------应用进程看不见沙箱外的文件系统。这是有意为之的安全边界:业务侧务必传入 path_provider 系列接口返回的沙箱内路径。
七、工作原理
整个调用链路如下:
text
Dart: OpenAppFile.open(filePath)
→ File.exists 预检(不存在直接返回 fileNotFound)
→ MethodChannel('open_app_file') / 'open_app_file'
→ ArkTS: OpenAppFilePlugin
→ fs.openSync 只读探测(无权限返回 permissionDenied)
→ fileUri.getUriFromPath 把路径转成 file:// URI
→ context.startAbility(隐式 Want)
→ 系统按 action + MIME 匹配文件查看器应用
→ 目标应用拿到临时读权限,打开文件
→ resolve → done / reject → 按错误码映射
两个使用方常踩的概念在此展开:
沙箱与路径 。鸿蒙应用运行在各自的沙箱里,getTemporaryDirectory() 对应 /data/storage/el2/base/cache,getApplicationDocumentsDirectory() 对应文档目录。插件打开文件的前提是应用自己对它有读权限,所以传入的路径必须在沙箱内;跨应用的文件共享走系统其他机制(如文件托管),不在这个插件的职责范围内。
隐式 Want 与系统兜底 。插件的 Want 携带 ohos.want.action.viewData 动作、file:// URI 与 MIME 类型,系统按这三项匹配声明了对应 skills 的查看器应用。当 MIME 是未知类型(如 */* 且无应用认领)时,鸿蒙系统弹出「暂无可用打开方式」的兜底对话框,startAbility 正常 resolve,返回 done------这与 Android 抛异常返回 noAppToOpen 不同,是平台行为差异而非缺陷,详见第六章图四与图七的实测。适配层的实现细节(Android Intent 语义如何逐项映射到鸿蒙 Want)见姊妹篇《open_app_file 的鸿蒙适配教程》。
八、常见问题
Q1:返回 done 但屏幕上什么都没发生?
先确认文件类型的系统处理方在当前设备上是否存在:模拟器生态比真机精简,部分类型可能没有预装查看应用;再确认扩展名与文件真实格式一致(把一个 zip 改名 .png 拉起预览器后可能无内容展示)。未知扩展名在鸿蒙上会弹系统兜底对话框,若连对话框都没出现,用 hdc shell hilog | grep OpenAppFilePlugin 看插件的错误日志。
Q2:路径怎么拿才稳妥?
统一走 path_provider:getTemporaryDirectory()、getApplicationDocumentsDirectory() 返回的路径一定在沙箱内且应用有读写权限。自己拼 /data/... 路径极易踩沙箱隔离与权限两道墙。
Q3:能直接传网络 URL 吗?
不能,接口只接受本地文件路径。先下载到沙箱(如临时目录)再调用;文件较小时 example 的做法可参考------资产文件 rootBundle.load 后 writeAsBytes 到临时目录。
Q4:真机上的表现和模拟器一致吗?
本文全部结论在 DevEco 模拟器(OpenHarmony 7.0.0.105 / API 26)上实测得出,未在真机验证;接口语义由系统标准能力承载(隐式 Want、fileUri),模拟器与真机遵循同一套框架协议。真机生态更全(更多预装查看应用),「暂无可用打开方式」兜底出现得更少,返回值语义不变。
九、结语与相关链接
一行调用、五种明确的结果语义、三端一致的业务代码------open_app_file 把"打开文件"这件小事在鸿蒙上补齐了。使用中发现问题:适配层的 Issue 请提到鸿蒙仓库,原库行为问题请提到上游仓库。
相关链接
欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:
- CPF-Flutter 鸿蒙社区
- Flutter OHOS 开发环境搭建指南
- open_app_file 鸿蒙版仓库
- 本文 TAG:
4.0.5-ohos-1.0.0-beta.1(分支feat/ohos_open_app_file_4.0.5) - example 源码目录
- 上游仓库
- pub.dev 包页
- 姊妹篇:《open_app_file 的鸿蒙适配教程》