本文记录 DhyanToast 接入 OpenHarmony 的完整过程,覆盖 Toast 库形态盘点、Compose 源码集隔离、无头状态机建模、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

DhyanToast 是 Compose Multiplatform 的 Toast 通知库:ToastHost 渲染堆叠 toast,支持手势(点按暂停展开、滑动消除)、弹性动画、分类主题(成功/警告/错误/自定义)与自动消失,API 形态对齐 Material3 的 Snackbar。本次适配与 KStore 那类"真编译整库"路线正好是两个极端:DhyanToast 的全部上游源码都站在 compose.ui / compose.material3 / compose.animation 上,而鸿蒙侧没有可用的 CMP 制品线——所以既没有内核可抽、也没有整库可编。适配把边界切在语义与渲染之间:上游 ToastHost / ToastHostState 的队列语义(600ms 退出窗口、50ms 出现延迟、oldest-first 自动消失、拖拽停表、堆叠 offset/scale/alpha 公式)被逐常量复刻进一个零依赖的纯 Kotlin ToastEngine,编译成 OpenHarmony 动态库;Compose 源码原样隔离在 composeMain 保留不编;ArkTS 页面以 16ms 帧循环驱动引擎、用 spring 动画逼近引擎给出的渲染目标值,作为验收宿主。

项目地址: AtomGit/oh-tpc/DhyanToast.git

上游仓库: androidpoet/DhyanToast

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

DhyanToast 为 Compose Multiplatform 应用提供"消息 ↔ 浮层"这一件事的完整答案:rememberToastHostState() 一行建宿主,showToast(message, title, icon, category, height) 全生命周期覆盖,ToastHost 负责堆叠渲染(最新的 toast 在最前,旧的按 positionedIndex 层叠下沉并缩放淡出),ToastScaffold 把宿主包进 Material3 Scaffold。手势语义完整:点按切换暂停并展开堆叠布局、拖拽超过阈值松手消除、✕ 显式关闭;自动消失按 oldest-first 节奏推进,拖拽期间停表。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套通知能力,但上游构建(Kotlin 2.1.0 / AGP 8.5.2 / Compose 1.7.3)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮和卡片,却无法证明公共的队列状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态机引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 动态库。
工具链不一致上游 Kotlin 2.1.0 / Gradle 8.9 / AGP 8.5.2 + Android target 与鸿蒙工具链无法共存于一个 Gradle 构建;AGP classpath 必须随根构建替换剥离。
依赖矩阵引擎侧(ToastCategory / ToastAlignment)零依赖,但渲染侧依赖 compose 全家桶(ui / material3 / animation / materialIconsExtended / featherIcons),鸿蒙没有任何一条对应制品线。
语言边界不同ArkTS 不能直接持有 Kotlin 对象与 Compose 状态,需要 C ABI、C++ N-API 和 JSON。
动画语义跨不了桥mutableStateListOf、LaunchedEffect、animateFloatAsState(spring(0.85f, 300f)) 都是 Compose 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:队列状态机的 ohos 编译(语义镜像)、Compose 源码集隔离(composeMain 保留不编)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。渲染交给 ArkTS,语义完全由 ToastEngine 负责。

1.2 库提供的能力

Demo 提供一个真实的 toast 会话,对应桥接的全部入口:

  • 入队(show):DhyanToastShow 把消息、标题、分类、图标、高度写入队列,引擎按上游语义排队并分配 id;
  • 消除(dismiss):✕ 按钮 or 滑动手势触发 DhyanToastDismiss,toast 进入 600ms 退出窗口(alpha 动画到 0 后移除);
  • 暂停展开(setPaused):点按 toast 切换,堆叠从 8dp 间距的紧凑布局展开为 16dp 间距、逐条全高排布;
  • 拖拽停表(setDragging):拖拽期间自动消失被抑制——这是上游唯一真正停表的途径;
  • 运行时配置(configure):顶部/底部对齐、可见数量 1~5、自动消失开关与 1.0~6.0s 时长;
  • 帧驱动(tick):ArkTS 每 16ms 调一次 DhyanToastTick,推进队列并取回每条 toast 的渲染目标值;
  • 清空(clear) 与 版本握手(version):一键清空队列;DhyanToastVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

tick 响应的 toasts 数组中,每条 toast 携带:

