开发工具: 华为云码道

原始仓库: xyf1326131920-collab/project_utils

本文配套仓库: oh-flutter/project_utils

DevEco Studio: 26.0.0(26.0.0.851)

project_utils 是一个“三合一”的小工具集:ProjectUtils 取平台版本,KeepAliveWrapper 做子项状态缓存,ToastUtils 封装 BotToast 的提示框。三块能力里只有第一块过通道,另外两块是纯 Dart。本文以 project_utils 0.0.1 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

在这里插入图片描述

插件目前支持 ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以提交 d1a5150ece6d6bf95c6db5208fdcaed3704aba40 为参考,对应 TAG 0.0.1-ohos-1.0.0-beta.1。

在这里插入图片描述


一、插件简介与适配目标

这个库把三件互相独立的小事放在了一个包里:

  • ProjectUtils.getPlatformVersion():走 MethodChannel,返回当前系统的版本字符串;
  • KeepAliveWrapper:一个 StatefulWidget,内部混入 AutomaticKeepAliveClientMixin,让 ListView/TabBarView 里的子项在滚出屏幕或切页后保留状态;
  • ToastUtils:对 bot_toast 4.1.3 的静态封装,提供 showText、showSuccess、showError、showInfo、showLoadingText、hide 六个入口。

上游三个平台的 getPlatformVersion 写法一致,都是读系统版本号:

  • Android:Build.VERSION.RELEASE,返回 Android 14 之类;
  • iOS:UIDevice.current.systemVersion;
  • Windows/macOS/Linux:各自的原生版本字符串。

OHOS 侧的适配目标因此很明确:找一个语义相同的系统字段,让应用侧拿到的仍然是一段可读的系统版本。

系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置

与持续订阅型插件相比,本库的通道部分结构简单:没有 EventChannel,没有状态枚举,没有回调注册,也没有权限。Dart 层发起一次 invokeMethod,ArkTS 侧读一次设备信息回传即可。真正麻烦的部分不在“怎么取版本号”,而在没有原生实现时它以什么形式失败:


Future<String?> getPlatformVersion() async {
  final version =
      await methodChannel.invokeMethod<String>('getPlatformVersion');
  return version;
}

MethodChannelProjectUtils 没有写 Platform.isAndroid || Platform.isIOS 这类平台判断,任何平台都会直接调用通道。所以在鸿蒙上未注册插件时,得到的是 MissingPluginException;而示例页只捕获了 PlatformException:

try {
  platformVersion = await _projectUtilsPlugin.getPlatformVersion() ??
      'Unknown platform version';
} on PlatformException {
  platformVersion = 'Failed to get platform version.';
}

MissingPluginException 并不继承 PlatformException,这个 catch 接不住它,结果是异步异常被抛出、界面永远停在“获取中…”。这是适配前最典型的现象,FAQ 9.4 会把它拆开讲。

另外两块能力是纯 Dart:KeepAliveWrapper 只是 Flutter 框架的混入,ToastUtils 依赖的 bot_toast 也是纯 Dart 包。它们在鸿蒙上不需要任何原生改动,但接入方式有两个固定要求(MaterialApp.builder 和 navigatorObservers),漏了就完全不显示,第七节会说明。


二、环境准备

环境搭建参考社区文档: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-dev对应的开发分支
Dart SDK3.13.3Dart 语言与包管理环境
HarmonyOS 开发套件7.0.0(API 26)开发套件版本及对应的 API 级别
DevEco Studio26.0.0(26.0.0.851)工程同步、签名与 HAP 构建
compileSdkVersion26.0.0编译时使用的 SDK API
targetSdkVersion26.0.0应用面向的行为版本
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
插件版本0.0.1pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物HAR被应用 entry 模块依赖

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

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

  • 7.0.0(API 26) 表示 HarmonyOS 开发套件版本为 7.0.0,对应 API 26。
  • 26.0.0 是本文 HarmonyOS 应用工程中 compileSdkVersion 和 targetSdkVersion 的属性值。
  • 6.1.1(24) 是本文工程中 compatibleSdkVersion 的属性值,声明最低兼容 API 24。

对应的 product 配置为:

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

在这里插入图片描述

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 24。compatibleSdkVersion 写高了装机阶段就会被拒(9568297 older sdk version),而不是等到运行时报错,所以它要与真机 API 对齐后再定。


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

3.1 将上游源码同步到 AtomGit

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

