Flutter 插件 asset_toast 鸿蒙适配实战:Overlay 纯 Dart 渲染 Toast,源码迁移、ArkTS 插件壳开发与 API24 真机调试全流程记录
开发工具: 华为云码道
DevEco Studio:
26.0.0(26.0.0.851)原始仓库: ggichure/asset_toast
本文配套仓库: oh-flutter/asset_toast
asset_toast 是一个 Reddit 风格的 Toast 提示组件,左侧一条徽标色竖条、可选一张资源图片,右侧是消息文本,支持背景色、文本色、显示位置和显示时长。本文以 asset_toast 1.0.3 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

插件目前支持 android、ios、ohos 三个平台,源码位于 AtomGit 配套仓库。文中的代码以提交 585747d 为参考。

一、插件简介与适配目标
Toast 提示是移动应用最常用的一类轻量反馈。asset_toast 的实现方式与其他 Toast 库不同:它不调用系统原生的 Toast 能力,而是用 Flutter 的 Overlay 在应用自己的渲染层里插入一张卡片,到达设定时长后把 OverlayEntry 移除。因此它的外观可以完全定制,在不同平台上也能保持一致。
例如,社交应用可以用它提示"帖子已收藏",下载器可以用它提示"任务已加入队列",表单页面可以用它提示"提交成功"。左侧的徽标色适合承载语义(成功/失败/提示),可选的资源图片适合展示封面、头像或应用图标。
Toast 的显示与消失完全由 Dart 侧完成:调用 AssetToast.show 后,Dart 层自己完成构造卡片、插入 Overlay 和定时移除,不经过原生通道。
二、环境准备
环境搭建参考社区文档: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 | oh-flutter 对应开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| DevEco Studio | 26.0.0(26.0.0.851) | 工程 Sync、签名配置与 hvigor 构建 |
| HarmonyOS 开发套件 | 7.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compileSdkVersion | 26.0.0 | 编译时使用的 SDK API |
targetSdkVersion | 26.0.0 | 应用面向的行为版本 |
compatibleSdkVersion | 6.1.1(24) | 当前工程声明的最低兼容版本 |
| 插件版本 | 1.0.3 | 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)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 24。模板默认生成的是 compatibleSdkVersion: "26.0.0",装到 API 24 的测试机上会报 9568297 older sdk version,所以这里把它降到了与真机一致。
需要注意,降 compatibleSdkVersion 只影响"能不能装"和"按哪个行为版本运行",编译仍使用本机 DevEco 的 API 26 SDK。这是 hvigor 的默认行为:工程不写 compileSdkVersion 时按当前安装的 SDK 编译。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
asset_toast 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-flutter/asset_toast.git
cd asset_toast
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 asset_toast/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。Git 仓库名和 Dart 包名都是 asset_toast。
适配后的仓库使用 master 分支,并在适配提交上打了 1.0.3-ohos-1.0.0 标签。需要与本文代码完全一致时,在干净仓库中执行:
git switch --detach 1.0.3-ohos-1.0.0
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。
3.3 在仓库根目录确认适配分支与版本标记
接着在 asset_toast/ 根目录确认分支与版本标记。上游仓库只有一个 master 分支,适配改动直接落在这个分支上,并通过 tag 固定版本。分支名与 tag 的对应关系如下:
git branch --show-current
git tag --list
git log --oneline -3
git remote -v
git status --short --branch
分支策略与本系列其余库保持一致:分支名使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本,本例为 feat/ohos_asset_toast_1.0.3。适配完成后合并到 master,再在合并后的提交上打 tag:
git switch -c feat/ohos_asset_toast_1.0.3
git tag 1.0.3-ohos-1.0.0
如果该分支已存在,使用 git switch feat/ohos_asset_toast_1.0.3 切换即可。