字段作用
id / title / message / category / icon内容载荷,icon 是名字字符串(check / alert / x / info),由 ArkTS 映射为字形。
heightDp / gapDp卡片高度与当前堆叠间距(8 或 16)。
positionedIndex堆叠序:最新可见 toast 为 0,旧的递增。
offsetYDp / scale / alpha渲染目标值,公式与常量逐项来自上游 ToastItem。
hasAppeared / markedForDeletion50ms 出现延迟与 600ms 退出窗口的阶段标记。

上游状态机的完整语义(堆叠公式、手势阈值、自动消失节奏)全部进入 .so,由 ToastEngine 在 Native 侧执行;ArkTS 只负责动画逼近与绘制。

1.3 实现适配

维度要求
代码复用模型层原样保留:ToastCategory 一行没改,ToastAlignment 仅剥离 1 个 Compose 扩展方法;渲染层 5 个文件原样移入 composeMain 隔离不删;ToastEngine 是全仓唯一新增 Kotlin 源文件(语义镜像,非重写业务)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);app / baselineprofile* / buildSrc 移出构建(源文件留盘)。
桥接稳定8 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律引擎核心零第三方依赖——不需要验证 kotlinx 制品线,也不引入 kotlinx.serialization(手写 MiniJson,避免 KN 产物膨胀)。
UI 完整单页覆盖全部桥接入口:内容输入、分类/图标 chips、对齐/可见数量/自动消失配置、显示/连发/暂停/清空、手势与状态行。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。DhyanToast 恰恰整个库都是 UI——这正是它与存储类库相反的极端案例:适配面从"引擎"换成了"语义"。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点 7 个源文件的 Compose 依赖度、确定"语义/渲染"切分线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:引擎建模       ── ToastEngine 无头状态机:逐常量镜像 ToastHost/ToastHostState、nowMs 注入
第 4 阶段:原生桥接       ── @CName × 8 C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 16ms tick 帧循环、springMotion 动画逼近、PanGesture 手势、控制面板
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
dhyantoast/src/commonMain/          纯 Kotlin 核心:ToastCategory + ToastAlignment + ToastEngine(进 ohos 构建)
dhyantoast/src/composeMain/         上游 Compose 源码 ×5(ToastHost/ToastScaffold/ToastHostState/ToastStyle/ToastData,
                                    保留不编,等社区 CMP ohos 制品对齐后接回)
example/nativeApp/                  Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                 DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md      鸿蒙适配说明(中英双份,CN 为主文档 19 节)
README.upstream.md                  上游原文保存
app/ baselineprofile*/ scripts/ spotless/   上游遗留,留盘不进 settings

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :dhyantoast 与 :example:nativeApp,rootProject.name = "ohos_dhyantoast"。上游的 AGP / nexus 发布 / spotless / dokka / baselineprofile 插件全部随根构建替换剥离——本 fork 不发 Maven Central。buildSrc 整目录删除:Gradle 会自动编译它,留着就会把旧世界的 Configuration 引用拖进每次构建。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101-hotspot,Windows)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(从上游 8.9 升级并对齐社区线;2.2.21 线必须 Gradle 8.x,qrose 先例)
依赖引擎核心零第三方依赖;桥上手写 MiniJson
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.dhyantoast
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version

Windows 下 gradlew.bat 启动 Gradle 守护进程依赖 JAVA_HOME(org.gradle.java.home 只管守护进程内部 JDK,不管 wrapper 引导)——不设置会得到一条与鸿蒙毫无关系的 JAVA_HOME is not set 报错。

1.3 源码依赖度盘点

上游 commonMain 一共 7 个 Kotlin 文件,逐个查 import:

文件Compose 依赖处置
ToastCategory.kt零(纯 sealed class)原样保留 commonMain
ToastAlignment.kt仅 toComposeAlignment() 用 Alignment剥离该方法,纯枚举留在 commonMain
ToastData.ktDp 类型移入 composeMain(渲染接口)
ToastHostState.ktmutableStateListOf / mutableStateMapOf / remember移入 composeMain
ToastHost.kt全套(ui/foundation/animation/material3)移入 composeMain
ToastScaffold.ktmaterial3 Scaffold移入 composeMain
ToastStyle.ktColor / Shape / RoundedCornerShape移入 composeMain

结论很清晰:模型本来就纯,渲染本来就是 Compose。正确的切分不是"抽内核",而是"语义/渲染分离"——渲染文件原地搬进 composeMain(applyDefaultHierarchyTemplate() 不认识这个目录名,天然不参与任何目标编译),commonMain 留下的两个纯文件加上新增的 ToastEngine 构成 ohos 侧的全部 Kotlin 资产。