project_utils 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。

上游仓库只有一次提交,LICENSE 是 TODO 占位。导入后第一件事是把这两点记进交付文档(第六节),而不是假设它已经声明了许可证。

3.2 将代码拉取到宿主机

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

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

git clone 会创建 project_utils/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。Git 仓库名是 project_utils,Dart 包名也是 project_utils。

需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:

git switch --detach d1a5150ece6d6bf95c6db5208fdcaed3704aba40

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

请添加图片描述

图 1:拉取上游仓库并查看提交历史。

3.3 在仓库根目录创建适配分支

接着在 project_utils/ 根目录创建分支。通用做法是分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本。本例为:

git switch -c feat/ohos_project_utils_0.0.1
git branch --show-current

本仓库的适配提交直接落在了 main 上(仓库只有一条主干,没有上游活跃开发分支需要隔离),版本边界由 TAG 标记:0.0.1-ohos-1.0.0-beta.1 指向适配完成后的提交 d1a5150。如果维护者要求走合并请求,可以按上面命令拉出特性分支,再把已有提交 cherry-pick 过去。

请添加图片描述

图 2:分支、TAG 与远端配置。

3.4 自动补全 OHOS 适配结构

分支确定后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:

flutter create --template=plugin --platforms=ohos --project-name project_utils .
git status --short
git diff -- pubspec.yaml lib example
  • --template=plugin 指定插件模板。
  • --platforms=ohos 指定需要补全的平台。
  • --project-name project_utils 使用 Dart 包名,与目录名一致时也不能省,避免模板按目录名猜错。
  • 最后的 . 表示在当前插件目录补全工程,不是另建一层 project_utils/。

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

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

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

配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos project_utils;已有插件使用上面的 . 在当前目录补全。

3.5 适配后的项目目录

适配后的关键目录如下:

project_utils/
├── lib/
│   ├── project_utils.dart
│   ├── project_utils_platform_interface.dart
│   ├── project_utils_method_channel.dart
│   ├── project_utils_web.dart
│   ├── keep_alive_wrapper.dart
│   └── print/toast_utils.dart
├── ohos/
│   ├── index.ets
│   ├── oh-package.json5
│   └── src/main/
│       ├── ets/components/plugin/ProjectUtilsPlugin.ets
│       └── module.json5
├── example/
│   ├── lib/main.dart
│   └── ohos/entry/
├── test/
└── pubspec.yaml

请添加图片描述

图 3:适配后的目录结构(行数为实际统计)。

文件主要职责
lib/project_utils.dart为业务应用提供最简入口
lib/project_utils_platform_interface.dart定义平台无关接口与 PlatformInterface token 校验
lib/project_utils_method_channel.dart实现 MethodChannel 通信
lib/keep_alive_wrapper.dart纯 Dart 组件,AutomaticKeepAliveClientMixin 封装
lib/print/toast_utils.dart纯 Dart 组件,BotToast 六个静态入口
ProjectUtilsPlugin.ets注册 Flutter 通道并读取 deviceInfo.osFullName
插件 module.json5声明 HAR 模块与设备类型
示例 entry module.json5声明宿主应用 Ability、设备类型
example/lib/main.dart双 Tab 演示:平台版本 + KeepAlive + 五类 Toast

四、Dart 接口与通道分析

OHOS 实现需要遵循 Dart 层已有的方法、参数和回调约定。先阅读 lib/project_utils.dart、lib/project_utils_platform_interface.dart 和 lib/project_utils_method_channel.dart,再在 ohos/src/main/ets/components/plugin/ProjectUtilsPlugin.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
ProjectUtils.getPlatformVersion()project_utils / getPlatformVersiondeviceInfo.osFullName返回可读的系统版本字符串,失败时回传错误码与消息
KeepAliveWrapper无通道无需原生实现Flutter 框架自身能力,与其他平台一致
ToastUtils.*无通道无需原生实现依赖 bot_toast 的 Overlay 实现,与其他平台一致

原生端只需要保持方法名与通道名一致。设备信息属于公共基础能力,读取失败时通过错误回调通知 Dart 层即可。

4.1 跨端架构与调用时序

Flutter 侧和 HarmonyOS 侧之间只有一条通道:

  1. MethodChannel('project_utils'):负责发送 getPlatformVersion 请求并接收返回字符串。

Flutter 页面

ProjectUtils

ProjectUtilsPlatform

