开发工具: 华为云码道

DevEco Studio: 26.0.0(26.0.0.851)

原始仓库: julien-kv/document_opener(GitHub)

本文配套仓库: oh-flutter/document_opener(AtomGit)

document_opener 只做一件事:把应用沙箱里的 PDF 或 CSV 文件交给系统查看器打开,并且不申请任何宽泛的媒体权限。它不解析文档内容,也不内置阅读器——真正的渲染由系统应用负责,插件只负责「校验 + 生成 URI + 把 URI 的临时读权限授予目标应用 + 发起查看」。本文以 document_opener 1.0.3 为例,介绍源码准备、OHOS 平台补全、ArkTS 实现和 example 真机运行验证。

在这里插入图片描述

插件目前支持 ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以提交 52b5c929e5709441836bbd43af93b9e7e0b8d135 为参考,对应 TAG 1.0.3。

在这里插入图片描述


一、插件简介与适配目标

document_opener 的公开 API 只有一个静态方法:

能力入口说明
打开文档DocumentOpener.open(filePath)只接受 .pdf 与 .csv,返回 DocumentOpenResult
结果模型DocumentOpenResult.type / .message类型为 DocumentResultType 枚举,消息为原生返回的字符串

DocumentResultType 有四个取值:done、error、noAppToOpen、permissionDenied。业务侧通常只需要判断 type == DocumentResultType.done,失败时把 message 展示给用户即可。

CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开

这个库的设计取舍值得先说明,因为它直接决定了鸿蒙适配的形态:

  1. 不做文件类型嗅探,只看扩展名。 _isValidFileType 判断字符串是否以 .pdf 或 .csv 结尾,不看文件头。
  2. 不申请存储权限。 Android 侧用 FileProvider 把应用私有目录下的文件映射成 content:// URI,再给目标应用授予一次性读权限;OpenHarmony 侧用 fileUri.getUriFromPath 生成 file:// URI,再用 FLAG_AUTH_READ_URI_PERMISSION 做同样的事。
  3. 原生侧只负责「交出去」。 文件打开之后的渲染、翻页、分享,全部由系统应用承担。因此插件返回值里的 done 语义是「已经成功移交给系统查看器」,不是「用户已经看完」。

需要强调第三点:done 只代表 startAbility 没有抛异常。用户是否真的看到内容,取决于系统里有没有能处理该 MIME 类型的应用,以及那个应用能否读到这个 URI。

本次适配的目标就是把上述语义在 OpenHarmony 上等价实现,同时保持 Dart 层 API、返回结构、错误分类完全不变。


二、环境准备

环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。

完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:

flutter --version
flutter doctor -v
hdc list targets

在这里插入图片描述
在这里插入图片描述

编辑用户

工程使用的工具链和 SDK 配置如下:

项目版本或配置用途
Flutter OHOS SDK3.47.7-ohos-1.0.0Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.47-devCPF-Flutter 对应开发分支
Dart SDK3.13.3Dart 语言与包管理环境(example 声明 sdk: ^3.13.3)
DevEco Studio26.0.0(26.0.0.851)工程 Sync、签名配置与 hvigor 构建
HarmonyOS 开发套件6.1.1(24)开发套件版本及对应的 API 级别
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
targetSdkVersion6.1.1(24)应用面向的行为版本
插件版本1.0.3pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物HAR被应用 entry 模块依赖
验证设备TLR-AL00,OpenHarmony-6.1.1.120,API 24,arm64-v8a真机运行与截图

2.1 开发套件版本与工程中的 SDK 版本配置

6.1.1(24) 和 24 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:

  • 6.1.1(24) 表示开发套件版本为 6.1.1,对应 API 24。
  • 本文 example/ohos/build-profile.json5 中 compatibleSdkVersion 与 targetSdkVersion 都写成 6.1.1(24),即「用 API 24 编译、以 API 24 为目标、最低兼容 API 24」。
  • 本机安装的 DevEco Studio 自带 SDK 为 API 26,编译时向下兼容 API 24 的工程;但运行时只看设备实际能力。

对应的 product 配置为:

{
  "name": "default",
  "signingConfig": "default",
  "compatibleSdkVersion": "6.1.1(24)",
  "targetSdkVersion": "6.1.1(24)",
  "runtimeOS": "HarmonyOS"
}

在这里插入图片描述

本工程没有显式写 compileSdkVersion,此时 hvigor 按 DevEco Studio 当前安装的 SDK 编译。不同 DevEco 版本对缺省行为的处理可能不同,需要固定编译 SDK 时再显式补上该字段。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。document_opener 的上游是 GitHub 上的 julien-kv/document_opener,许可证为 MIT(Copyright (c) 2024 Scanpay),版本 1.0.3。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。本次适配的起点是上游 a1c07db(Bump up version to 1.0.3)。

同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。document_opener 现可从 AtomGit 获取。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

git clone https://atomgit.com/oh-flutter/document_opener.git
cd document_opener
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

git clone 会创建 document_opener/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/、ohos/ 和 example/。这个库的仓库名与 Dart 包名一致,都是 document_opener,不需要额外传 --project-name 做名称映射。

请添加图片描述

图 1:在宿主机终端输入 AtomGit 仓库拉取命令并查看目录内容。

上游本身没有 ohos/ 目录,也没有 example/,只有 android/、ios/ 和 lib/。因此拉取后不能直接构建,需要先补全 OHOS 平台结构。

3.3 确认分支与版本标记

适配工作可以在 main 上直接完成,也可以先开分支。本次适配的提交 52b5c92 落在 main,并且已经与 origin/main 同步:

git branch --show-current
git tag --list
git log --oneline -3
git remote -v
git status --short --branch

请添加图片描述

图 2:确认当前分支、TAG、提交历史与双远端配置。