1.4 页面边界
标题区          DHYANTOAST · OPENHARMONY / 版本握手行(CMP Toast 库 · Kotlin/Native ohosArm64/ohosX64)
内容区          Toast 消息输入框 + 标题输入框(可选)
分类区          常规 / 成功 / 警告 / 错误 / 自定义 chips
图标区          无 / ✓ / ⚠ / ✕ / ⓘ chips
宿主区          顶部/底部 chips、可见数量 Slider(1~5)、自动消失 Toggle + 时长 Slider(1.0~6.0s)
操作区          显示 Toast(主按钮)/ 连发×5 / 暂停展开 / 清空
状态行          桥版本握手 + 队列计数 + 最近一次操作结果
浮层            toast 堆叠 overlay(手势、动画、消除)

页面启动时先调 dhyantoastVersion() 做握手,再注入两条欢迎 toast 直观展示堆叠效果。

第 2 阶段:目标与依赖打通

2.1 根构建替换

settings.gradle.kts 把 eazytec 双仓写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_dhyantoast"

include(":dhyantoast")
include(":example:nativeApp")

gradle.properties 收敛为鸿蒙最小集:JDK 21 路径、version=0.0.1-ohos.1、kotlin.native.ignoreDisabledTargets=true、kotlin.native.distribution.downloadFromMaven=true;gradle/libs.versions.toml 只剩一个别名(kotlin = 2.2.21-1.0.0)。上游的 Android/发布/spotless 配置项全部移除。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

社区对纯 Compose 库的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。DhyanToast 引擎核心零第三方依赖,没有 KStore 那种"coroutines 制品线元数据反推 stdlib 版本"的约束——工具链直接钉社区稳定线即可;上游源码是 Kotlin 2.1.0 编的,2.2.21 向后兼容零成本。Gradle 方面 2.2.21 线要求 8.x(KStore 那篇的 2.4.20 线才能留 9.x)——从上游 8.9 升到 8.14.1 对齐 qrose,两条工具链线的经验不能互相套用。

2.3 编译纯 Kotlin 核心

dhyantoast/build.gradle.kts 从 160 行收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
    }
}

上游的 androidTarget、ios×3、macos×2、js、wasmJs 目标,explicitApi、baselineProfile、发布配置全部随替换剥离。实测:

./gradlew :dhyantoast:compileKotlinJvm        # BUILD SUCCESSFUL

commonMain 里的 ToastCategory 一行没动,ToastAlignment 只删了 import androidx.compose.ui.Alignment 和 toComposeAlignment();composeMain 5 个文件字节级未改。

第 3 阶段:引擎建模(技术难点)

3.1 语义镜像清单

ToastEngine 的每一条行为都能在上游 ToastHost.kt / ToastHostState.kt 里找到出处:

上游行为出处引擎实现
toast 按 add 顺序排队,oldest-firsttoastsState.add(toast)entries.add(...),show() 返回 id
hide() 只标记,600ms 后才移除LaunchedEffect { delay(600); removeToast() }markedAtMs 时间戳,tick() 里到期清除(ExitWindowMs = 600)
出现有 50ms 延迟ToastItem 里 delay(50); setIndexAppearedhasAppeared = nowMs >= createdAtMs + 50
出现前 offset ±34dp、scale 0.97、alpha 0targetOffsetY/targetScale/targetAlpha 的 !hasAppeared 分支同公式同常量(AppearNudgeDp = 34f、AppearScale = 0.97f)
堆叠序:最新在前positionedIndex = 可见数 - 可见序 - 1同公式,对未标记删除的 toast 计算
堆叠 offset = p×(暂停? height+gap : gap),Top 正向下targetOffsetY 正常分支同公式;gap 暂停 16 / 常规 8(PausedGapDp / StackedGapDp)
scale = 1 − 0.03×p(暂停时 1)targetScale同公式(StackScaleStep = 0.03f)
alpha:标记删除→0、未出现→0、p≥visibleCount→0targetAlpha 三条规则同规则
删除动画 offset:−(height+gap×2)+p×stackStep 再按对齐取符号isMarkedForDeletion 分支同公式
自动消失每 autoDismissDelayMs 标记最旧的未标记 toastwhile(true){ delay; firstOrNull().hide() }tick() 里按 lastAutoDismissAtMs 节奏推进
拖拽中停表循环里只检查 hostState.isDragging()isDragging 时 tick() 跳过自动消失
点按暂停只展开布局,不停表上游循环不看 isPaused忠实保留(见 8.2)
拖拽释放超 ±50dp 消除、超 ±20dp 渐隐onDragEnd 的 velocity 检查 / dragAlpha常量 DragDismissThresholdDp = 50f、DragAlphaThresholdDp = 20f 由 ArkTS 侧执行
默认高度 64dpToastDataImpl 的 height: Dp = 64.dpDefaultToastHeightDp = 64f,入队时 clamp 到 32~256
public fun tick(nowMs: Long) {
    entries.removeAll { entry ->
        val marked = entry.markedAtMs
        marked != null && nowMs - marked >= ExitWindowMs
    }
    draggingIds.retainAll { id -> entries.any { it.id == id } }

    if (!autoDismissEnabled || autoDismissDelayMs <= 0L) return
    if (isDragging || entries.isEmpty()) return
    if (nowMs - lastAutoDismissAtMs < autoDismissDelayMs) return

    val oldest = entries.firstOrNull { it.markedAtMs == null } ?: return
    oldest.markedAtMs = nowMs
    lastAutoDismissAtMs = nowMs
}
3.2 时间注入与快照