MethodChannel project_utils

ArkTS ProjectUtilsPlugin

deviceInfo.osFullName

KeepAliveWrapper 纯 Dart

ToastUtils 纯 Dart

KeepAliveWrapper 与 ToastUtils 不上通道,它们的虚线连接表示“同属一个库、但走框架自身的 Widget 与 Overlay 机制”,与原生侧无关。

4.1.1 一次取值的时序

没有订阅、没有事件流,一次调用一次返回,页面只需要处理 Future 的完成与异常。

4.2 返回值模型:一个字符串,没有枚举

与返回状态枚举的接口不同,本库没有状态码、没有枚举、没有兜底值。getPlatformVersion() 的返回类型是 Future<String?>,原生返回什么,业务就拿到什么。

这也意味着“返回空字符串”和“返回一段奇怪的字符串”在类型上都合法,只有内容能判断对错。适配阶段因此要额外核对取到的字段是否带平台名前缀、版本号是否与系统设置里显示的一致——本文实测的取值出现在第八节,也正是这一步发现了双前缀问题。

4.3 公开 API 与平台接口

ProjectUtils 是对外入口,ProjectUtilsPlatform 定义平台接口,MethodChannelProjectUtils 是默认实现。业务通过入口调用,测试中也可以替换平台实现:

class ProjectUtils {
  /// 获取平台版本
  Future<String?> getPlatformVersion() {
    return ProjectUtilsPlatform.instance.getPlatformVersion();
  }
}

平台接口使用 plugin_platform_interface 的 token 校验,只有拿得到同一个 _token 的子类才能替换默认实现:

abstract class ProjectUtilsPlatform extends PlatformInterface {
  ProjectUtilsPlatform() : super(token: _token);

  static final Object _token = Object();

  static ProjectUtilsPlatform _instance = MethodChannelProjectUtils();

  static ProjectUtilsPlatform get instance => _instance;

  static set instance(ProjectUtilsPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }
}

单元测试里替换实现时,继承 ProjectUtilsPlatform 并调用父类构造即可拿到 token;用 implements 实现的假类会被 verifyToken 拒绝。

4.4 Dart 通道协议分析

4.4.1 通道名称必须两端完全一致
final methodChannel = const MethodChannel('project_utils');
this.channel = new MethodChannel(binding.getBinaryMessenger(), 'project_utils');

通道名称属于跨语言协议。Dart 和 ArkTS 任何一端拼写不一致,都会出现“方法未实现”或收不到返回的问题。这个库的通道名与包名相同,改动时容易顺手一起改,需要分开对待。

4.4.2 方法名与参数

通道上只有一个方法 getPlatformVersion,无参数。ArkTS 侧用 call.method 做字符串比较,其他方法统一走 result.notImplemented(),让 Dart 侧得到明确的“未实现”而不是超时。

4.4.3 错误回传

Dart 侧没有包一层 try/catch,所以任何原生错误都会以 PlatformException 的形式抛给业务。示例页的 on PlatformException 正是为此准备的。这里有一个容易忽略的边界:未注册插件抛的是 MissingPluginException,它不继承 PlatformException,所以这段 catch 接不住“插件没注册”这种情况,界面会停在初始文案。

第六节之后的构建流程里,GeneratedPluginRegistrant.ets 是否包含 ProjectUtilsPlugin 是判断“注册成功”的唯一依据,不能只看 pubspec.yaml 写没写。


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

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

以 getPlatformVersion 为例:业务仍调用 ProjectUtils.getPlatformVersion(),Dart 通道仍发送同名命令。需要补全的是 ProjectUtilsPlugin.ets 中的对应分支:读取 deviceInfo.osFullName,通过 result.success 回传;异常时通过 result.error 把错误码和消息传回 Dart。这样业务页面沿用原有接口即可拿到 OHOS 的系统版本。

ProjectUtilsPlugin 实现 FlutterPlugin 和 MethodCallHandler。下面列出类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口。

原生插件位于:

ohos/src/main/ets/components/plugin/ProjectUtilsPlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import deviceInfo from '@ohos.deviceInfo';
import { BusinessError } from '@ohos.base';

其中:

  • FlutterPlugin 负责接入 Flutter Engine 生命周期;
  • MethodChannel 接收 Dart 发来的命令并回传结果;
  • deviceInfo 提供 osFullName 等系统信息字段;
  • BusinessError 用于读取 HarmonyOS 异常码和异常消息。

