给鸿蒙 App 增加用系统应用打开文件的能力 —— open_app_file 的鸿蒙使用指南

给鸿蒙 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 开发环境搭建指南

本文实测环境:

版本
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/cachegetApplicationDocumentsDirectory() 对应文档目录。插件打开文件的前提是应用自己对它有读权限,所以传入的路径必须在沙箱内;跨应用的文件共享走系统其他机制(如文件托管),不在这个插件的职责范围内。

隐式 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_providergetTemporaryDirectory()getApplicationDocumentsDirectory() 返回的路径一定在沙箱内且应用有读写权限。自己拼 /data/... 路径极易踩沙箱隔离与权限两道墙。

Q3:能直接传网络 URL 吗?

不能,接口只接受本地文件路径。先下载到沙箱(如临时目录)再调用;文件较小时 example 的做法可参考------资产文件 rootBundle.loadwriteAsBytes 到临时目录。

Q4:真机上的表现和模拟器一致吗?

本文全部结论在 DevEco 模拟器(OpenHarmony 7.0.0.105 / API 26)上实测得出,未在真机验证;接口语义由系统标准能力承载(隐式 Want、fileUri),模拟器与真机遵循同一套框架协议。真机生态更全(更多预装查看应用),「暂无可用打开方式」兜底出现得更少,返回值语义不变。

九、结语与相关链接

一行调用、五种明确的结果语义、三端一致的业务代码------open_app_file 把"打开文件"这件小事在鸿蒙上补齐了。使用中发现问题:适配层的 Issue 请提到鸿蒙仓库,原库行为问题请提到上游仓库

相关链接

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:

相关推荐
李游Leo2 小时前
HarmonyOS 7 系统能力深度实战 02:用模块化对象开放应用内部能力
harmonyos
HouWan2 小时前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios
HMS Core4 小时前
AI 赋能 Push Kit 场景化消息开发,高效完成鸿蒙应用推送能力接入
harmonyos
花先锋队长7 小时前
华为Mate XT2铰链防尘保养指南:如何让三折叠开合长久丝滑如初?
华为·智能手机·harmonyos
马剑威(威哥爱编程)7 小时前
【共创稿事节】HarmonyOS 7 图像超分实战:端侧 4 倍高清放大,数据不出设备
华为·harmonyos
silianpan7 小时前
CAD 文档预览 UTS 插件
android·ios·harmonyos
马剑威(威哥爱编程)7 小时前
【共创稿事节】HarmonyOS 7 空间音频实战:降噪、美化、变声、空间渲染的节点编排
华为·音视频·harmonyos
silianpan7 小时前
Office 文档预览 UTS 插件
android·微信小程序·harmonyos
Flutter_OH8 小时前
Flutter OHOS 如何使用多引擎 FlutterEngineGroup
harmonyos