引擎不读系统时钟,nowMs 全部由调用方注入(ArkTS 传 Date.now()):一是确定性——同一时间序列必然得到同一动画目标序列;二是 JVM 可测——交叉验证可以在没有设备的情况下对账公式。snapshot(nowMs) 返回每条 toast 的渲染目标值(offset/scale/alpha/gap/height/positionedIndex/阶段标记),不返回插值——插值是宿主的事:

上游用 animateFloatAsState(spring(dampingRatio = 0.85f, stiffness = 300f)) 做动画;鸿蒙侧 ArkTS 用 animateTo({ curve: curves.springMotion(0.35, 0.85) }) 逼近同一组目标值。动画手感留在平台侧,语义留在 Kotlin 侧。

3.3 为什么不直接编 Compose

社区 CMP 线是 1.9.2 / HarmonyOS Kotlin 2.2.21;上游站在 Compose 1.7.3 / Kotlin 2.1.0(implementation(compose.ui) 系 jetbrains compose 插件语法)。把两者混进一个构建不是周末工程。composeMain 保留上游源码的意义:社区 compose.ui ohos 制品与上游版本对齐后,把目录接回 sourceSets、光栅化/渲染路径降级为 fallback——上游资产零丢失。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string(16ms tick + 事件驱动)
  ▼
C++ N-API entry(libdhyantoast_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosdhyantoast.so)
  │ DhyanToastShow / Dismiss / Clear / SetPaused / SetDragging / Configure / Tick / Version
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;每帧传完整快照 JSON 又会让页面复制队列语义——所以选择"C ABI + 目标值 JSON":引擎算,ArkTS 画。

4.2 Kotlin/Native 导出函数
C ABI 符号ArkTS 方法覆盖语义
DhyanToastShowdhyantoastShow(req)showToast(message, title, icon, category, height) 入队
DhyanToastDismissdhyantoastDismiss(req)hide(id):标记 + 600ms 退出窗口
DhyanToastCleardhyantoastClear()clearAll()
DhyanToastSetPauseddhyantoastSetPaused(req)点按暂停:堆叠展开(不停表,忠实上游)
DhyanToastSetDraggingdhyantoastSetDragging(req)拖拽跟踪:自动消失抑制
DhyanToastConfiguredhyantoastConfigure(req)对齐 / visibleCount / 自动消失开关与时长(50060000ms、18 clamp)
DhyanToastTickdhyantoastTick(req)推进队列 + 返回渲染目标快照
DhyanToastVersiondhyantoastVersion()fork 版本握手(真机版本行的数据来源)

JSON 解析用手写 MiniJson(扁平字符串键值,继承自 cmp_qrose 审核模式):不引 kotlinx.serialization,.so 体积保持在 4MB 级。每个导出整体 try/catch,Kotlin 异常永远不跨 FFI,统一走 {"ok":false,"error":"..."} 通道。

4.3 C++ N-API 方法分发

8 个 Napi 包装共用两个模板函数:带参的 CallWithJsonArg(napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回)和无参的 CallNoArg。KN 返回的字符串必须经 libohosdhyantoast_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosdhyantoast_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与 8 个业务符号)。