这个库不需要 hilog:通道只有一个方法,返回值和异常都已经通过 MethodResult 回到 Dart,原生侧再加日志反而重复。排查阶段确实需要看原生日志时,可以临时加 hilog.info,验证完再决定是否保留。

5.1.2 连接 Flutter Engine
private channel?: MethodChannel;

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.channel = new MethodChannel(binding.getBinaryMessenger(), 'project_utils');
  this.channel.setMethodCallHandler(this);
}

getUniqueClassName() 返回 'ProjectUtilsPlugin',供 Flutter 侧注册表识别。

5.1.3 读取系统版本
onMethodCall(call: MethodCall, result: MethodResult): void {
  try {
    if (call.method === 'getPlatformVersion') {
      // osFullName already includes the platform name, e.g.
      // "OpenHarmony-6.1.1.120"; prepending "OpenHarmony " again used to
      // yield "OpenHarmony OpenHarmony-…". Android returns
      // "Android ${Build.VERSION.RELEASE}" the same way — the runtime
      // supplies the release string, no prefix is concatenated twice.
      result.success(deviceInfo.osFullName);
    } else {
      result.notImplemented();
    }
  } catch (e) {
    result.error('PLATFORM_VERSION_ERROR', describeError(e), null);
  }
}

这里记录了一次真实的返工。首版写成 result.success(\OpenHarmony ${deviceInfo.osFullName}`),理由是“对齐 Android 的 Android 格式”。但在鸿蒙上osFullName本身就已经是OpenHarmony-7.0.0.105(第八节有真机截图),再拼一次前缀,界面显示成 OpenHarmony OpenHarmony-7.0.0.105`。

正确的理解是:Android 的 "Android " + RELEASE 里,前缀是 Kotlin 代码拼的,因为 RELEASE 只有 14;而 OHOS 的 osFullName 已经是完整名字,Dart 侧不需要再补任何东西。

5.1.4 把 BusinessError 解成可读文本
/**
 * BusinessError interpolates as "[object Object]" with a plain template
 * string; unpack code and message explicitly.
 */
function describeError(e: Object): string {
  const err = e as BusinessError;
  if (err && typeof err.code === 'number' && typeof err.message === 'string') {
    return `(${err.code}) ${err.message}`;
  }
  return `${e}`;
}

catch (e) 拿到的是 Object 类型,用它做模板字符串插值时,HarmonyOS 的 BusinessError 会变成 [object Object],错误码和消息全部丢失。首版正是 ${e} 直接透传,Dart 侧只看到一行没有信息量的报错。改成先按 BusinessError 拆出 code 和 message,拿不到字段时才退回原始字符串。

5.1.5 Engine 解绑时释放资源
onDetachedFromEngine(_binding: FlutterPluginBinding): void {
  if (this.channel) {
    this.channel.setMethodCallHandler(null);
  }
}

Flutter Engine 销毁时解除 Handler,避免通道对象被继续持有。这个库没有系统监听需要清理,所以解绑逻辑只有一行。

5.2 声明插件和宿主权限

本插件无需申请任何系统权限:deviceInfo.osFullName 属于公共基础系统能力,设备型号、系统版本这类信息不涉及用户隐私授权。KeepAliveWrapper 与 ToastUtils 都是纯 Dart,同样没有权限要求。

5.2.1 插件 HAR 的模块声明

ohos/src/main/module.json5 只需要声明模块信息,不需要 requestPermissions:

{
  "module": {
    "name": "project_utils",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}
5.2.2 应用 entry 的配置

宿主应用不需要合并权限条目。example/ohos/entry/src/main/module.json5 保持模板生成的状态即可,deviceTypes 为 ["phone"],mainElement 为 EntryAbility。

5.3 注册并导出插件

pubspec.yaml 通过以下配置声明 OHOS 插件类,与 android、ios 等平台并列:

flutter:
  plugin:
    platforms:
      ohos:
        pluginClass: ProjectUtilsPlugin

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

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

执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。本例生成的 example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 中包含:

flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
flutterEngine.getPlugins()?.add(new ProjectUtilsPlugin());

通常不应手工编辑这个文件,因为下次构建可能覆盖它。注册异常的排查步骤见第九节 MissingPluginException。

请添加图片描述

图 4:pubspec.yaml 的 ohos 注册项与 ArkTS 侧的取值位置。

5.4 检查 example 的 OHOS 应用结构

本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;提交版把 signingConfigs 置空,签名材料只保留在本机,装机时再从备份恢复。

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "compatibleSdkVersion": "6.1.1(24)",
        "targetSdkVersion": "26.0.0",
        "runtimeOS": "HarmonyOS"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

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

本例的 AppScope/app.json5 中 bundleName 沿用模板的 com.example.color_system_example,与其他示例工程相同。这意味着在同一台设备上装多个示例时会互相覆盖——第八节截图前先确认设备上跑的确实是本库的 Demo,必要时用 hdc install -r 重装。


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

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

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:

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

README.OpenSource 记录库本身的来源与版本。本例的包名为 project_utils,版本为 0.0.1;上游 LICENSE 是 TODO 占位,没有声明许可证,文档里如实写成 Unknown (upstream LICENSE is a TODO placeholder),并在 README 末尾提示商用前先与作者确认授权,而不是替上游补一个许可证。

ohos/oh-package.json5 里的 license 保留了脚手架生成的 Apache-2.0,与“上游未声明许可证”并不一致。这个字段只影响 HAR 的元数据描述,不影响构建与运行;文档层面以 README.OpenSource 为准,它明确写着 Unknown (upstream LICENSE is a TODO placeholder)。要彻底消除这处不一致,需要先与上游作者确认许可证,再同步修改 oh-package.json5 与 README.OpenSource,而不是单方面补一个。

6.2 提交前检查

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

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

检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。本例在这一步发现了 example/ohos/build-profile.json5 里的本机证书路径与口令,改为 signingConfigs: [] 后重新提交。

6.3 提交并推送到 AtomGit

文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:

git add ohos pubspec.yaml example test .gitignore analysis_options.yaml
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for project_utils 0.0.1"
git remote -v
git branch --show-current
git push -u atomgit main

本仓库的推送目标叫 atomgit(指向 git@atomgit.com:oh-flutter/project_utils.git),origin 指向上游 GitHub,两者不要混用。分支是 main,不是 master——写 README 的版本表之前先 git branch --show-current 确认一次。

版本边界用注释 TAG 标记,并单独推送:

git tag -a 0.0.1-ohos-1.0.0-beta.1 -m "Release 0.0.1-ohos-1.0.0-beta.1: OpenHarmony platform support"
git push atomgit 0.0.1-ohos-1.0.0-beta.1

TAG 名要和 README 里 ref: 写的完全一致。本例早期曾打过一次 v0.0.1,与文档里的 0.0.1-ohos-1.0.0-beta.1 对不上,按 ref: 拉取会直接失败;正确做法是删掉旧名、按文档重建,而不是反过来改文档。

推送后如需在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。


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

仓库自带 example/,可以直接用来调试插件和体验三块能力:平台版本读取、ToastUtils 五类提示、KeepAliveWrapper 状态缓存。

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

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

dependencies:
  flutter:
    sdk: flutter
  project_utils:
    path: ../

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

7.2 通过 AtomGit 引入插件

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

dependencies:
  flutter:
    sdk: flutter
  project_utils:
    git:
      url: https://atomgit.com/oh-flutter/project_utils.git
      ref: 0.0.1-ohos-1.0.0-beta.1

使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_project_utils_0.0.1。正式发布后可固定到 tag 或 commit。

从插件根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lock 中 project_utils 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。

7.3 调用接口实现演示页

下面的页面结构来自 example/lib/main.dart(完整文件 246 行,含说明卡片与卡片列表,这里省略样式细节),两个 Tab 分别演示平台版本与 KeepAlive:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:project_utils/keep_alive_wrapper.dart';
import 'package:project_utils/print/toast_utils.dart';
import 'package:project_utils/project_utils.dart';

void main() {
  runApp(const ProjectUtilsApp());
}

class ProjectUtilsApp extends StatelessWidget {
  const ProjectUtilsApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ProjectUtils Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      builder: ToastUtils.appBuilder(),
      navigatorObservers: ToastUtils.navigatorObservers(),
      home: const HomePage(),
    );
  }
}