图 2 里有几个细节值得说明:

  • git tag --list 同时列出上游的 v1.0.3 和本次适配的 1.0.3-ohos-1.0.0-beta.1。前者是上游历史版本,后者是适配版本,两类标记并存是正常现象。
  • origin 指向 AtomGit 的 oh-flutter/document_opener,upstream 仍指向 GitHub 上游,便于后续 git fetch upstream 跟踪更新。
  • git status --short --branch 输出 ## main...origin/main,表示工作区干净且与远端一致。

如果按 PR 流程协作,建议改成先建分支再提交,分支名统一使用 feat/ohos_库名称_版本号,本例为 feat/ohos_document_opener_1.0.3:

git switch -c feat/ohos_document_opener_1.0.3
git branch --show-current

本次适配的 TAG 只在本地创建,尚未推送:

git tag -a 1.0.3-ohos-1.0.0-beta.1 -m "document_opener 1.0.3 OpenHarmony adaptation beta.1" 52b5c92
git push origin 1.0.3-ohos-1.0.0-beta.1

3.4 自动补全 OHOS 适配结构

仍在插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:

flutter create --template=plugin --platforms=ohos --org com.scanpay .
git status --short
git diff -- pubspec.yaml lib example
  • --template=plugin 指定插件模板。
  • --platforms=ohos 指定只补全 OHOS 平台。
  • --org com.scanpay 与上游 Android 侧的 com.scanpay.document_opener 包名保持一致。这一点很重要:如果这里换了组织名,pubspec.yaml 中 Android 的 package 与 ohos 的插件类会分属两套命名,后续排查通道问题会多一层干扰。
  • 最后的 . 表示在当前插件目录补全工程,不是另建一层 document_opener/。

该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异——本例生成后 pubspec.yaml 只增加了两行:

@@ -24,3 +24,5 @@ flutter:
        ios:
          pluginClass: DocumentOpenerPlugin
+      ohos:
+        pluginClass: DocumentOpenerPlugin

请添加图片描述

图 3:补全 OHOS 结构后,ohos/ 与 example/ohos/ 出现在未跟踪文件列表,pubspec.yaml 只增加两行。

如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:

cd example
flutter create --platforms=ohos .
cd ..

配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。

3.5 适配后的项目目录

适配后的关键目录如下:

document_opener/
├── lib/
│   └── document_opener.dart
├── ohos/
│   ├── index.ets
│   ├── oh-package.json5
│   ├── build-profile.json5
│   ├── hvigorfile.ts
│   └── src/main/
│       ├── module.json5
│       └── ets/components/plugin/
│           └── DocumentOpenerPlugin.ets
├── android/src/main/
│   ├── AndroidManifest.xml
│   ├── res/xml/file_paths.xml
│   └── kotlin/.../DocumentOpenerPlugin.kt
├── example/
│   ├── lib/main.dart
│   └── ohos/
├── README.OpenHarmony_CN.md
├── CHANGELOG.OpenHarmony.md
└── pubspec.yaml

请添加图片描述

图 4:适配后的项目结构。

与「lib/ 下多个文件 + 平台接口抽象」的插件不同,document_opener 的全部 Dart 逻辑都在单个 document_opener.dart(113 行)里,没有 *_platform_interface.dart、*_method_channel.dart 这一套分层。这是上游自己的选择,适配时不要为了「对齐模板」去改结构。

文件主要职责
lib/document_opener.dart公开 API、扩展名校验、平台分支判断、结果解析
DocumentOpenerPlugin.ets注册方法通道,校验路径与文件存在性,发起 startAbility
插件 module.json5声明 HAR 模块类型与设备类型(本插件不申请权限)
示例 entry module.json5声明宿主应用 Ability、设备类型与权限场景
example/lib/main.dart生成样例文件、触发四种打开场景、展示结果记录

四、Dart 接口与通道分析

OHOS 实现需要遵循 Dart 层已有的方法名、参数键和返回结构。先读 lib/document_opener.dart 的平台分支,再看 ArkTS 侧要补什么。

实际的调用关系只有一条链路:

Dart 入口或模型通道协议OHOS 实现应保持的行为
DocumentOpener.open(filePath)com.scanpay.document_opener / openDocumentfileUri.getUriFromPath + startAbility只接受 .pdf / .csv,成功移交返回 done
参数 file_pathMap<String, String>call.argument('file_path') as string键名必须与 Android 的 call.argument<String>("file_path") 一致
DocumentOpenResult原生返回 JSON 字符串JSON.stringify({type, message})Dart 侧执行 json.decode(result),不能返回对象
DocumentResultType 四枚举type 字段的字符串值done / error / noAppToOpen未知取值会被解析为 error

4.1 跨端架构与调用时序

这条链路没有事件流,只有一次性的方法调用:

非法

合法

JSON 字符串

Flutter 页面

DocumentOpener.open

扩展名校验 _isValidFileType

直接返回 error

MethodChannel com.scanpay.document_opener

ArkTS DocumentOpenerPlugin

fs.accessSync 校验存在

fileUri.getUriFromPath

startAbility viewData + FLAG_AUTH_READ_URI_PERMISSION

系统文件预览 / 第三方阅读器