4.4 CMake 和类型声明
add_library(ohosdhyantoast SHARED IMPORTED)
set_target_properties(ohosdhyantoast PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(dhyantoast_napi SHARED napi_init.cpp)
target_link_libraries(dhyantoast_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosdhyantoast)

OHOS_ARCH 为空或 libohosdhyantoast.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。Gradle 侧 copyKnSoArm64 / copyKnSoX64 把链接产物和 ~/.konan 里的 libc++_shared.so 自动回填 entry/libs/{arm64-v8a,x86_64}。

export const dhyantoastShow: (request: string) => string;
export const dhyantoastDismiss: (request: string) => string;
export const dhyantoastClear: () => string;
export const dhyantoastSetPaused: (request: string) => string;
export const dhyantoastSetDragging: (request: string) => string;
export const dhyantoastConfigure: (request: string) => string;
export const dhyantoastTick: (request: string) => string;
export const dhyantoastVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 帧循环与动画逼近
private pump(): void {
  const raw: string = dhyantoastTick(JSON.stringify({ nowMs: Date.now() }));
  if (raw === this.lastRaw) {
    return;                       // 队列没变化就不触发动画,省 CPU 也省电
  }
  this.lastRaw = raw;
  const reply: TickReply = JSON.parse(raw) as TickReply;
  this.getUIContext().animateTo({ duration: 300, curve: curves.springMotion(0.35, 0.85) }, () => {
    this.frames = reply.toasts ?? [];
    this.hostPaused = reply.paused ?? false;
    this.total = reply.total ?? 0;
  });
}

setInterval(() => this.pump(), 16) 驱动;raw 字符串相等即跳过——引擎快照里没有时间戳字段,状态不变时序列化结果逐字节相同,这个去重天然成立。变化才进 animateTo,translate / scale / opacity 沿 spring 曲线逼近引擎目标值。

5.2 手势与浮层

toast overlay 是全屏 Stack(hitTestBehavior(HitTestMode.Transparent),空区域穿透到下层控制面板),按对齐方向锚定 Top/Bottom。每条 toast 两层 Column:内层卡片(固定高度 + 分类配色 + 圆角 + 阴影),外层占位(height + gap,挂 translate/scale/opacity/手势)。手势语义与上游逐项对齐:

  • PanGesture.onActionStart 上报 setDragging(true),offsetY 实时跟手(直接改 @State dragY,不进 animateTo——对应上游 snap());
  • 松手超过 ±50dp(按对齐方向)调 dismiss,否则回弹;
  • onClick 切换 setPaused;
  • ✕ 显式关闭;拖拽超过 ±20dp 时 alpha 提前归零(对应上游 dragAlpha)。
5.3 控制面板与配色

分类配色表(ArkTS 侧渲染层资产,不进引擎):成功 #15803D、警告 #B45309、错误 #B91C1C、常规 #2D2D2D(上游 ToastStyle 默认底色)、自定义 #4338CA;图标 check/alert/x/info 映射 ✓/⚠/✕/ⓘ 字形。面板提供消息/标题输入、分类与图标 chips、顶部/底部对齐、可见数量 1~5、自动消失开关 + 1.0~6.0s 时长、显示 / 连发×5 / 暂停展开 / 清空、状态行(握手版本 + 队列计数 + 拖拽/暂停标记)。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosdhyantoast)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 8 + MiniJson(继承 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.dhyantoast、versionName 0.0.1-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出 × 8、参数检查、DisposeString
        ├── src/main/cpp/include/libohosdhyantoast_api.h
        ├── src/main/cpp/types/libdhyantoast_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),File → Project Structure →
# Signing Configs 勾选 Automatically generate signature,然后 Run

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

hdc list targets -v
hdc -t <设备序列号> install -r \
  entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.dhyantoast -m entry

四、完整代码对照

4.1 整体架构

io.androidpoet.dhyantoast(commonMain 纯 Kotlin)
    │ ToastCategory(原样) / ToastAlignment(纯枚举) / ToastEngine(语义镜像)
Bridge.kt + MiniJson
    │ DhyanToastShow / Dismiss / Clear / SetPaused / SetDragging / Configure / Tick / Version
libdhyantoast_napi.so
    │ dhyantoast* × 8
Index.ets
    │ 16ms tick + animateTo(springMotion) + PanGesture
    ▼