class _HomePageState extends State<HomePage> {
  final _projectUtilsPlugin = ProjectUtils();
  String _platformVersion = '获取中…';
  int _callCount = 0;

  
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future<void> initPlatformState() async {
    String platformVersion;
    try {
      platformVersion = await _projectUtilsPlugin.getPlatformVersion() ??
          'Unknown platform version';
      _callCount++;
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }

    if (!mounted) return;
    setState(() => _platformVersion = platformVersion);
  }
  // build 中为 DefaultTabController + TabBarView:
  //   平台信息:版本卡片、重新获取按钮、五类 Toast 按钮、说明卡片
  //   KeepAlive 演示:20 张 _CounterCard,每张用 KeepAliveWrapper 包住
}

KeepAlive 页的每张卡片外层是库里的 KeepAliveWrapper,内部是自身 State 持有计数的 _CounterCard:

ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: 20,
  itemBuilder: (_, index) {
    return KeepAliveWrapper(
      key: ValueKey('item_$index'),
      child: _CounterCard(index: index),
    );
  },
)

7.4 Toast 与 KeepAlive 的接入要求

两者都不需要原生改动,但各有固定前提:

  • Toast:MaterialApp.builder 必须包上 ToastUtils.appBuilder(),navigatorObservers 必须带上 ToastUtils.navigatorObservers()。漏掉前者 Overlay 不存在,所有提示静默不显示;漏掉后者,跨页面显示的提示在路由变化时不会被正确关闭。
  • KeepAlive:KeepAliveWrapper 只能放在懒加载列表或可切换视图里(ListView.builder、PageView、TabBarView),且子项本身必须是 StatefulWidget。子项写成无状态组件时,“状态保留”无从体现。