4.1.1 一次完整打开的时序
OpenHarmony 系统 DocumentOpenerPlugin.ets DocumentOpener Flutter App OpenHarmony 系统 DocumentOpenerPlugin.ets DocumentOpener Flutter App open('/data/.../sample_report.pdf') _isValidFileType → true invokeMethod(openDocument, {file_path: ...}) 路径非空 → 扩展名 → fs.accessSync startAbility({action: viewData, uri: file://..., flags: AUTH_READ_URI}) 成功(或抛 16000001) '{"type":"done","message":"File opened successfully"}' DocumentOpenResult(done) 系统查看器接管渲染

4.2 平台分支:这次适配唯一需要改的 Dart 代码

理解这次适配的关键,是看清上游的平台判断写成了什么形状:

// 上游 1.0.3 的写法
if (!Platform.isIOS && !Platform.isAndroid) {
  // 桌面分支:macOS 走 open,Windows 走 cmd /c start,Linux 走 xdg-open
  ...
  } else {
    throw UnsupportedError("Unsupported platform");
  }
}

// iOS、Android 才走方法通道
final result = await _channel.invokeMethod('openDocument', {...});

这段逻辑的隐含假设是「非 iOS 非 Android 的平台 = 桌面平台」。OpenHarmony 不满足这个假设:它既不是 iOS/Android,也不是 macOS/Windows/Linux,于是会落入桌面分支,在三个 else if 都不匹配之后执行 throw UnsupportedError("Unsupported platform")。

这个异常在 Dart 层抛出,根本到不了方法通道,所以不会出现 MissingPluginException,也不会在 hilog 里留下任何插件日志——表现就是「点一下就报错,原生侧完全没收到调用」。

修复只需要放宽条件:

// Handle desktop platforms
// 注意:OpenHarmony 不是桌面平台,必须走下面的方法通道;
// 若不排除 Platform.isOhos,OHOS 会落入 desktop 分支并抛出 UnsupportedError。
if (!Platform.isIOS && !Platform.isAndroid && !Platform.isOhos) {
  ...
}

// For iOS, Android and OpenHarmony, use platform channel
try {
  final result = await _channel.invokeMethod('openDocument', {
    "file_path": filePath,
  });
  ...
}

整个 Dart 文件的改动是 2 行代码 + 2 行注释,公开 API 一个字符都没动。这也说明:跨平台插件的适配难度,往往不在「实现能力」,而在「找到那处把平台穷举成两类的假设」。

Platform.isOhos 由 Flutter OH 工具链提供。如果用了不带该扩展的官方 Flutter SDK,这个符号不存在,代码无法编译——这也是必须使用 OHOS 版 Flutter 的原因之一。

4.3 结果模型与 JSON 契约

原生侧返回的不是对象,而是字符串:

final result = await _channel.invokeMethod('openDocument', {
  "file_path": filePath,
});
final resultMap = json.decode(result) as Map<String, dynamic>;
return DocumentOpenResult.fromJson(resultMap);

这意味着 ArkTS 侧必须自己序列化。如果按直觉写成 result.success(payload) 传一个对象过去,Dart 侧 json.decode 会直接抛类型错误。ArkTS 里的写法因此固定为:

private createResult(type: string, message: string): string {
  const payload: Record<string, string> = { 'type': type, 'message': message };
  return JSON.stringify(payload);
}

_parseResultType 对未知 type 一律兜底为 error,所以即使将来原生侧新增了取值,Dart 层也不会崩,只是归类到 error。

4.4 permissionDenied 的现状

DocumentResultType.permissionDenied 在 Dart 层被定义、被 _parseResultType 解析,但没有任何原生实现会产生它:

  • Android 侧 DocumentOpenerPlugin.kt 只返回 error、noAppToOpen、done;
  • OpenHarmony 侧同样只返回这三个值。

它是上游留下的预留取值。实际使用中,如果系统真的拒绝了 URI 授权,你会拿到 error,message 来自系统异常。这一点在 README 里也有说明,适配时保持一致即可,不要为了「让四枚举都有出处」而自行发明返回值。


五、补全 OHOS 原生实现与工程配置

5.1 在 DocumentOpenerPlugin.ets 中实现原生能力

以 openDocument 为例:业务仍调用 DocumentOpener.open(path),Dart 通道仍发送同名命令。需要补全的是 DocumentOpenerPlugin.ets 中的对应分支:校验路径、生成 file URI、发起 startAbility,再把结果序列化成 JSON 字符串回传。

DocumentOpenerPlugin 同时实现 FlutterPlugin、MethodCallHandler 和 AbilityAware:

export default class DocumentOpenerPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private abilityContext: common.UIAbilityContext | null = null;
}

原生插件位于:

ohos/src/main/ets/components/plugin/DocumentOpenerPlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
  FlutterPlugin,
  FlutterPluginBinding,
  AbilityAware,
  AbilityPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import wantConstant from '@ohos.app.ability.wantConstant';
import common from '@ohos.app.ability.common';
import fileUri from '@ohos.file.fileuri';
import fs from '@ohos.file.fs';
import { BusinessError } from '@ohos.base';
import hilog from '@ohos.hilog';

其中:

  • FlutterPlugin 负责接入 Flutter Engine 生命周期;
  • MethodChannel 接收 Dart 发来的命令;
  • AbilityAware / AbilityPluginBinding 提供 UIAbility 上下文,startAbility 必须依赖它;
  • fileUri 把沙箱路径转成 file:// URI;
  • fs 用 accessSync 判断文件是否存在;
  • wantConstant.Flags 提供授权旗标常量;
  • BusinessError 用于读取错误码,hilog 用于原生侧诊断。
5.1.2 连接 Flutter Engine
onAttachedToEngine(binding: FlutterPluginBinding): void {
  try {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
    this.channel.setMethodCallHandler(this);
    hilog.info(DOMAIN, TAG, 'attached to engine, channel [%{public}s] registered', CHANNEL_NAME);
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'onAttachedToEngine failed, code: %{public}d, message: %{public}s',
      error.code, error.message);
  }
}

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  try {
    if (this.channel != null) {
      // 置空 handler,避免引擎销毁后插件实例仍被通道持有而无法释放
      this.channel.setMethodCallHandler(null);
      this.channel = null;
    }
    hilog.info(DOMAIN, TAG, 'detached from engine');
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'onDetachedFromEngine failed, code: %{public}d, message: %{public}s',
      error.code, error.message);
  }
}
5.1.3 取得 UIAbility 上下文

