开发工具: 华为云码道

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 移除。因此它的外观可以完全定制,在不同平台上也能保持一致。

KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

例如,社交应用可以用它提示"帖子已收藏",下载器可以用它提示"任务已加入队列",表单页面可以用它提示"提交成功"。左侧的徽标色适合承载语义(成功/失败/提示),可选的资源图片适合展示封面、头像或应用图标。

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 SDK3.47.7-ohos-1.0.0Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.47-devoh-flutter 对应开发分支
Dart SDK3.13.3Dart 语言与包管理环境
DevEco Studio26.0.0(26.0.0.851)工程 Sync、签名配置与 hvigor 构建
HarmonyOS 开发套件7.0.0(API 26)开发套件版本及对应的 API 级别
compileSdkVersion26.0.0编译时使用的 SDK API
targetSdkVersion26.0.0应用面向的行为版本
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
插件版本1.0.3pubspec.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)",
  "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
getPlatformVersionasset_toastOpenHarmony ${deviceInfo.osFullName}声明 ohos 平台后由模板壳提供,业务不调用

原生端只需要让 asset_toast 通道注册成功。业务功能不依赖它,通道缺失也不影响 Toast 显示。

4.1 跨端架构与调用时序

Flutter 侧和 HarmonyOS 侧之间只有一条通道,且不参与业务:

  1. MethodChannel('asset_toast'):模板生成的插件壳,仅响应 getPlatformVersion。

Toast 从调用到消失的全部过程都发生在 Dart 侧:

渲染

Flutter 页面

AssetToast.show

ToastView 单例

先移除上一条 OverlayEntry

插入新的 OverlayEntry

Future.delayed duration 秒

dismiss 移除

Flutter Engine

AssetToastPlugin 通道 asset_toast

图中最后一条虚线表示两者的关系仅限"通道被注册过",AssetToast.show 不会走到 AssetToastPlugin。

4.1.1 一次完整显示的时序
Flutter Engine OverlayState ToastView 单例 AssetToast.show Flutter 页面 Flutter Engine OverlayState ToastView 单例 AssetToast.show Flutter 页面 await Future.delayed(Duration(seconds: duration)) show(msg, context, duration, gravity, ...) dismiss() _isVisible 为 false 时直接返回 createViewToast(9 个参数) Overlay.of(context) 计算 effectiveBackgroundColor 构造 OverlayEntry(ToastWidget) insert(_overlayEntry) _isVisible = true 下一帧绘制卡片 界面出现 Toast remove(_overlayEntry) _isVisible = false Toast 消失

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.lengthShort1显示 1 秒
AssetToast.lengthLong2显示 2 秒
AssetToast.bottom0距底部 viewInsets.bottom + 50
AssetToast.center1不设 top/bottom
AssetToast.top2距顶部 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.mdOHOS 新增能力、适配版本、兼容限制与测试范围
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 传空字符串时 isNotEmptyfalse,走无图分支
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 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs;
  4. 为 default product 选择或生成签名;
  5. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行 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)的真机上验证,操作步骤如下:

  1. 打开应用,确认示例页正常渲染:顶部卡片、消息输入框、资源图开关、位置与时长选项、颜色配置和"显示 Toast"按钮;
  2. 保持"Bottom + lengthShort (1s) + 绿色徽标 + 显示资源图片",点击"显示 Toast",确认底部弹出卡片,且约 1 秒后自动消失;
  3. 切换到"Center",再次点击,观察 Toast 的落点;
  4. 切换到"Top",再次点击,确认 Toast 出现在顶部;
  5. 输入一段较长的消息,确认文本在卡片内换行而不是溢出;
  6. 关闭"显示资源图片"开关,确认卡片左侧图片位为空;
  7. 依次切换徽标色、背景色、文本色,确认每次点击后 Toast 都使用新的颜色;
  8. 连续多次点击按钮,确认不会出现两条 Toast 叠加;
  9. 把时长切到"3s (custom)",确认显示时间明显变长。

启动日志中能确认插件已注册:

FlutterEngineCxnRegistry --> Adding plugin: AssetToastPlugin
DartMessenger --> Setting handler for channel 'asset_toast'

8.6 鸿蒙设备运行效果

完成适配后,Flutter 应用可以在 OpenHarmony 上正常显示 Toast,并且背景色、文本色、徽标色、资源图片、显示时长都按参数生效。

先看示例页本身:

示例页底部(Bottom)顶部(Top)
消息输入、开关、位置与时长、颜色配置距底部 50,带图片与绿色徽标距顶部 50

接下来验证的是"参数各不相同的组合",可以看出每个参数都独立生效:

红色徽标、无图片长文本换行自定义背景色
关闭资源图开关,左侧图片位为空文本在卡片内换行,仍单行徽标背景改为 teal,文本仍为白色

最后一张是文本色切换后的效果,与上一张只差"文本色"这一项:

KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

时长切到 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 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 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 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;
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效。

9.4 能安装但点击按钮没有出现 Toast

按以下顺序检查:

  1. 点击的按钮是不是真的调用了 AssetToast.show,回调里有没有抛异常;
  2. context 是否来自 MaterialApp 内部。在 MaterialApp 之前或之外调用,Overlay.of(context) 会失败;
  3. 传进 show 的 msg 是否为空字符串——空消息仍会插入卡片,但只有一个空的文本区;
  4. 卡片是否被其他 Overlay 内容挡住,或 backgroundColor 与页面底色接近而看不出;
  5. 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 里记录清楚,便于后续跟上游同步时区分。


相关链接

Logo

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

更多推荐