开源鸿蒙平台 CMP 三方库「DhyanToast」适配全流程
本文记录
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 公式)被逐常量复刻进一个零依赖的纯 KotlinToastEngine,编译成 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 / markedForDeletion | 50ms 出现延迟与 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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101-hotspot,Windows) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(从上游 8.9 升级并对齐社区线;2.2.21 线必须 Gradle 8.x,qrose 先例) |
| 依赖 | 引擎核心零第三方依赖;桥上手写 MiniJson |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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.kt | Dp 类型 | 移入 composeMain(渲染接口) |
ToastHostState.kt | mutableStateListOf / mutableStateMapOf / remember | 移入 composeMain |
ToastHost.kt | 全套(ui/foundation/animation/material3) | 移入 composeMain |
ToastScaffold.kt | material3 Scaffold | 移入 composeMain |
ToastStyle.kt | Color / 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-first | toastsState.add(toast) | entries.add(...),show() 返回 id |
hide() 只标记,600ms 后才移除 | LaunchedEffect { delay(600); removeToast() } | markedAtMs 时间戳,tick() 里到期清除(ExitWindowMs = 600) |
| 出现有 50ms 延迟 | ToastItem 里 delay(50); setIndexAppeared | hasAppeared = nowMs >= createdAtMs + 50 |
| 出现前 offset ±34dp、scale 0.97、alpha 0 | targetOffsetY/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→0 | targetAlpha 三条规则 | 同规则 |
| 删除动画 offset:−(height+gap×2)+p×stackStep 再按对齐取符号 | isMarkedForDeletion 分支 | 同公式 |
自动消失每 autoDismissDelayMs 标记最旧的未标记 toast | while(true){ delay; firstOrNull().hide() } | tick() 里按 lastAutoDismissAtMs 节奏推进 |
| 拖拽中停表 | 循环里只检查 hostState.isDragging() | isDragging 时 tick() 跳过自动消失 |
| 点按暂停只展开布局,不停表 | 上游循环不看 isPaused | 忠实保留(见 8.2) |
| 拖拽释放超 ±50dp 消除、超 ±20dp 渐隐 | onDragEnd 的 velocity 检查 / dragAlpha | 常量 DragDismissThresholdDp = 50f、DragAlphaThresholdDp = 20f 由 ArkTS 侧执行 |
| 默认高度 64dp | ToastDataImpl 的 height: Dp = 64.dp | DefaultToastHeightDp = 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 方法 | 覆盖语义 |
|---|---|---|
DhyanToastShow | dhyantoastShow(req) | showToast(message, title, icon, category, height) 入队 |
DhyanToastDismiss | dhyantoastDismiss(req) | hide(id):标记 + 600ms 退出窗口 |
DhyanToastClear | dhyantoastClear() | clearAll() |
DhyanToastSetPaused | dhyantoastSetPaused(req) | 点按暂停:堆叠展开(不停表,忠实上游) |
DhyanToastSetDragging | dhyantoastSetDragging(req) | 拖拽跟踪:自动消失抑制 |
DhyanToastConfigure | dhyantoastConfigure(req) | 对齐 / visibleCount / 自动消失开关与时长(50060000ms、18 clamp) |
DhyanToastTick | dhyantoastTick(req) | 推进队列 + 返回渲染目标快照 |
DhyanToastVersion | dhyantoastVersion() | 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.kt | 8 个 C ABI、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出 × 8、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
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 | 拖拽停表 / 暂停展开 / 运行时配置 |
| Native | DhyanToast* × 8 | C ABI JSON 导出 |
| N-API | dhyantoast* × 8 | 向 ArkTS 暴露能力 |
| ArkTS | pump() / 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 功能用例
- 启动后状态行出现握手结果
桥已就绪 0.0.1-ohos.1(来自真实 Native 调用,非静态文字); - 启动即入队两条欢迎 toast(info 蓝 + 常规深灰),顶部堆叠出现,50ms 出现延迟后 spring 浮现;
- 切分类(成功/警告/错误/自定义)+ 图标,点"显示 Toast",配色与图标正确(绿 ✓ / 橙 ⚠ / 红 ✕ / 靛 ⓘ);
- 点"连发 ×5",五条不同分类 toast 依次入队,最新的在最前,旧的按堆叠序下沉缩放淡化,超出
visibleCount的 alpha 归零排队; - 点按任意 toast,堆叠展开(gap 8→16、逐条全高、scale 归 1),再点恢复紧凑;
- 上滑 toast 超过 50dp 松手即消除(600ms 退出动画),不足则回弹;拖拽期间自动消失停表;
- ✕ 按钮显式关闭对应 toast;
- 自动消失按 oldest-first 节奏推进(默认 3s,可配 1.0~6.0s 或关闭);暂停展开状态下自动消失照常推进——上游语义如此;
- 切换底部对齐,堆叠方向与滑动消除方向随之翻转;
- "清空"一键移除全部;状态行实时显示队列计数与暂停/拖拽标记。
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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:dhyantoast真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Windows 下
gradlew.bat引导需要JAVA_HOME环境变量——org.gradle.java.home管不到 wrapper 启动,报错信息完全不提鸿蒙,最容易误诊; - C++ 侧
napi_throw_error/napi_throw_type_error的第三参必须是const char*:把 qrose 模板里的字符串字面量泛化成std::string拼接后编译失败(no matching function),修正为(std::string(...) + ...).c_str()——临时对象生命周期覆盖整个调用,安全; - ArkTS struct 的属性名不能与内置通用属性重名:
@State align: string与组件内置.align()冲突,报Type 'string' is not assignable to ...——改名alignSel; - ForEach 条目组件的修饰链必须完整挂在组件上:给 toast 加"占位层 + 卡片层"两层结构时漏了外层包裹 Column,
}提前闭合箭头函数体,修饰链悬空引发 27 个连锁语法错误(comma operator / any-unknown / unexpected token 全是它)——ArkTS 的 UI 语法错误几乎全是结构错误,先查大括号配对; - DevEco 打开工程后会生成
.clangd/.clang-tidy,要追加 gitignore,否则混进提交; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - ArkTS 只 import
libdhyantoast_napi.so,不要 importlibohosdhyantoast.so——HarmonyOS CAdapter 不会把@CName业务符号挂到 ArkTS; - 签名材料由 DevEco 自动写入
build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧无 Compose:
composeMain的 5 个上游文件未参与编译,ToastHost渲染由 ArkTS 复刻(引擎出目标值,动画在 ArkTS);等社区compose.uiohos 制品对齐后接回; - "点按暂停"不停自动消失的表——上游
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 三条经验
- 适配路线取决于库的"可移植切面"在哪里:KStore 全是内核 → 真编译整库;
DhyanToast全是 UI → 语义/渲染分离。不存在万能路线,盘点 import(哪些文件站在什么依赖上)永远第一步——模型本来就纯的库,切分线自己会浮出来; - UI 库跨界的正确单位是"渲染目标值":offset/scale/alpha 目标 + 宿主弹簧动画逼近,让动画手感留在平台侧、语义留在 Kotlin 侧。比传插值帧省带宽(16ms 只在变化时传),比让页面自算语义可靠(公式不会有两份真相);
- 忠实镜像包括那些"像 bug"的行为:上游"暂停不停表"第一眼就是 bug,但 fork 改了语义就和 Android 侧对不上账。镜像的纪律是:行为定义一字不改地搬,有疑问记进已知限制、去上游提 issue——这比"顺手修好"可贵得多。
9.4 适配成果
当前适配已完成:
DhyanToast0.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。
参考文档
更多推荐



所有评论(0)