Android 侧用 ActivityAware 拿 Activity,OpenHarmony 侧用 AbilityAware 拿 UIAbilityContext。没有这个上下文就无法 startAbility,语义完全对应:

onAttachedToAbility(binding: AbilityPluginBinding): void {
  try {
    const ability: UIAbility = binding.getAbility();
    this.abilityContext = ability.context;
    hilog.info(DOMAIN, TAG, 'attached to ability');
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'onAttachedToAbility failed, code: %{public}d, message: %{public}s',
      error.code, error.message);
  }
}

onDetachedFromAbility(): void {
  // 释放对 UIAbility 上下文的引用,避免内存泄漏
  this.abilityContext = null;
  hilog.info(DOMAIN, TAG, 'detached from ability');
}

工程没有配置 Ability 场景或使用了错误的 Ability 类型(例如把插件挂在非 UIAbility 上),这一步就拿不到上下文,后续 openDocument 会返回 Ability is not available。

5.1.4 处理 MethodChannel 命令
onMethodCall(call: MethodCall, result: MethodResult): void {
  try {
    hilog.debug(DOMAIN, TAG, 'onMethodCall: %{public}s', call.method);
    if (call.method == METHOD_OPEN_DOCUMENT) {
      const filePath: string = call.argument(ARG_FILE_PATH) as string;
      this.handleOpenDocument(filePath, result);
    } else {
      result.notImplemented();
    }
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'onMethodCall [%{public}s] failed, code: %{public}d, message: %{public}s',
      call.method, error.code, error.message);
    result.error(ERR_CODE, 'Failed to handle ' + call.method + ': ' + error.message, error.message);
  }
}

result.notImplemented() 对应 Dart 侧的 MissingPluginException。如果方法名拼错,这里会走进 else 分支。

5.1.5 校验顺序与 Android 保持一致
private handleOpenDocument(filePath: string, result: MethodResult): void {
  if (filePath == null || filePath.length == 0) {
    result.success(this.createResult('error', 'File path cannot be null'));
    return;
  }

  if (!this.isValidFileType(filePath)) {
    result.success(this.createResult('error', 'Only PDF and CSV files are supported'));
    return;
  }

  let fileExists: boolean = false;
  try {
    fileExists = fs.accessSync(filePath);
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'access file failed, code: %{public}d, message: %{public}s',
      error.code, error.message);
    fileExists = false;
  }
  if (!fileExists) {
    result.success(this.createResult('error', 'File does not exist'));
    return;
  }

  if (this.abilityContext == null) {
    result.success(this.createResult('error', 'Ability is not available'));
    return;
  }

  this.startViewData(filePath, result);
}

五步顺序是刻意设计的:路径非空 → 扩展名合法 → 文件存在 → 上下文可用 → 发起调用。前一步不通过就不往下走,这样每种失败都有明确且唯一的 message,用户看到的提示能直接对应到问题所在。

fs.accessSync 在文件不存在时抛异常而不是返回 false,所以必须包在 try/catch 里,并在 catch 中把 fileExists 显式置为 false。这是从 Android 版本迁移过来时最容易踩的一个坑——Java 的 File.exists() 是返回布尔值,语义不同。

5.1.6 生成 URI 并交付给系统
private async startViewData(filePath: string, result: MethodResult): Promise<void> {
  const context: common.UIAbilityContext | null = this.abilityContext;
  if (context == null) {
    result.success(this.createResult('error', 'Ability is not available'));
    return;
  }

  try {
    // 生成 file:// URI,等价于 Android 的 FileProvider.getUriForFile
    const uri: string = fileUri.getUriFromPath(filePath);
    const mimeType: string = this.mimeTypeOf(filePath);

    const want: Want = {
      action: ACTION_VIEW_DATA,
      uri: uri,
      type: mimeType,
      // 把该 URI 的读权限临时授予目标应用,对齐 Android 的 FLAG_GRANT_READ_URI_PERMISSION
      flags: wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION,
    };

    await context.startAbility(want);
    hilog.info(DOMAIN, TAG, 'startAbility succeeded for [%{public}s]', uri);
    result.success(this.createResult('done', 'File opened successfully'));
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'startAbility failed, code: %{public}d, message: %{public}s',
      error.code, error.message);
    if (error.code == ERR_ABILITY_NOT_FOUND) {
      // 系统里没有能处理该 MIME 类型的应用,对应 Android 的 ActivityNotFoundException
      result.success(this.createResult('noAppToOpen', 'No app found to open this file type'));
    } else {
      result.success(this.createResult('error',
        error.message != null && error.message.length > 0 ? error.message : 'Unknown error occurred'));
    }
  }
}

这里是整个适配最需要讲清楚的一处差异:

维度AndroidOpenHarmony
URI 生成FileProvider.getUriForFile → content://fileUri.getUriFromPath → file://
授权方式FLAG_GRANT_READ_URI_PERMISSIONwantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION
拉起方式Intent.ACTION_VIEWaction: 'ohos.want.action.viewData'
无匹配应用ActivityNotFoundExceptionstartAbility 抛 16000001
需要 Provider 声明是(AndroidManifest.xml + file_paths.xml)否

Android 自 API 24 起禁止把 file:// 直接放进 Intent,必须走 FileProvider 换成 content://,否则会抛 FileUriExposedException。OpenHarmony 没有这条限制,插件直接给出 file:// 形式的应用沙箱 URI,再靠授权旗标把读权限临时借给目标应用:

file://com.scanpay.document_opener_example/data/storage/el2/base/haps/entry/cache/doc_opener_demoFQPOHV/sample_report.pdf

这条 URI 来自第八节抓取的真实 hilog。可以看到它包含 bundleName、el2/base/haps/entry/cache 这类沙箱层级——目标应用本来无权访问,能读到完全依赖 FLAG_AUTH_READ_URI_PERMISSION。因此这个旗标不是可选项,漏掉它 startAbility 依然会「成功」,但查看器会打开失败或显示空白。