ArkUI 真机页面(堆叠 toast + 控制面板)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_dhyantoast、app/baselineprofile 移出
gradle/libs.versions.toml只剩 Kotlin 2.2.21-1.0.0 一个别名
build.gradle.kts(根)版本 0.0.1-ohos.1、全局禁用 Ohos*Test* 任务
dhyantoast/build.gradle.kts纯 KMP:jvm() + ohosArm64 + ohosX64
dhyantoast/src/commonMain/.../ToastEngine.kt唯一新增源文件:无头状态机(队列/自动消失/快照)
dhyantoast/src/composeMain/**上游 5 个 Compose 文件,字节级未改、不参与编译
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt8 个 C ABI、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出 × 8、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(19 节、6000+ 字)

4.3 关键 API 对照

层次API作用
上游ToastHostState.showToast(...)入队(oldest-first)
上游ToastHostState.hide(id) / clearAll()标记删除 / 清空
引擎ToastEngine.show / dismiss / clearAll同语义的 Native 实现
引擎ToastEngine.tick(nowMs)退出窗口清除 + 自动消失节奏
引擎ToastEngine.snapshot(nowMs)渲染目标值(offset/scale/alpha/…)
引擎setDragging / setPaused / configure拖拽停表 / 暂停展开 / 运行时配置
NativeDhyanToast* × 8C ABI JSON 导出
N-APIdhyantoast* × 8向 ArkTS 暴露能力
ArkTSpump() / fireCurrent() / fireBurst()帧驱动 / 入队 / 连发

4.4 ArkTS 与 Kotlin 的边界

import { dhyantoastShow, dhyantoastTick } from 'libdhyantoast_napi.so';

dhyantoastShow(JSON.stringify({
  message: 'Hello, Toast!', title: '已保存',
  category: 'success', icon: 'check', nowMs: Date.now()
}));

const reply = JSON.parse(dhyantoastTick(JSON.stringify({ nowMs: Date.now() })));
// reply.toasts[0] → { offsetYDp, scale, alpha, gapDp, heightDp, ... } → animateTo 逼近
public fun snapshot(nowMs: Long): List<Frame> {
    val gapDp = if (paused) PausedGapDp else StackedGapDp
    val visible = entries.filter { it.markedAtMs == null }
    return entries.map { entry ->
        val positionedIndex = /* 可见数 - 可见序 - 1,同上游 */
        Frame(
            offsetYDp = positionedIndex * (if (paused) entry.heightDp + gapDp else gapDp),
            scale = 1f - StackScaleStep * positionedIndex,
            alpha = if (positionedIndex >= visibleCount) 0f else 1f,
            /* ... */
        )
    }
}

两者之间只传输扁平 JSON 字符串与目标值,不传输 Kotlin 对象、Compose 状态或插值帧。

五、关键决策说明

决策 1:语义/渲染分离,而不是抽"零依赖内核"

KStore 走"真编译整库"是因为 kotlinx ohos 制品线补齐了;DhyanToast 的处境相反——compose 全家桶没有任何 ohos 制品,而库的全部价值又恰恰在渲染上。硬抽"内核"没有意义(模型层本来就只有两个纯文件),正确切分是把 ToastHost 的队列语义(什么时候出现、什么时候消失、堆叠怎么排、手势怎么算)镜像进零依赖的 ToastEngine,把绘制留给 ArkTS。语义有对账表(3.1 节),每一行都能指回上游源码行号级别的出处。

决策 2:工具链钉 2.2.21-1.0.0,Gradle 降 8.14.1

引擎核心零第三方依赖,没有"依赖元数据反推工具链"的约束,直接钉社区对纯 Compose 库的稳定线(qrose / haze 先例);Gradle 8.14.1 是该线的配套要求。与 KStore 篇的 2.4.20 + 9.6.1 互为印证:工具链版本跟着依赖走——零依赖就钉稳定线,依赖 kotlinx 新线就跟着升。

决策 3:composeMain 保留不删

上游 5 个 Compose 文件字节级移进 composeMain:默认层级模板不认识这个目录,它不参与任何目标编译;社区 compose.ui ohos 制品与上游 Compose 版本对齐后,把它接回 sourceSets、ArkTS 渲染降级为 fallback。删除等于丢掉上游资产,隔离才是可逆的。

决策 4:目标值驱动,而不是状态推送

C ABI 是同步一来一回,Compose 的响应式(mutableStateListOf + 重组)过不了桥。桥上传输的是渲染目标值:引擎在 snapshot 里算出每条 toast 的 offset/scale/alpha,ArkTS 用 animateTo(springMotion) 逼近——对应上游 animateFloatAsState(spring(...)) 的"目标 + 弹簧"结构。动画手感留在平台侧(ArkUI 的 spring 与 Compose 的 spring 参数无法逐帧相等,但行为模型一致),语义留在 Kotlin 侧。

决策 5:JVM 目标保留交叉验证 + nowMs 注入