图 2:在 asset_toast 仓库根目录查看分支、TAG 与远程地址。
3.4 自动补全 OHOS 适配结构
分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:
flutter create --template=plugin --platforms=ohos --project-name asset_toast .
git status --short
git diff -- pubspec.yaml lib example
--template=plugin指定插件模板。--platforms=ohos指定需要补全的平台。--project-name asset_toast使用 Dart 包名。- 最后的
.表示在当前插件目录补全工程,不是另建一层asset_toast/。
该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。
这个库还有一处与结构补全同时进行的工作:上游 pubspec.yaml 的 SDK 约束是 >=2.7.0 <3.0.0,属于空安全之前的版本,在当前 Dart 3.13.3 上无法解析。所以 lib/toast.dart 一并做了一次空安全迁移,example/lib/main.dart 也重写成了覆盖全部参数的示例页。这部分改动没有触碰公开 API,详见第四节。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos asset_toast;已有插件使用上面的 . 在当前目录补全。
3.5 适配后的项目目录
适配后的关键目录如下:
asset_toast/
├── lib/
│ ├── asset_toast.dart
│ └── toast.dart
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ └── src/main/
│ ├── ets/components/plugin/AssetToastPlugin.ets
│ └── module.json5
├── example/
│ ├── assets/img.jpeg
│ ├── lib/main.dart
│ └── ohos/
│ └── entry/
├── android/
├── ios/
├── sc/
└── pubspec.yaml
项目根目录如下,其中包含 ohos/、example/,以及 OpenHarmony 中英文说明和变更记录文件:

图 4:适配后的 asset_toast 项目目录。
| 文件 | 主要职责 |
|---|---|
lib/asset_toast.dart | 库唯一导出入口,export 'toast.dart' |
lib/toast.dart | 声明 AssetToast 常量与 show 方法,用 Overlay 完成渲染和定时移除 |
AssetToastPlugin.ets | 注册 Flutter 通道,提供 getPlatformVersion |
插件 module.json5 | 声明 HAR 模块与设备类型 |
示例 entry module.json5 | 声明宿主应用 Ability 和设备类型 |
example/assets/img.jpeg | 示例中 Image.asset 使用的本地图片 |
example/lib/main.dart | 展示消息输入、资源图开关、位置、时长和颜色配置 |
四、Dart 接口与通道分析
这个库的 Dart 层没有任何 MethodChannel 调用。阅读 lib/asset_toast.dart 只有一行 export 'toast.dart';,全部实现集中在 lib/toast.dart 的 141 行里,用 Overlay 完成渲染。因此 OHOS 侧不需要"实现对应原生调用",要做的是确认插件通道能被注册、以及确认纯 Dart 渲染路径在 OpenHarmony 上表现一致。适配其他已有平台的插件时,也应先看清它到底有没有真实的原生依赖,再决定原生侧要写多少。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
AssetToast.show(msg, context, ...) | 无 | 无 | 在 Dart 侧插入 OverlayEntry,到时自动移除 |
AssetToast.lengthShort/lengthLong | 无 | 无 | 保持 1/2,作为 duration 的秒数 |
AssetToast.bottom/center/top | 无 | 无 | 保持 0/1/2,映射到 Positioned 的 top/bottom |
getPlatformVersion | asset_toast | OpenHarmony ${deviceInfo.osFullName} | 声明 ohos 平台后由模板壳提供,业务不调用 |
原生端只需要让 asset_toast 通道注册成功。业务功能不依赖它,通道缺失也不影响 Toast 显示。
4.1 跨端架构与调用时序
Flutter 侧和 HarmonyOS 侧之间只有一条通道,且不参与业务:
MethodChannel('asset_toast'):模板生成的插件壳,仅响应getPlatformVersion。
Toast 从调用到消失的全部过程都发生在 Dart 侧:
图中最后一条虚线表示两者的关系仅限"通道被注册过",AssetToast.show 不会走到 AssetToastPlugin。
4.1.1 一次完整显示的时序
4.2 位置与时长模型:lib/toast.dart
库用两组整数常量表达位置和时长,没有枚举,也没有字符串:
class AssetToast {
static const int lengthShort = 1;
static const int lengthLong = 2;
static const int bottom = 0;
static const int center = 1;
static const int top = 2;
| Dart 常量 | 值 | 业务含义 |
|---|---|---|
AssetToast.lengthShort | 1 | 显示 1 秒 |
AssetToast.lengthLong | 2 | 显示 2 秒 |
AssetToast.bottom | 0 | 距底部 viewInsets.bottom + 50 |
AssetToast.center | 1 | 不设 top/bottom |
AssetToast.top | 2 | 距顶部 viewInsets.top + 50 |
duration 直接作为 Duration(seconds: duration) 的参数,所以可以传 3 这样的任意秒数,示例页的"3s (custom)"就是这样做的。
定位逻辑在 ToastWidget 中,只有一行:
return Positioned(
top: gravity == 2 ? MediaQuery.of(context).viewInsets.top + 50 : null,
bottom:
gravity == 0 ? MediaQuery.of(context).viewInsets.bottom + 50 : null,
child: Material(
color: Colors.transparent,
child: widget,
));
只有在 gravity == 2 时才设 top,只有在 gravity == 0 时才设 bottom。gravity == 1(center)两个属性都是 null,Positioned 就退化成"按父级左上角对齐"。这一点在真机上是可观察的:真机验证时,选择 Center 后 Toast 会贴到状态栏附近,主体被挤出屏幕左边界,详见 8.6 节。
4.3 公开 API 与渲染参数
AssetToast.show 是唯一的公开方法,全部参数都有默认值:
static void show(String msg, BuildContext context,
{int duration = 1,
int gravity = 0,
Color backgroundColor = const Color(0xAA000000),
textStyle = const TextStyle(fontSize: 15, color: Colors.white),
Color prefixBadge = Colors.green,
Color msgColor = Colors.white,
String asset = ""}) {
ToastView.dismiss();
ToastView.createViewToast(msg, asset, context, duration, gravity,
prefixBadge, msgColor, backgroundColor, textStyle);
}
各参数的作用:
msg:消息文本,TextAlign.center,超出宽度会换行;duration:显示秒数,常用AssetToast.lengthShort与AssetToast.lengthLong;gravity:位置,取AssetToast.bottom/center/top;backgroundColor:卡片背景色,默认半透明黑0xAA000000;prefixBadge:左侧 10×60 竖条的颜色,默认绿色;msgColor:文本颜色,默认白色;asset:图片资源路径,空字符串表示不显示图片;textStyle:形参存在,但当前实现里文本样式是写死的fontSize: 16.0+msgColor,传入的textStyle不会生效。
show 的第一句是 ToastView.dismiss(),作用是"后一条顶掉前一条"。同一时刻只会有一条 Toast。
4.4 渲染实现分析
4.4.1 单例与状态字段
class ToastView {
static final ToastView _singleton = ToastView._internal();
factory ToastView() {
return _singleton;
}
ToastView._internal();
static OverlayState? overlayState;
static OverlayEntry? _overlayEntry;
static bool _isVisible = false;
overlayState、_overlayEntry、_isVisible 都是静态字段,ToastView 本身是单例但业务侧不需要实例化它。空安全迁移时这三个字段加了 ?,_singleton 保持 static final。
4.4.2 创建并插入 OverlayEntry
static void createViewToast(
String msg,
String asset,
BuildContext context,
int duration,
int gravity,
Color prefixBadge,
Color msgColor,
Color? backgroundColor,
TextStyle textStyle,
) async {
overlayState = Overlay.of(context);
// 保持与迁移前一致的兜底语义:未显式指定背景色时退回主题主色。
final Color effectiveBackgroundColor =
backgroundColor ?? Theme.of(context).primaryColor;
背景色的兜底在这里被提到了一个局部变量。原版在两个地方分别判断 backgroundColor != null(Paint.color 和 Card.color),空安全迁移后合并成一次 ??,语义不变但少了一处重复。
卡片结构是 Row:左侧 10 宽的徽标条、中间 50×50 的图片位(asset 非空时才放 Image.asset)、右侧 Expanded 里的文本。整条卡片宽度是 MediaQuery.of(context).size.width * .9。
4.4.3 定时移除
_isVisible = true;
overlayState?.insert(_overlayEntry!);
await Future.delayed(Duration(seconds: duration));
dismiss();
}
static dismiss() async {
if (!_isVisible) {
return;
}
_isVisible = false;
_overlayEntry?.remove();
}
_isVisible 是幂等保护。show 开头先调 dismiss,如果当前没有显示中的 Toast,dismiss 直接返回;如果正在显示,就立即移除。定时结束后的 dismiss 也会检查这个标记,避免移除一个已经被移除的 OverlayEntry。
空安全迁移时这里还删掉了一处冗余判断:原版在 duration == null 时回退到 AssetToast.lengthShort,但 duration 在 show 的签名里是非空 int 且有默认值 1,这个分支永远不会命中。
五、补全 OHOS 原生实现与工程配置
5.1 在 AssetToastPlugin.ets 中补全插件壳
以 AssetToast.show 为例:业务调用的仍是 Dart 方法,途中不经过任何通道。需要补全的 AssetToastPlugin.ets 只承担一件事——声明 ohos 平台后,让 asset_toast 通道注册成功,并保留模板自带的 getPlatformVersion。它不参与 Toast 的显示逻辑。
AssetToastPlugin 实现 FlutterPlugin 和 MethodCallHandler。完整文件只有 44 行,下面逐段列出。
原生插件位于:
ohos/src/main/ets/components/plugin/AssetToastPlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
import deviceInfo from '@ohos.deviceInfo';
其中:
FlutterPlugin负责接入 Flutter Engine 生命周期;MethodChannel注册插件通道;MethodCall和MethodResult是通道两侧的数据结构;deviceInfo提供osFullName,用于拼出getPlatformVersion的返回值。
与母版那个库不同,这里没有事件通道:asset_toast 不存在任何"持续推送"的需求,Toast 的生命周期完全由 Dart 的定时器控制。
5.1.2 连接 Flutter Engine
export default class AssetToastPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
constructor() {
}
getUniqueClassName(): string {
return "AssetToastPlugin"
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "asset_toast");
this.channel.setMethodCallHandler(this)
}
通道名 asset_toast 与包名一致。setMethodCallHandler(this) 把当前实例注册为处理器,后续 onMethodCall 才会被调用。
5.1.3 通道方法
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
result.success(`OpenHarmony ${deviceInfo.osFullName}`);
} else {
result.notImplemented()
}
} catch (e) {
result.error("ASSET_TOAST_ERROR", `method ${call.method} failed: ${e}`, null)
}
}
只有 getPlatformVersion 一个分支,其余方法一律 result.notImplemented()。catch 里用固定的错误码 ASSET_TOAST_ERROR 把异常带回 Dart 侧。
实际运行时不走这些代码:Dart 层没有任何 invokeMethod。保留它的意义是让插件结构完整,将来若有原生需求(例如读取系统字体缩放、系统 Toast 样式)可以直接在 onMethodCall 里加分支,不用再改工程结构。
5.1.4 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null)
}
}
}
Flutter Engine 销毁时清空通道 Handler。这里没有 EventSink 和系统监听需要释放,因为插件从未注册过它们。
5.2 声明插件和宿主权限
asset_toast 不使用任何系统能力:Toast 由 Flutter Overlay 渲染,图片走 Flutter 的资源加载,不读取传感器、不访问网络、不写文件。当前工程不需要声明任何权限。
5.2.1 插件 HAR 的权限
插件的 ohos/src/main/module.json5 保持模板生成的内容,requestPermissions 字段留空即可:
{
"module": {
"name": "asset_toast",
"type": "har",
"deviceTypes": [
"default",
"tablet"
]
}
}
5.2.2 应用 entry 的权限
宿主 example/ohos/entry/src/main/module.json5 同样不需要补充 requestPermissions。因为插件没有申请任何权限,宿主也没有需要向用户解释的能力场景。
如果后续在插件里引入了需要权限的原生能力,权限要同时写在插件 HAR 和宿主 entry 两处:HAR 的声明决定"这个模块需要什么",宿主的声明和 usedScene 决定"实际安装的应用向用户申请什么"。只写 HAR 而漏写宿主,是这类问题最常见的原因。
5.3 注册并导出插件
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
android:
package: com.ggichure.github.asset_toast
pluginClass: AssetToastPlugin
ios:
pluginClass: AssetToastPlugin
ohos:
pluginClass: AssetToastPlugin
插件的 ohos/index.ets 需要导出实现:
import AssetToastPlugin from './src/main/ets/components/plugin/AssetToastPlugin';
export default AssetToastPlugin;
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
注册是否成功可以直接在日志里确认。清理日志缓冲区后启动应用,hilog 中会出现两行连续输出:
FlutterEngineCxnRegistry --> Adding plugin: AssetToastPlugin
DartMessenger --> Setting handler for channel 'asset_toast'
前一行表示插件实例被注册进 Engine,后一行表示 asset_toast 通道的处理器已就位。这两行都出现,说明第 5.1 节的代码生效了。
注册异常的排查步骤见第九节 MissingPluginException。
5.4 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS",
"targetSdkVersion": "26.0.0"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
还有一处需要留意:本例 example/ohos/AppScope/app.json5 的 bundleName 是 com.example.color_system_example。这是原示例工程的包名沿用了模板默认值,安装和运行都不受影响,但自定义签名时 Profile 里的包名必须与它一致。改成正式名称时,app.json5 和签名 Profile 要一起改,否则签名校验会失败。
六、补全交付文件并提交适配分支
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 | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖位置、时长、颜色和资源图开关 |
pubspec.yaml、ohos/oh-package.json5 | 核对包名、版本、插件注册、仓库地址、许可证和依赖 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
README.OpenSource 记录库本身的来源与版本。本例的包名为 asset_toast,版本为 1.0.3,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add ohos pubspec.yaml example lib .gitignore
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(ohos): add OpenHarmony platform support for asset_toast"
git remote -v
git branch --show-current
git push -u origin feat/ohos_asset_toast_1.0.3
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 feat/ohos_asset_toast_1.0.3。
推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
合并后打上 tag,便于业务侧按 tag 引用:
git switch master
git tag 1.0.3-ohos-1.0.0
git push origin 1.0.3-ohos-1.0.0
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和体验不同参数下的 Toast 外观。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
asset_toast:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。
示例还需要注册资源图片,否则 Image.asset 找不到文件:
flutter:
uses-material-design: true
assets:
- assets/img.jpeg
这段配置写在 example/pubspec.yaml 里,assets/img.jpeg 是相对于 example/ 的路径。插件本身不带图片,图片由接入的应用提供。
7.2 通过 AtomGit 引入插件
业务应用通过 AtomGit 引入时,将 asset_toast 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的 tag:
dependencies:
flutter:
sdk: flutter
asset_toast:
git:
url: https://atomgit.com/oh-flutter/asset_toast.git
ref: 1.0.3-ohos-1.0.0
使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_asset_toast_1.0.3。正式发布后可固定到 tag 或 commit。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 asset_toast 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口显示一条 Toast
下面的页面只有一个按钮,点击后在底部弹出一条绿色徽标、无图片的 Toast。仓库中的完整 Demo 另外提供消息输入、资源图开关、位置/时长选择和颜色配置。
import 'package:asset_toast/asset_toast.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: ToastPage()));
}
class ToastPage extends StatelessWidget {
const ToastPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('asset_toast')),
body: Center(
child: FilledButton.icon(
onPressed: () {
AssetToast.show(
'Hello OpenHarmony!',
context,
duration: AssetToast.lengthShort,
gravity: AssetToast.bottom,
prefixBadge: Colors.green,
backgroundColor: const Color(0xAA000000),
msgColor: Colors.white,
);
},
icon: const Icon(Icons.send),
label: const Text('显示 Toast'),
),
),
);
}
}
需要带图片时,把图片放进应用的 assets/、在 pubspec.yaml 注册,再传入路径:
AssetToast.show(
'Hello OpenHarmony!',
context,
asset: 'assets/img.jpeg',
duration: AssetToast.lengthLong,
gravity: AssetToast.top,
);
context 必须是 MaterialApp 内部的某个 BuildContext,因为它要能拿到 Overlay。在 main() 里、MaterialApp 之外调用会抛异常。
7.4 页面退出与连续调用的处理
createViewToast 是一个 async 方法,内部 await Future.delayed(...) 之后才移除 OverlayEntry。这期间如果所在页面被销毁,OverlayEntry 不会自动清理,因为它挂在 Overlay 上而不是页面本身。要在页面退出时立刻收起 Toast,可以在 dispose 中调用一次 ToastView.dismiss()。
连续点击同一个按钮不需要额外处理:show 的第一句就是 dismiss(),上一条会被立即移除,只留下最新的一条。这也是 _isVisible 标记的作用——避免对同一个 OverlayEntry 重复 remove()。
如果希望 Toast 跨页面存在(例如提交后跳转到另一个页面再提示),把 AssetToast.show 的 context 换成 MaterialApp 之上的 navigatorKey.currentContext,这样 Toast 挂的是全局 Overlay,不随页面销毁而消失。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
cd example
flutter pub get
flutter analyze
本次的实际结果是:插件根目录与 example/ 各报 1 条 warning,都是 include_file_not_found——analysis_options.yaml 里 include 了 package:flutter_lints/flutter.yaml,但 dev_dependencies 里没有声明 flutter_lints。这是上游仓库自带的问题,不影响编译,加一行依赖即可消除。
仓库根目录没有 test/ 目录,example/ 下有一个 widget_test.dart。本机执行 flutter test 时 flutter_tester 报告 SDK 哈希不匹配(Invalid SDK hash),测试运行器无法启动,因此 example 的 Widget 测试未在本机执行,本节不声称测试已通过。
作为替代,用 dart run 临时脚本复算了不依赖 Flutter 引擎的纯逻辑,覆盖以下几组:
| 验证项 | 结果 |
|---|---|
lengthShort / lengthLong 的取值 | 1 / 2 |
bottom / center / top 的取值 | 0 / 1 / 2 |
bottom 的 bottom 偏移(viewInsets=0) | 50 |
top 的 top 偏移(viewInsets=0) | 50 |
center 的 top / bottom | 两者均为 null |
键盘弹出后 bottom 的偏移(viewInsets.bottom=96) | 146 |
asset 传空字符串时 isNotEmpty | false,走无图分支 |
Duration(seconds: 1) / Duration(seconds: 2) | 1000 / 2000 毫秒 |
渲染结果、OverlayEntry 的插入与移除属于 Flutter 框架行为,无法用这种方式覆盖,需要在真机上验证,见 8.5 与 8.6。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行 Flutter 构建或运行。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
8.4 运行示例
以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:
flutter run -d <device-id>
也可以先构建 HAP:
flutter build hap --debug
本次 flutter build hap 在 assembleHap 之前的 ohpm install 阶段失败,报 00306053 Specification Limit Violation,终端里同时出现 BATCH RECURSION exceeds STACK limits。原因是 hvigorw 通过批处理递归调用 ohpm 时触发了 Windows 的批处理递归限制。
绕过方式是先在 example/ohos 下手动执行一次依赖安装,再直接调 hvigorw 构建:
cd example/ohos
"D:/Program Files/Huawei/DevEco Studio/tools/ohpm/bin/ohpm" install
然后在同一个目录用 DevEco 自带的 hvigor 构建。Windows 上需要把 JBR 同时加进 JAVA_HOME 和 PATH,否则打包阶段找不到 java:
$env:JAVA_HOME = "D:\Program Files\Huawei\DevEco Studio\jbr"
$env:Path = "$env:JAVA_HOME\bin;" + $env:Path
& "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" `
assembleHap -p product=default -p buildMode=debug --no-daemon
构建成功的输出结尾是 BUILD SUCCESSFUL,本次耗时约 55 秒,42 个任务中 26 个实际执行。
典型产物位于:
example/ohos/entry/build/default/outputs/default/
目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物:
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
8.5 在设备上测试 Toast
本次在一台 HarmonyOS 6.1.1.120(API 24)的真机上验证,操作步骤如下:
- 打开应用,确认示例页正常渲染:顶部卡片、消息输入框、资源图开关、位置与时长选项、颜色配置和"显示 Toast"按钮;
- 保持"Bottom + lengthShort (1s) + 绿色徽标 + 显示资源图片",点击"显示 Toast",确认底部弹出卡片,且约 1 秒后自动消失;
- 切换到"Center",再次点击,观察 Toast 的落点;
- 切换到"Top",再次点击,确认 Toast 出现在顶部;
- 输入一段较长的消息,确认文本在卡片内换行而不是溢出;
- 关闭"显示资源图片"开关,确认卡片左侧图片位为空;
- 依次切换徽标色、背景色、文本色,确认每次点击后 Toast 都使用新的颜色;
- 连续多次点击按钮,确认不会出现两条 Toast 叠加;
- 把时长切到"3s (custom)",确认显示时间明显变长。
启动日志中能确认插件已注册:
FlutterEngineCxnRegistry --> Adding plugin: AssetToastPlugin
DartMessenger --> Setting handler for channel 'asset_toast'
8.6 鸿蒙设备运行效果
完成适配后,Flutter 应用可以在 OpenHarmony 上正常显示 Toast,并且背景色、文本色、徽标色、资源图片、显示时长都按参数生效。
先看示例页本身:
| 示例页 | 底部(Bottom) | 顶部(Top) |
|---|---|---|
| 消息输入、开关、位置与时长、颜色配置 | 距底部 50,带图片与绿色徽标 | 距顶部 50 |
接下来验证的是"参数各不相同的组合",可以看出每个参数都独立生效:
| 红色徽标、无图片 | 长文本换行 | 自定义背景色 |
|---|---|---|
| 关闭资源图开关,左侧图片位为空 | 文本在卡片内换行,仍单行徽标 | 背景改为 teal,文本仍为白色 |
最后一张是文本色切换后的效果,与上一张只差"文本色"这一项:
| 时长切到 3s | 文本色改为黄色 |
|---|---|
duration: 3,卡片样式不变 | 与上一张只差 msgColor |
真机上还暴露了一个上游缺陷:选择 Center 时 Toast 不会居中。由于 ToastWidget 在 gravity == 1 时既不设 top 也不设 bottom,Positioned 按父级左上角对齐,结果是文本跑到状态栏附近、卡片主体被挤出屏幕左边界:
| Center 位置 |
|---|
| 文本落在状态栏区域,卡片主体被裁切到屏幕左侧 |
这个行为与平台无关,Android 和 iOS 上同样存在。要修正它,需要给 gravity == 1 补一个居中约束,例如用 Align 包裹并设置 alignment: Alignment.center,或者在外层改用 Positioned.fill 加 Center。本次适配保持与上游一致,没有改动这个行为,只在此如实记录。
九、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 路径是否一致;
- 避免误用
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 24 的设备在安装版本门槛上是满足的。asset_toast 不依赖任何设备能力或权限,装上即可使用;如果仍然没有出现 Toast,排查方向是业务代码里的 context 与参数,而不是设备能力。
9.2 DevEco Studio 中看不到 entry 模块
插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
asset_toast/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 能安装但点击按钮没有出现 Toast
按以下顺序检查:
- 点击的按钮是不是真的调用了
AssetToast.show,回调里有没有抛异常; context是否来自MaterialApp内部。在MaterialApp之前或之外调用,Overlay.of(context)会失败;- 传进
show的msg是否为空字符串——空消息仍会插入卡片,但只有一个空的文本区; - 卡片是否被其他
Overlay内容挡住,或backgroundColor与页面底色接近而看不出; hilog中是否出现Setting handler for channel 'asset_toast',确认插件已注册(这一条只影响通道是否可用,不影响 Toast 显示)。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 AssetToastPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。
需要注意,asset_toast 的 Toast 功能不经过通道,所以即使出现这个异常,Toast 依然能正常显示。它影响的只有 getPlatformVersion 这类通道方法,排查显示问题时不必从这里入手。
9.6 连续点击出现两条 Toast 叠加
AssetToast.show 的第一句是 ToastView.dismiss(),正常情况下不会叠加。如果确实看到两条,检查是否绕过 show 直接调用了 ToastView.createViewToast——后者没有前置的 dismiss。
另一种可能是两个 Overlay:应用自己额外插入了一个 Overlay 层(例如自定义弹层或 Navigator 嵌套),两个 context 拿到不同的 OverlayState,静态字段 overlayState 会被后一次调用覆盖。统一使用同一个 Navigator 下的 context 即可。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。
本例最容易踩的是第四条:模板生成的 compatibleSdkVersion 是 26.0.0,装到 API 24 的设备上会报 9568297 older sdk version。按 2.1 节把它降到 6.1.1(24) 即可。
9.8 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name asset_toast。生成后检查 diff,再补充 ArkTS 业务实现。
9.9 AtomGit 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库,再确认 feat/ohos_asset_toast_1.0.3 已推送。分支尚未推送时,可以先使用第七节 Git 依赖里的 tag。私有仓库还需在本机配置 Git 认证。
9.10 改了本地 ArkTS,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。
9.11 选择 Center 后 Toast 没有居中
这不是适配引入的问题,是上游实现本身的缺陷。ToastWidget 只在 gravity == 2 时设 top、gravity == 0 时设 bottom,center(值为 1)两个都不设,Positioned 于是按父级左上角对齐,卡片主体会被裁到屏幕外。
如果业务上必须用居中位置,有两种改法:
- 在业务侧改用
gravity: AssetToast.bottom,配合viewInsets.bottom控制大致高度; - 修改插件源码,在
ToastWidget里为gravity == 1分支补一个居中约束,例如整体换成Align(alignment: Alignment.center, child: ...)。
第二种改法会与上游产生差异,建议在适配仓库的 CHANGELOG.OpenHarmony.md 里记录清楚,便于后续跟上游同步时区分。
相关链接
更多推荐


所有评论(0)