mimeTypeOf 与 Android 的 when(extension) 分支一一对应:

private mimeTypeOf(filePath: string): string {
  const dotIndex: number = filePath.lastIndexOf('.');
  const extension: string = dotIndex >= 0 ? filePath.substring(dotIndex + 1).toLowerCase() : '';
  if (extension == 'pdf') {
    return 'application/pdf';
  }
  if (extension == 'csv') {
    return 'text/csv';
  }
  return '';
}

type 字段会影响系统如何挑选目标应用:application/pdf 会命中 PDF 阅读器,text/csv 会命中表格类应用。这也解释了第八节里为什么两个文件被交给了不同的系统界面。

5.2 权限:这个插件不需要任何权限声明

插件 ohos/src/main/module.json5 的完整内容只有模块声明:

{
  "module": {
    "name": "document_opener",
    "type": "har",
    "deviceTypes": [
      "default",
      "tablet"
    ]
  }
}

没有 requestPermissions。原因在于两条路径都不越界:

  • fs.accessSync 读取的是本应用沙箱内的路径,不需要权限;
  • startAbility 通过 URI 授权把访问权转交给目标应用,调用方同样不需要声明权限。

示例 entry 模块里仍然保留了脚手架自带的 ohos.permission.INTERNET:

"requestPermissions": [
  {"name" :  "ohos.permission.INTERNET"},
]

这个示例不联网,该权限实际未被使用。接入真实应用时按需保留或删除,它和 document_opener 的功能无关。

需要打开沙箱外的文件(例如「下载」目录)时,属于另一套授权模型:要么走系统文件选择器拿到 URI,要么按业务需要申请 ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY 这类目录权限。这部分不在插件职责范围内。

5.3 注册并导出插件

pubspec.yaml 通过以下配置声明 OHOS 插件类:

flutter:
  plugin:
    platforms:
      android:
        package: com.scanpay.document_opener
        pluginClass: DocumentOpenerPlugin
      ios:
        pluginClass: DocumentOpenerPlugin
      ohos:
        pluginClass: DocumentOpenerPlugin

插件的 ohos/index.ets 导出实现:

import DocumentOpenerPlugin from './src/main/ets/components/plugin/DocumentOpenerPlugin';
export default DocumentOpenerPlugin;

执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。

注册成功时,hilog 里会出现两行(第八节有完整抓取):

Adding plugin: DocumentOpenerPlugin
DocumentOpenerPlugin: attached to engine, channel [com.scanpay.document_opener] registered
DocumentOpenerPlugin: attached to ability

注册异常的排查步骤见第九节 MissingPluginException。

5.4 检查 example 的 OHOS 应用结构

本例的 example/ohos/build-profile.json5 在 products 中声明版本与签名配置:

{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "6.1.1(24)",
        "targetSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  }
}

signingConfigs 段落在本仓库里已经写入了本机 DevEco Studio 自动生成的证书路径与密钥口令。这部分内容不应进入公开仓库,提交前需要把 signingConfigs 整段移除,只保留 signingConfig: "default" 的引用;本机重新用 DevEco Studio 配置即可。第 6.2 节给出了检查命令,第 9.7 节说明了这个遗留问题。

配置后,在 DevEco Studio 中打开 example/ohos 执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。


六、补全交付文件并提交适配分支

6.1 除代码外还要补全哪些文件

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息:

文件应写清楚的内容
README.OpenSource上游名称、源码地址、适配版本、版权及许可证;本例为 MIT、julien-kv (Scanpay)
README.md原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息
README.OpenHarmony_CN.md简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题
README.OpenHarmony.md与中文说明对应的英文文档
CHANGELOG.OpenHarmony.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE保留上游 MIT 许可证原文
example/README.md依赖方式、运行目录、签名、操作步骤与效果图
pubspec.yaml、ohos/oh-package.json5核对包名、版本、插件注册、仓库地址、许可证和依赖
.gitignore忽略构建缓存及本机签名材料

CHANGELOG.OpenHarmony.md 里这一次写了四条实质变更,可以对照检查自己的适配是否漏项:

## 1.0.3-ohos-1.0.0-beta.1
- Beta official version.
- Add OpenHarmony platform support: a new `ohos` platform declaration in pubspec.yaml,
  an ArkTS plugin class `DocumentOpenerPlugin` implementing the `com.scanpay.document_opener`
  method channel, and a newly created `example/ohos` host project.
- Add the missing `Platform.isOhos` exclusion in the Dart platform check, otherwise the
  desktop branch would be wrongly taken on OpenHarmony and `UnsupportedError` would be
  thrown before reaching the method channel.
- `openDocument` is implemented with `fileUri.getUriFromPath` plus `startAbility`
  (action `ohos.want.action.viewData`, `FLAG_AUTH_READ_URI_PERMISSION`), and reports
  `noAppToOpen` on error code `16000001`, matching the Android `ActivityNotFoundException` branch.
- The public Dart API is unchanged.

注意 ohos/oh-package.json5 里的 license 字段写的是 Apache-2.0,而仓库 LICENSE 与 README.OpenHarmony_CN.md 都写 MIT。这是当前仓库的一处不一致,接入时以 LICENSE 文件的 MIT 为准,并建议把 oh-package.json5 的字段一并改成 MIT(见第 9.7 节)。

6.2 提交前检查

提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:

git branch --show-current
git diff --check
git status --short
git diff --stat
git diff

重点检查两件事:diff 里有没有本机路径与签名信息,文档里的版本和分支是否与提交内容一致。签名材料可以用下面的命令确认是否误入库:

git log -p --follow -- example/ohos/build-profile.json5 | grep -nE "keyPassword|storePassword|certpath"