引擎在 commonMain,JVM 与 ohos 跑同一份代码。上游没有留测试套件(无 desktopTest),所以交叉验证的形态是:编译级验证(compileKotlinJvm)+ 语义对账(CN 主文档第 13 节给出对账思路:对每个快照断言 scale == 1 - 0.03 * p、alpha 四条规则、Top 对齐 offset 随序递增)。nowMs 注入让引擎确定性可测——同一时间序列必然得到同一目标序列,这是"语义没漂"的可信证明方式。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21(Eclipse Adoptium)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

.\gradlew.bat :dhyantoast:compileKotlinJvm     # BUILD SUCCESSFUL

上游无测试套件,交叉验证落在编译级与语义对账:引擎全部行为有 3.1 节的出处表,常量(600/50/34/0.97/0.03/8/16/50/20/64)逐个与上游源码核对——镜像必须忠实,包括"暂停不停表"这种第一眼像 bug 的行为,否则鸿蒙侧和 Android 侧没法对账。

6.3 HAP 验证

:dhyantoast:compileKotlinJvm: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: DhyanToastShow / Dismiss / Clear / SetPaused / SetDragging / Configure / Tick / Version
         + libohosdhyantoast_symbols 均为全局 T 符号
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a, x86_64}
BuildNativeWithNinja: finished(双 ABI 编出 libdhyantoast_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

KN so(debugShared 未 strip)arm64 约 3.8MB、x86_64 约 3.5MB,含 Kotlin/Native 运行时;每个 ABI 目录三件套(libdhyantoast_napi.so / libohosdhyantoast.so / libc++_shared.so)。

6.4 功能用例

  1. 启动后状态行出现握手结果 桥已就绪 0.0.1-ohos.1(来自真实 Native 调用,非静态文字);
  2. 启动即入队两条欢迎 toast(info 蓝 + 常规深灰),顶部堆叠出现,50ms 出现延迟后 spring 浮现;
  3. 切分类(成功/警告/错误/自定义)+ 图标,点"显示 Toast",配色与图标正确(绿 ✓ / 橙 ⚠ / 红 ✕ / 靛 ⓘ);
  4. 点"连发 ×5",五条不同分类 toast 依次入队,最新的在最前,旧的按堆叠序下沉缩放淡化,超出 visibleCount 的 alpha 归零排队;
  5. 点按任意 toast,堆叠展开(gap 8→16、逐条全高、scale 归 1),再点恢复紧凑;
  6. 上滑 toast 超过 50dp 松手即消除(600ms 退出动画),不足则回弹;拖拽期间自动消失停表;
  7. ✕ 按钮显式关闭对应 toast;
  8. 自动消失按 oldest-first 节奏推进(默认 3s,可配 1.0~6.0s 或关闭);暂停展开状态下自动消失照常推进——上游语义如此;
  9. 切换底部对齐,堆叠方向与滑动消除方向随之翻转;
  10. "清空"一键移除全部;状态行实时显示队列计数与暂停/拖拽标记。

6.5 验证结论

状态机引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的 toast 会话:版本握手、堆叠入队、暂停展开、手势消除与自动消失闭环(见下节效果图)。

七、运行效果

7.1 真机截图在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/DhyanToast.git
cd DhyanToast
export JAVA_HOME="/path/to/jdk-21"
./gradlew :dhyantoast:compileKotlinJvm                                          # 交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.dhyantoast -m entry
hdc shell hilog | grep DhyanToast

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :dhyantoast 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Windows 下 gradlew.bat 引导需要 JAVA_HOME 环境变量——org.gradle.java.home 管不到 wrapper 启动,报错信息完全不提鸿蒙,最容易误诊;
  4. C++ 侧 napi_throw_error / napi_throw_type_error 的第三参必须是 const char*:把 qrose 模板里的字符串字面量泛化成 std::string 拼接后编译失败(no matching function),修正为 (std::string(...) + ...).c_str()——临时对象生命周期覆盖整个调用,安全;
  5. ArkTS struct 的属性名不能与内置通用属性重名:@State align: string 与组件内置 .align() 冲突,报 Type 'string' is not assignable to ...——改名 alignSel;
  6. ForEach 条目组件的修饰链必须完整挂在组件上:给 toast 加"占位层 + 卡片层"两层结构时漏了外层包裹 Column,} 提前闭合箭头函数体,修饰链悬空引发 27 个连锁语法错误(comma operator / any-unknown / unexpected token 全是它)——ArkTS 的 UI 语法错误几乎全是结构错误,先查大括号配对;
  7. DevEco 打开工程后会生成 .clangd / .clang-tidy,要追加 gitignore,否则混进提交;
  8. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. ArkTS 只 import libdhyantoast_napi.so,不要 import libohosdhyantoast.so——HarmonyOS CAdapter 不会把 @CName 业务符号挂到 ArkTS;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前用 git update-index --skip-worktree 保护,证书绝不进入 AtomGit。