ToastUtils.showText 还有一处上游实现细节值得注意:

static showText(String? loadingText, {VoidCallback? onClose}) {
  if (loadingText == null || loadingText == "重复点击") {
    return;
  }
  return _showToast(loadingText, _ToastType.text, onClose: onClose);
}

传入 null 或字面量 "重复点击" 时直接 return,不显示任何提示。这是为了防连点做的取巧写法,接入时不要拿这两个值当业务文案。


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

8.1 分别验证插件与 example

从插件仓库根目录执行:

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

两个目录的 analyze 结果均为 No issues found!。flutter test 在本机没能跑通:

00:00 +0 -1: loading D:/flutter/lib4/project_utils/example/test/widget_test.dart [E]
  Failed to load "…/test/widget_test.dart": Connection closed before test suite loaded.
00:00 +0 -1: Some tests failed.

失败发生在测试文件加载阶段,还没执行到任何断言,属于本机测试工具链的问题(同一台机器上其他库还出现过 Invalid SDK hash 等不同形态),与库代码无关。因此本文的测试结论一律以真机演示为准,不把“有测试文件”写成“测试通过”。example/test/widget_test.dart 已按当前 Demo 页重写为两条用例,用 mock 通道替换 project_utils 通道,覆盖“平台版本上屏”和“点 showSuccess 出现操作成功”:

TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
    .setMockMethodCallHandler(const MethodChannel('project_utils'),
        (call) async {
  if (call.method == 'getPlatformVersion') {
    return 'OpenHarmony-test-1.0.0';
  }
  return null;
});

这份测试文件原来是模板留存的 MyApp 用例,引用已经不存在的类,flutter analyze 直接报 creation_with_non_type。改页面后同步改测试,是这类示例工程最容易漏掉的一步。

Dart 测试覆盖接口和页面逻辑,真实设备信息读取还需要在鸿蒙设备上验证。

8.2 确认设备连接

hdc list targets
flutter devices

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

本例真机为 TLR-AL00,系统 7.0.0.109(SP8C00E105R3P2),API 26。设备 ROM 会随系统升级变化,每次适配前用下面的命令核对一次,而不是沿用上一篇记录:

hdc shell "param get const.product.software.version"
hdc shell "param get const.ohos.apiversion"

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. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行构建或运行。

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

8.4 构建与运行示例

如果 flutter build hap --debug 在 ohpm 阶段失败(ohpm install failed、00306053 Specification Limit Violation),可以拆成两步:先在 example/ohos 手动装依赖,再直接调 Hvigor:

$env:NODE_OPTIONS=""
$env:JAVA_HOME="D:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
Set-Location "D:\flutter\lib4\project_utils\example\ohos"
& "D:\Program Files\Huawei\DevEco Studio\tools\ohpm\bin\ohpm.bat" install
& "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" `
    assembleHap -p product=default -p buildMode=debug --no-daemon

三点经验:

  • PowerShell 调 ohpm 必须写 ohpm.bat,直接调无扩展名的 ohpm 会报“无法在管道中间运行文档”;
  • NODE_OPTIONS 必须清空,否则构建期批量删缓存会被安全策略拦下(SAFE_DELETE_BULK_CONFIRM_REQUIRED),最后显示 COMPILE RESULT:FAIL,很容易误判成 ArkTS 语法错误;
  • 命令收尾常以退出码 1 结束,报 wmic.exe 被安全策略阻止。这只影响版本信息收集,只要前面出现过 hvigor BUILD SUCCESSFUL,产物就是有效的。

本次实测两次构建分别 BUILD SUCCESSFUL in 1 min 22 s 170 ms(首次)与 58 s 569 ms(改完示例页后重编)。典型产物位于:

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

目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物;hdc file recv 与 hdc install 都要先 cd 到产物目录,路径里带空格时更要注意。

8.5 在设备上测试三项能力

  1. 打开应用,确认首屏“平台信息”卡片显示 OpenHarmony-7.0.0.105 与“已调用 1 次”;
  2. 点“重新获取平台版本”,确认计数递增、返回一致;
  3. 依次点 showText、showSuccess、showInfo,确认提示出现在页面中部偏上(showError 与前两者共用同一套 toastBuilder,只是图标不同,本次未单独截图);
  4. 点 showLoadingText + hide,确认转圈提示出现并在约 2 秒后自行消失;
  5. 切到“KeepAlive 演示”,对卡片 #1 连点 3 次、卡片 #2 点 2 次;
  6. 切回“平台信息”再切回“KeepAlive 演示”,确认两个计数仍是 3 和 2。

第 6 步是 KeepAlive 的有效验证方式:只看当前页面的数字不能证明缓存生效,必须经过一次“子项被移出可见区域”的过程。上下滚动列表属于同一原理的补充验证,本文未单独截图。

8.6 鸿蒙设备运行效果

完成适配后,getPlatformVersion 返回鸿蒙系统的完整名称,ToastUtils 五类提示正常显示,KeepAliveWrapper 在切页与滚动后保留子项状态。

系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置 系统日期和时间设置

页面操作实测结果
平台信息首次进入OpenHarmony-7.0.0.105,已调用 1 次
平台信息点“重新获取平台版本”返回一致,计数变为 2 次
平台信息showText / showSuccess / showInfo分别显示“这是文本 Toast”“操作成功”“提示信息”;showText 只有文字,后两者各带对勾与感叹图标
平台信息showLoadingText + hide显示“加载中…”转圈,约 2 秒后自动关闭
KeepAlive卡片 #1 ×3、卡片 #2 ×2计数 3 / 2
KeepAlive切到“平台信息”再切回计数仍为 3 / 2
8.6.1 返回值与系统版本对照

设备 param get const.product.software.version 返回 7.0.0.109(SP8C00E105R3P2),而 deviceInfo.osFullName 返回 OpenHarmony-7.0.0.105,尾号相差一位。这是两个不同的字段:前者是产品软件版本(含 SP 补丁段),后者是系统全名里的 OpenHarmony 版本号,两者不要求逐字相同。

对适配来说,关键结论只有两条:

  1. 返回值带着 OpenHarmony- 前缀,说明字段本身已含平台名,Dart 侧不需要再拼;
  2. 返回值不为空且可读,说明字段在当前 ROM 上可用,不需要退回 sdkApiVersion 之类的兜底值。

如果将来某个 ROM 上 osFullName 返回空串,再考虑补兜底分支;在没有证据的情况下先加兜底,只会让返回值与另外两端更难对齐。


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

9.1 Missing SDK components

典型错误如下:

Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.

这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchains、ets、js、native、previewer;
  5. 执行 flutter config --ohos-sdk <正确路径>;
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 Sync 和 Compile 首先读取本机 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。

当前工程的 compatibleSdkVersion 是 API 24,因此 API 26 设备在安装版本门槛上是满足的;但反过来说明:SDK 组件是否完整与设备无关,必须单独确认。

9.2 DevEco Studio 中看不到 entry 模块

插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。

请直接使用 DevEco Studio 打开:

project_utils/example/ohos

如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 无法手动签名

签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。

建议先确认:

  • 打开的是 example/ohos;
  • SDK 组件完整并且 Sync 成功;
  • entry 的模块类型为 entry;
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效。

9.4 首屏一直停在“获取中…”

这是本库在鸿蒙上最常见的失败形态,原因是插件没被注册:

  1. Dart 层 MethodChannelProjectUtils 没有任何平台判断,直接调用通道;
  2. 通道对面没有原生实现,平台返回 MissingPluginException;
  3. 示例页只 catch 了 PlatformException,MissingPluginException 不继承它,异常逃出 try;
  4. 页面停在 initState 里的初始文案,setState 从未执行。

排查顺序:先在 example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 里搜 ProjectUtilsPlugin(注册成功必然出现),再确认 pubspec.yaml 的 ohos 段写了 pluginClass、ohos/index.ets 有默认导出、ohos/oh-package.json5 的 name 与插件名一致。

若要更稳妥地兜底,业务侧可以同时捕获两种异常:

} on PlatformException {
  version = 'Failed to get platform version.';
} on MissingPluginException {
  version = 'MissingPluginException: 插件未注册';
}

9.5 MissingPluginException

这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:

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

如果仍然出现,检查自动生成的插件注册文件中是否包含 ProjectUtilsPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。

9.6 重复收到相同事件

本库不存在事件订阅,也没有回调注册,因此不会出现“重复回调”。如果业务侧看到重复提示,先检查是不是连续点击触发了多次 ToastUtils 调用——showText 的 onlyOne: true 只保证同一分组内不叠加,不阻止重复调用。

9.7 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • 包名与签名 Profile 不匹配;
  • 安装包的 compatibleSdkVersion 高于设备 API,报 9568297 older sdk version;
  • 手机上已经安装了使用不同证书签名的同包名应用。

本例的 compatibleSdkVersion 曾写成 26.0.0,在 API 24 设备上被拒;改成 6.1.1(24) 后才能安装。第 5 条在示例工程里尤其常见,因为多个示例共用 com.example.color_system_example 这个包名。

9.8 flutter create 不认识 ohos,或包名不合法

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name project_utils。生成后检查 diff,再补充 ArkTS 业务实现。

9.9 AtomGit 依赖提示找不到分支或无权限

先检查 URL 是否指向已同步的目标仓库,再确认 ref 里的 TAG 或分支名真实存在。本例的 ref: 0.0.1-ohos-1.0.0-beta.1 必须与仓库 TAG 完全一致;早期仓库里只有 v0.0.1 时,按文档拉取会直接失败。私有仓库还需在本机配置 Git 认证。

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

先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。

9.11 返回的版本号出现两个 OpenHarmony

首版把 osFullName 当成“只有版本号”,在前面又拼了一次 "OpenHarmony "。实际 osFullName 已经是 OpenHarmony-7.0.0.105,拼接后变成 OpenHarmony OpenHarmony-7.0.0.105。

改法只有一行:result.success(deviceInfo.osFullName)。判断是否踩了这个坑,可以直接看真机截图里的字符串,或 uitest dumpLayout 读 text 属性逐字核对——等宽字体下截图肉眼看 - 和空格很容易看错。

9.12 错误信息显示 [object Object]

catch (e) 里的 e 是 Object,HarmonyOS 的 BusinessError 用模板字符串插值会输出 [object Object]。处理方式见 5.1.4:先按 BusinessError 拆出 code 与 message,拿不到字段再回退原始字符串。业务侧最终看到的形式是 (code) message,比 null 或 [object Object] 更能定位问题。

9.13 KeepAlive 看起来没生效

三个前提逐一核对:

  1. KeepAliveWrapper 必须位于懒加载或可切换容器内(ListView.builder、PageView、TabBarView);
  2. 子项必须是 StatefulWidget,状态才存在“被保留”的对象;
  3. 验证必须经过一次“子项离开可见区域”的过程——切页或滚动,只在当前页面点数字不能说明问题。

KeepAliveWrapper 的 keepAlive 参数默认为 true;运行时切换为 false 时,didUpdateWidget 会调用 updateKeepAlive() 让改动立即生效。

9.14 Toast 完全不显示

先确认 MaterialApp 的两处接入:

builder: ToastUtils.appBuilder(),
navigatorObservers: ToastUtils.navigatorObservers(),

builder 负责插入 BotToast 的 Overlay,缺了它所有提示都无处渲染;navigatorObservers 负责路由变化时清理跨页面提示。另有两点实现细节:showText 收到 null 或字面量 "重复点击" 会直接 return;crossPage 默认 true,需要提示跟随路由关闭时把它改成 false。


相关链接

Logo

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

更多推荐