有输出就说明签名口令已经进入历史记录,需要单独处理(重写历史或更换证书口令),仅删文件不足以消除泄露。

6.3 提交并推送到 AtomGit

文档和代码整理完成后,在根目录暂存并提交:

git add ohos pubspec.yaml lib example .gitignore
git add README.OpenHarmony_CN.md README.OpenHarmony.md
git add README.OpenSource CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git commit -m "feat: 完成 document_opener 插件 OpenHarmony 平台适配"
git remote -v
git branch --show-current
git push -u origin main

本次提交 52b5c92 的统计是 52 files changed, 1725 insertions(+), 2 deletions(-)——其中真正的业务代码只有 lib/document_opener.dart(2 行改动)和 DocumentOpenerPlugin.ets(243 行新增),其余都是脚手架、示例工程与文档。

推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。


七、使用根目录 example 演示接入

仓库自带 example/,可以直接用来调试插件和体验四种打开场景。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

dependencies:
  flutter:
    sdk: flutter
  document_opener:
    path: ../
  cupertino_icons: ^1.0.8

../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。

7.2 通过 AtomGit 引入插件

业务应用通过 AtomGit 引入时,将 path 配置替换为 Git 依赖。这里固定到本文使用的提交:

dependencies:
  document_opener:
    git:
      url: https://atomgit.com/oh-flutter/document_opener.git
      ref: 52b5c929e5709441836bbd43af93b9e7e0b8d135

使用自己的适配版本时,先推送分支或 TAG,再把 ref 改为 feat/ohos_document_opener_1.0.3 或 1.0.3-ohos-1.0.0-beta.1。

README.OpenHarmony_CN.md 里给的地址是 https://atomgit.com/CPF-Flutter/fluttertpc_document_opener,与本文仓库(oh-flutter/document_opener)不一致——组织名和仓库名都不同。按 README 复制会拉不到代码,详见第 9.7 节。

从插件根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lock 中 document_opener 的来源为 git(或 path),并核对 url、ref 和 resolved-ref。

7.3 示例页面做了什么

example/lib/main.dart(352 行)并没有让用户手选文件,而是在 initState 里现场造三个样例文件:

void _prepareSampleFiles() {
  final Directory dir = Directory.systemTemp.createTempSync('doc_opener_demo');
  _sampleDir = dir.path;
  _pdfPath = '${dir.path}/sample_report.pdf';
  _csvPath = '${dir.path}/sample_table.csv';
  _txtPath = '${dir.path}/sample_notes.txt';

  File(_pdfPath).writeAsStringSync('%PDF-1.4\n% demo pdf placeholder\n');
  File(_csvPath).writeAsStringSync('name,age\nAlice,30\nBob,25\n');
  File(_txtPath).writeAsStringSync('unsupported file type');
}

用 Directory.systemTemp 的好处是:文件天然落在应用沙箱内(真机上是 el2/base/haps/entry/cache),因此不需要任何存储权限就能读;也正因为走通了,才能验证「沙箱文件 + 授权旗标」这条路径。

四个按钮分别覆盖四种结果:

按钮目标文件预期结果拦截位置
Open PDFsample_report.pdfdone无拦截,交给系统预览
Open CSVsample_table.csvdone无拦截,交给系统表格视图
Unsupported .txtsample_notes.txterror:「Only PDF and CSV files are supported」Dart 层 _isValidFileType
Missing filenot_exists.pdferror:「File does not exist」ArkTS 层 fs.accessSync

这张表是本文验证部分的核心:四个按钮刻意让失败发生在不同层,从而能分别验证 Dart 侧拦截和原生侧拦截是否都按预期工作。

7.4 结果展示与状态说明

每次调用后,DocumentOpenResult 会被塞进一个本地列表,界面按「图标 + 标签 + type」展示;点击某条记录会弹出对话框,显示完整的 message。页面底部还固定列出 DocumentResultType 四个取值的含义。

这种设计对适配调试很友好:失败时不用连调试器,直接从 message 就能判断是哪一层返回的。


八、验证、构建与鸿蒙设备运行效果

8.1 分别验证插件与 example

从插件仓库根目录执行:

flutter pub get
flutter analyze
cd example
flutter pub get
flutter analyze

本次执行结果:

Analyzing document_opener...
No issues found! (ran in 5.5s)

Analyzing example...
No issues found! (ran in 5.1s)

flutter test 在本仓库无法执行,因为上游没有 test/ 目录:

$ flutter test
Test directory "test" not found.

这是上游的现状,不是适配引入的问题。适配时没有补充测试,也没有把 flutter test 写成「通过」。要补测试,可以先建 test/document_opener_test.dart,用 TestDefaultBinaryMessengerBinding 打桩通道,覆盖四个按钮对应的四条分支。

Dart 测试覆盖接口与解析逻辑,文件打开、URI 授权和系统查看器行为仍必须在鸿蒙设备上验证。

8.2 确认设备连接

hdc list targets
flutter devices

本次输出:

4UQ9K25508013016

设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。

8.3 配置签名

真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs;
  4. 为 default product 选择或生成签名;
  5. 确认设备、应用包名(com.scanpay.document_opener_example)、证书和 Profile 匹配;
  6. 再回到终端执行构建或安装。

签名材料保存在本机,公开仓库中只保留构建所需的通用配置。

8.4 构建与安装

本次直接用 DevEco 自带的 hvigor 构建(Windows 下需把 JBR 加进 PATH,否则打包阶段找不到 java):

cd example/ohos
export JAVA_HOME="/d/Program Files/Huawei/DevEco Studio/jbr"
export PATH="/d/Program Files/Huawei/DevEco Studio/jbr/bin:$PATH"
"/d/Program Files/Huawei/DevEco Studio/tools/hvigor/bin/hvigorw" \
  assembleHap -p product=default -p buildMode=debug --no-daemon

产物位于:

example/ohos/entry/build/default/outputs/default/