8.2 已知问题

  • 鸿蒙侧无 Compose:composeMain 的 5 个上游文件未参与编译,ToastHost 渲染由 ArkTS 复刻(引擎出目标值,动画在 ArkTS);等社区 compose.ui ohos 制品对齐后接回;
  • "点按暂停"不停自动消失的表——上游 ToastHost 的自动消失循环只检查 isDragging() 不看 isPaused,引擎忠实保留;要停表用拖拽或关闭自动消失。如果认为是 bug 应去上游提 issue,不在 fork 里悄悄改语义;
  • 桥是同步一次一调用:Compose 的响应式(mutableStateListOf + 重组、updates 式订阅)不跨 C ABI,ArkTS 以 16ms tick 拉取快照观察状态;
  • MiniJson 是扁平字符串键值解析,不支持嵌套对象/数组载荷(快照 JSON 由引擎手写序列化,不走 MiniJson);
  • 单 host 单例:桥内全局一个 ToastEngine 实例,对应 Demo 的单个 ToastHost;多宿主需要扩展为句柄制;
  • 图标以名字字符串传递,字形映射在 ArkTS 侧;自定义 Painter / ImageVector 不跨桥;
  • android / ios / macos / js / wasmJs 目标剥离(源文件留在树里),发布、spotless、dokka、baselineprofile 一并剥离,本 fork 不发 Maven Central。

8.3 未来优化方向

  • 增加 N-API 线程安全回表(状态变化推送),替代 16ms 轮询,进一步省电;
  • 桥扩展为多宿主句柄制(对应上游多 ToastHost + categories 过滤参数);
  • 把 ToastStyle / ToastTheme 的主题模型补进引擎(分类配色下沉,ArkTS 只换皮);
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 dhyantoast(commonMain 7 文件,5 个纯 Compose)
    → 源码依赖度盘点:模型本来就纯、渲染本来就是 Compose
    → 语义/渲染分离:composeMain 隔离保留 + ToastEngine 语义镜像(逐常量对账)
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1,qrose 先例)
    → 根构建替换(cmp_qrose 模式,buildSrc 删除)
    → ohosArm64 / ohosX64 动态库 + @CName × 8
    → C++ N-API(DisposeString 纪律)+ MiniJson
    → ArkTS 16ms tick + animateTo(springMotion) 目标值逼近
    → ArkUI 真机单页验收(堆叠 / 手势 / 暂停展开 / 自动消失)

9.2 封装层次

  • 引擎层:ToastEngine——队列、自动消失节奏、退出窗口、堆叠目标值(上游语义镜像);
  • 模型层:上游原版 ToastCategory、ToastAlignment(零依赖,原样编译);
  • 渲染层:上游 5 个 Compose 文件隔离保留(未编,待接回);
  • Native 层:生成双 ABI 动态库并通过 8 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:帧循环、spring 动画逼近、手势、控制面板与状态行;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线取决于库的"可移植切面"在哪里:KStore 全是内核 → 真编译整库;DhyanToast 全是 UI → 语义/渲染分离。不存在万能路线,盘点 import(哪些文件站在什么依赖上)永远第一步——模型本来就纯的库,切分线自己会浮出来;
  2. UI 库跨界的正确单位是"渲染目标值":offset/scale/alpha 目标 + 宿主弹簧动画逼近,让动画手感留在平台侧、语义留在 Kotlin 侧。比传插值帧省带宽(16ms 只在变化时传),比让页面自算语义可靠(公式不会有两份真相);
  3. 忠实镜像包括那些"像 bug"的行为:上游"暂停不停表"第一眼就是 bug,但 fork 改了语义就和 Android 侧对不上账。镜像的纪律是:行为定义一字不改地搬,有疑问记进已知限制、去上游提 issue——这比"顺手修好"可贵得多。

9.4 适配成果

当前适配已完成:

  • DhyanToast 0.0.1(上游 Apache-2.0)队列状态机的双 ABI Kotlin/Native 编译(ToastEngine 语义镜像,模型层原样保留,渲染层隔离不删);
  • 8 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 单页验收页面(堆叠入队、连发、暂停展开、手势消除、自动消失配置、版本握手、状态行);
  • JVM 编译级交叉验证 + 语义对账文档 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份 + 6000 字主文档)、本地效果图与本文。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

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

更多推荐