Flutter 工具集 project_utils 鸿蒙适配落地,ArkTS 读取系统版本,KeepAliveWrapper 状态缓存、ToastUtils 提示组件零原生改造
开发工具: 华为云码道
原始仓库: 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 SDK | 3.47.7-ohos-1.0.0 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.47-dev | 对应的开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| HarmonyOS 开发套件 | 7.0.0(API 26) | 开发套件版本及对应的 API 级别 |
| DevEco Studio | 26.0.0(26.0.0.851) | 工程同步、签名与 HAP 构建 |
compileSdkVersion | 26.0.0 | 编译时使用的 SDK API |
targetSdkVersion | 26.0.0 | 应用面向的行为版本 |
compatibleSdkVersion | 6.1.1(24) | 当前工程声明的最低兼容版本 |
| 插件版本 | 0.0.1 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | 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 / getPlatformVersion | deviceInfo.osFullName | 返回可读的系统版本字符串,失败时回传错误码与消息 |
KeepAliveWrapper | 无通道 | 无需原生实现 | Flutter 框架自身能力,与其他平台一致 |
ToastUtils.* | 无通道 | 无需原生实现 | 依赖 bot_toast 的 Overlay 实现,与其他平台一致 |
原生端只需要保持方法名与通道名一致。设备信息属于公共基础能力,读取失败时通过错误回调通知 Dart 层即可。
4.1 跨端架构与调用时序
Flutter 侧和 HarmonyOS 侧之间只有一条通道:
MethodChannel('project_utils'):负责发送getPlatformVersion请求并接收返回字符串。
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.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
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 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行构建或运行。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
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 在设备上测试三项能力
- 打开应用,确认首屏“平台信息”卡片显示
OpenHarmony-7.0.0.105与“已调用 1 次”; - 点“重新获取平台版本”,确认计数递增、返回一致;
- 依次点
showText、showSuccess、showInfo,确认提示出现在页面中部偏上(showError与前两者共用同一套toastBuilder,只是图标不同,本次未单独截图); - 点
showLoadingText + hide,确认转圈提示出现并在约 2 秒后自行消失; - 切到“KeepAlive 演示”,对卡片 #1 连点 3 次、卡片 #2 点 2 次;
- 切回“平台信息”再切回“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 版本号,两者不要求逐字相同。
对适配来说,关键结论只有两条:
- 返回值带着
OpenHarmony-前缀,说明字段本身已含平台名,Dart 侧不需要再拼; - 返回值不为空且可读,说明字段在当前 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 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 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;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
9.4 首屏一直停在“获取中…”
这是本库在鸿蒙上最常见的失败形态,原因是插件没被注册:
- Dart 层
MethodChannelProjectUtils没有任何平台判断,直接调用通道; - 通道对面没有原生实现,平台返回
MissingPluginException; - 示例页只
catch了PlatformException,MissingPluginException不继承它,异常逃出try; - 页面停在
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 看起来没生效
三个前提逐一核对:
KeepAliveWrapper必须位于懒加载或可切换容器内(ListView.builder、PageView、TabBarView);- 子项必须是
StatefulWidget,状态才存在“被保留”的对象; - 验证必须经过一次“子项离开可见区域”的过程——切页或滚动,只在当前页面点数字不能说明问题。
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。
相关链接
更多推荐

所有评论(0)