本次执行结果为 BUILD SUCCESSFUL in 19 s 399 ms(42 tasks:20 executed、22 up-to-date)。目录中同时有 entry-default-signed.hap 与 entry-default-unsigned.hap。本次产出文件约 145,042,523 字节(约 138 MB),体积主要来自 debug 版 Flutter 引擎,与插件代码无关。

安装并启动:

hdc install -r entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.scanpay.document_opener_example

8.5 在设备上测试四种打开场景

按下面的顺序操作,即可复现第八节的截图:

  1. 打开应用,确认首屏 Platform 显示 ohos、Sample directory 显示 created、Records 为 0;
  2. 点击 Open CSV,系统弹出「文件预览服务」隐私声明,同意后表格内容打开;
  3. 返回应用,记录区出现一条 CSV / type: done;
  4. 点击 Open PDF,系统文件预览打开 sample_report.pdf;
  5. 点击 Unsupported .txt,记录区出现 TXT / type: error;
  6. 点击 Missing file,记录区出现 Missing / type: error;
  7. 点开 Missing 记录,对话框显示 message: File does not exist;
  8. 点开 TXT 记录,对话框显示 message: Only PDF and CSV files are supported。

8.6 鸿蒙设备运行效果

首屏:Platform 一栏显示 ohos,这是 Platform.operatingSystem 在鸿蒙上的返回值。它能正常显示,本身就说明该应用跑在 OHOS 上并进入了插件链路——若沿用上游代码,点击按钮会在 Dart 层直接抛 UnsupportedError。

CSV 打开:首次调用会拉起系统「文件预览服务」并弹出隐私声明。这张提示框本身就是 startAbility 生效的证据——说明 URI 已被系统接受,且有应用响应 text/csv。同意后,sample_table.csv 在表格视图中打开,能看到 name/age 两列与 Alice 30、Bob 25 两行真实内容。

PDF 打开:sample_report.pdf 交给了系统文件预览。注意截图里显示的是文件卡片和 32 B 的体积,而不是 PDF 页面——因为示例写入的是 %PDF-1.4\n% demo pdf placeholder\n 这段占位文本,不是合法的 PDF 文件,预览器无法渲染页面。

这一点必须如实说明:本次验证证明了「URI 生成、授权、startAbility 移交」这条链路通畅,但没有验证真实 PDF 的渲染效果。要验证后者,把 _prepareSampleFiles 里写入的内容换成一个真实 PDF 的字节即可——插件侧代码不需要任何改动。

四条记录汇总:四个按钮各产生一条记录,PDF、CSV 为绿色 done,TXT、Missing 为红色 error。

失败消息:点开记录可以看到完整 message。Missing 的 File does not exist 来自 ArkTS 侧的 fs.accessSync;TXT 的 Only PDF and CSV files are supported 来自 Dart 侧——注意这两句话在 Dart 和 ArkTS 里各写了一份,文案完全相同,所以单看消息无法判断来源,需要配合下面的 hilog 证据。

结果类型说明:页面底部固定列出四个枚举取值的含义,其中 permissionDenied 在当前实现中不会出现(见 4.4 节)。

CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开 CSV 交给系统表格视图打开

图 10:DocumentResultType 四个取值的业务含义。

8.7 用 hilog 区分「哪一层拦下的」

只看界面无法判断失败发生在 Dart 还是 ArkTS。把日志级别调到 Debug 后抓取插件日志,三条链路就分得很清楚:

hdc shell hilog -b D
hdc shell hilog | grep DocumentOpenerPlugin

插件注册与一次成功调用:

Adding plugin: DocumentOpenerPlugin
DocumentOpenerPlugin: attached to engine, channel [com.scanpay.document_opener] registered
DocumentOpenerPlugin: attached to ability
DocumentOpenerPlugin: onMethodCall: openDocument
DocumentOpenerPlugin: startAbility succeeded for [file://com.scanpay.document_opener_example/data/storage/el2/base/haps/entry/cache/doc_opener_demoFQPOHV/sample_report.pdf]

点击 TXT 时:日志里没有任何 onMethodCall。说明 Dart 层在 _isValidFileType 就已经返回,请求根本没进入通道。

点击 Missing file 时:有 onMethodCall: openDocument,但没有 startAbility succeeded。说明请求进了原生层,被 fs.accessSync 拦下并返回 File does not exist。

三种情况合起来构成一张分层验证表:

场景onMethodCallstartAbility succeeded结论
Open PDF / CSV有有全链路走通,已移交系统
Unsupported .txt无无Dart 层拦截,未过通道
Missing file有无原生层拦截,未发起调用

这张表比界面截图更有说服力:它证明了「Dart 校验」和「原生校验」是两道独立的关卡,而不是重复判断。

抓日志时注意:hilog 启动会先回放一段时间的历史缓冲区。如果不清空就统计条数,会把上一个进程的日志算进来,得出「TXT 也调用了通道」的错误结论。本文的做法是先 aa force-stop 重启应用,再按时间戳和进程号筛选。

8.8 未在真机触发的分支

noAppToOpen 分支本次没有被触发。它只在 startAbility 抛 16000001(目标 ability 不存在)时返回,而当前设备装有系统文件预览服务,application/pdf 与 text/csv 都有应用响应。要验证这一分支,需要在一台卸载了所有 PDF/表格查看器的设备上测试,或临时把 mimeTypeOf 改成一个无人处理的类型(例如 application/x-unknown)制造「无匹配应用」的状态。

permissionDenied 同样没有被触发,原因见 4.4 节:没有原生实现会产生它。


九、FAQ:适配过程与使用问题

9.1 点击按钮直接抛 UnsupportedError: Unsupported platform

这是本次适配遇到的核心问题,也是所有「按平台二分」的插件在鸿蒙上最容易踩的坑。

现象:Dart 侧抛出 UnsupportedError,hilog 里找不到任何插件日志,onMethodCall 一次都没有。

原因:上游判断写成 if (!Platform.isIOS && !Platform.isAndroid),把「非 iOS 非 Android」等价于「桌面平台」。OpenHarmony 落进桌面分支后,三个 else if(macOS/Windows/Linux)都不匹配,走到最后的 throw UnsupportedError("Unsupported platform")。

处理:在条件里补上 && !Platform.isOhos,让 OHOS 与 iOS/Android 一起走方法通道(见 4.2 节)。

排查这类问题的通用手法:如果异常在 Dart 层抛出,通道日志一定为空。反过来,只要 hilog 里有 onMethodCall,就说明问题在原生侧。

9.2 MissingPluginException

这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用:

cd example
flutter clean
flutter pub get
flutter run -d <device-id>

如果仍然出现,检查自动生成的插件注册文件中是否包含 DocumentOpenerPlugin,同时核对 pubspec.yaml 的 ohos.pluginClass、ohos/index.ets 的导出和 ohos/oh-package.json5 的 main 字段。三者名字必须一致。

另外注意:pubspec.yaml 里 ohos 节点的缩进必须在 platforms 下与 android、ios 同级。缩进错一层,Flutter 工具就识别不到 OHOS 平台。

9.3 返回 done 但查看器打不开或显示空白

done 只代表 startAbility 没抛异常,不代表目标应用能读到文件。按以下顺序检查:

  1. want.flags 是否包含 wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION。少了它,URI 能传过去但读不到,表现是查看器空白或提示文件损坏;
  2. uri 是否由 fileUri.getUriFromPath 生成,而不是手工拼接字符串;
  3. type 是否与实际扩展名匹配。type 为空或与内容不符时,系统可能选中错误的查看器;
  4. 文件是否真的在应用沙箱内。沙箱外路径即使生成了 URI,授权也覆盖不到。

9.4 想打开「下载」目录里的文件

本插件按设计只处理沙箱内文件。打开沙箱外文件属于另一套授权模型:

  • 用系统文件选择器(@ohos.file.picker)让用户主动选文件,拿到临时授权的 URI,再把这个 URI 交给查看器;
  • 或者按业务需要申请 ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY 这类目录权限,并在 module.json5 中声明 reason 与 usedScene。

这两条路都会改动 Dart 层 API 语义(从「传路径」变成「传 URI」),不建议在适配层硬塞,最好作为独立能力评估。

9.5 系统里没有能打开 PDF 的应用

startAbility 会抛 16000001,插件捕获后返回 noAppToOpen。业务侧应把这个类型单独处理,提示用户「请先安装 PDF 阅读器」,而不是笼统地显示「打开失败」。

如果想在打开前就探测可用性,可以用 @ohos.bundle.bundleManager 的 queryAbilityInfo 按 want 查询匹配应用,但要注意这会增加一次跨进程查询,且查询结果与真正拉起之间仍存在竞争,不能替代对 noAppToOpen 的处理。

9.6 fs.accessSync 的行为与 Java 不同

Android 的 File.exists() 返回布尔值,OpenHarmony 的 fs.accessSync 在文件不存在时抛异常。直接写成 if (!fs.accessSync(path)) 会出现两种问题:文件不存在时异常向外冒泡,或者文件存在时返回值语义被误读。

正确写法是包在 try/catch 中,并在 catch 里显式置为 false(见 5.1.5 节)。

9.7 当前仓库的已知遗留问题

以下几项是本次适配后仍然存在的状态,如实列出:

  1. README.OpenHarmony_CN.md 的安装地址错误:写的是 https://atomgit.com/CPF-Flutter/fluttertpc_document_opener,实际仓库为 oh-flutter/document_opener,组织名与仓库名均不一致。
  2. TAG 版本表的「分支」列与实际不符:表中写 br_v1.0.3_ohos,而适配提交实际在 main 上,仓库中也没有该分支。
  3. 签名材料已入库:example/ohos/build-profile.json5 包含本机 certpath、keyPassword、storePassword 等字段,已随 52b5c92 进入版本历史。这是需要优先处理的安全问题。
  4. 许可证字段不一致:ohos/oh-package.json5 写 Apache-2.0,仓库 LICENSE 与中文 README 写 MIT。
  5. 上游没有 test/ 目录:flutter test 无法执行,本次未补充测试。
  6. permissionDenied 无产生方:Dart 层定义并解析,但 Android 与 OHOS 实现都不会返回它。
  7. noAppToOpen 未在真机验证:当前设备有系统预览服务,无法构造 16000001。
  8. PDF 只验证了移交,未验证渲染:示例写入的是占位文本而非合法 PDF。
  9. 设备类型声明不一致:插件 module.json5 为 ["default", "tablet"],示例 entry 为 ["phone"],不影响运行但建议统一。
  10. compatibleSdkVersion 与 targetSdkVersion 都固定为 6.1.1(24):这意味着 API 低于 24 的设备无法安装。要覆盖更老的设备,需要下调 compatibleSdkVersion 并在低版本设备上重新验证 fileUri 与 FLAG_AUTH_READ_URI_PERMISSION 的可用性。

9.8 改了本地 ArkTS,Demo 为什么没变化

先检查 example/pubspec.yaml:如果用的是 Git 依赖,Flutter 读取的是远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。

原生代码(.ets)变动后必须停止应用并重新构建安装,Dart 热重载不会重新编译 ArkTS。

9.9 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • 包名 com.scanpay.document_opener_example 与签名 Profile 不匹配;
  • 安装包的 compatibleSdkVersion 高于设备 API(本例是 24,设备也是 24,刚好满足);
  • 手机上已经安装了使用不同证书签名的同包名应用。

根据安装错误码区分签名、版本和包名冲突,再处理对应配置。


相关链接

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