本文记录 DeclarativeSnackbar 接入 OpenHarmony 的完整过程,覆盖声明式 snackbar 状态机库盘点、鸿蒙 kotlinx 制品线选型、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI / N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

DeclarativeSnackbar 是基于 kotlinx.coroutines 的 Kotlin Multiplatform 声明式 Snackbar 库:declarative-snackbar-core 提供纯 Kotlin 的 SnackbarComponent<T> 状态机——持有 StateFlow<SnackbarState<T>>,用 show(SnackbarMessage) / hide() / onDestroy() 三个动词管理消息生命周期,Short(3 秒自动隐藏)/ Permanent / Custom(ms) 三种时长,替换语义是"先退场(Hidden 保留旧内容供退场动画渲染)再显示新消息";declarative-snackbar-compose 是站在 compose.ui / compose.foundation 上的 SnackbarBox 渲染层。本次适配走社区已过审的 cmp_qrose / KStore 路线:core 状态机整库真编译为 OpenHarmony KLIB 与双 ABI 动态库(上游 commonMain 全部 6 个文件零改动),compose 渲染层留盘不进构建,ArkTS 以 50ms 轮询 StateFlow 快照驱动 ArkUI 进出场动画,把替换语义的时间行为逐条可视化验收。

项目地址: AtomGit/oh-tpc/DeclarativeSnackbar

上游仓库: AJIEKCX/DeclarativeSnackbar

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

DeclarativeSnackbar 为 Compose Multiplatform 应用回答"消息怎么显示"这一件事:SnackbarComponent<T> 一个组件管到底,show() 提交消息、StateFlow 暴露状态、duration 决定自动隐藏、替换先退场再进场、onDestroy() 收尾生命周期。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享;SnackbarBox 渲染层再按平台用 Compose 画出来。OpenHarmony 应用同样需要这套声明式消息管理,但上游构建(Kotlin 2.2.0 / CMP 1.8.2 / AGP 8.9.3)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行——3 秒自动隐藏由 Kotlin 协程 delay 调度、替换间隙由 animationDuration 计时、onDestroy() 取消协程作用域,这些时间行为必须真的跑在 .so 里。适配需要同时验证状态机引擎、coroutines 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.2.0 / Gradle 8.11.1 / AGP 8.9.3 与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵引擎依赖 kotlinx-coroutines-core,必须有 ohos 变体才能解析。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
状态语义跨不了桥StateFlow 是订阅式推送,show() 的时长调度、替换间隙是 Kotlin 侧的异步时间行为,C ABI 只能同步一来一回。
渲染层跨不过去SnackbarBox 站在 compose.ui / compose.foundation 上(CMP 1.8.2),社区鸿蒙 CMP 工具链与之对不齐。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:状态机引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI / N-API 桥接层。时长调度与替换语义完全由上游原版实现负责,ArkTS 只负责轮询状态、渲染动画和展示结果。

1.2 库提供的能力

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

  • 版本握手:snackbarVersion() 返回 fork 版本号 0.2.1-ohos.1,来自真实 Native 调用而非静态文字;
  • 全局 Snackbar(global 会话):SnackbarComponent<String>,底部对齐,show 提交 Short / Permanent / Custom(200–60000ms) 三档时长的文本消息,hide 隐藏,destroy 销毁后可重建;
  • 局部 Banner(local 会话):SnackbarComponent<BannerMessage>,顶部对齐,title + subtitle 自定义消息类型——泛型 API 的另一边过桥;
  • 替换语义:Shown 状态下再 show(),状态机先转 Hidden(保留旧内容 500ms 供退场动画)再显示新消息,转换日志逐条可见;
  • 状态机监视器:50ms 轮询 StateFlow 快照,当前状态行 + 时间戳转换日志(最新在前,上限 100 条)。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / target回显本次操作与会话(global / local)。
visible当前是否 Shown——动画与渲染的唯一依据。
text / previousTextglobal 会话的当前与上一条内容(Hidden.previousMessageContent)。
title / subtitle / previousTitle / previousSubtitlelocal 会话的当前与上一条内容。
animationMs组件 animationDuration(500ms),ArkTS 动画时长与状态机间隙对齐的依据。

引擎侧完整能力(show / hide / onDestroy、三档时长、替换间隙、退场保留旧内容)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 8 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的状态机逻辑没有漂移。

1.3 实现适配

维度要求
代码复用declarative-snackbar-core 的全部 commonMain 文件原样编译,不提取、不重写;全仓没有一行改动上游源码。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);declarative-snackbar-compose(Compose 渲染层)与 android-sample 留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律版本主体与上游完全一致(coroutines 1.10.2 → 1.10.2-1.0.0),只把仓库换到 ohos 制品线(换线不换版)。
UI 完整单页覆盖全部桥接入口:双会话、三档时长、替换语义、销毁重建、状态监视与版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。declarative-snackbar-compose 的 SnackbarBox / noOverlap* 修饰符依赖 compose.ui,鸿蒙 CMP 工具链与之对不齐,留盘等接回;ArkTS Demo 以轮询 + ArkUI 动画复刻渲染语义,状态机是真件,渲染是参照。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界(core 零 Compose 依赖 / compose 依赖 compose.ui)、
                            确认 coroutines ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、
                            coroutines 1.10.2-1.0.0 换线不换版
第 3 阶段:桥协议建模     ── 双会话(global=String / local=BannerMessage)、
                            show/hide/state/destroy/version 五个 op、状态快照 JSON 契约
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 50ms StateFlow 轮询、500ms 进出场动画、退场保留旧内容、转换日志
第 6 阶段:示例与验证     ── jvmTest 交叉验证、双 ABI so、HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
declarative-snackbar-core/           上游状态机(进 ohos 构建,jvm + ohosArm64 + ohosX64)
  src/commonMain/…/core/             6 个文件:SnackbarComponent / DefaultSnackbarComponent /
                                      SnackbarState / SnackbarContent / SnackbarMessage / SnackbarDuration
  src/jvmTest/…/core/                新增:8 项交叉验证(Dispatchers.setMain + 虚拟时间)
declarative-snackbar-compose/        上游 SnackbarBox 渲染层(compose.ui/foundation 依赖,留盘不进 settings)
android-sample/                      上游 Android 示例,留盘不进 settings
example/nativeApp/                   Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                  DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md       鸿蒙适配说明(中英双份)
README.upstream.md                   上游原文保存
doc/img/                             真机效果图

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :declarative-snackbar-core 与 :example:nativeApp,rootProject.name = "ohos_declarativesnackbar"。上游的 AGP classpath、compose-compiler、CMP、vanniktech 发布插件、binary-compatibility-validator、GitHub publish workflow 全部随根构建替换剥离——本 fork 不发 Maven Central。

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)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(wrapper 由上游 8.11.1 升至此,沿用 qrose 同线钉法)
kotlinxcoroutines 1.10.2-1.0.0(ohos 制品线,版本主体与上游逐字相同)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.declarativesnackbar
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
declarative-snackbar-core6纯 Kotlin + coroutines,零 UI、零 AGP、零 expect/actual整体编译(jvm + 双 ohos)
declarative-snackbar-compose3compose.ui + compose.foundation(SnackbarBox、noOverlap* 修饰符、LocalMessengerOffset)留盘不进构建
android-sample—AGP + Material3 + Navigation Compose留盘不进构建

core 是这次适配的成败手:逐文件确认 6 个源文件只 import kotlinx.coroutines.* 与 kotlin.time.*,没有任何 @Composable / State / androidx 依赖——纯 Kotlin + coroutines 状态机。全仓没有任何 expect/actual,不需要像 KStore 那样补 dispatcher actual,适配面天然最小。

唯一的敏感点是 DefaultSnackbarComponent 内部使用 Dispatchers.Main.immediate 构造协程作用域。上游源码不可改(改了就不再是"原样编译"),处置放在桥的设计上:见第 5.1 节——所有 NAPI 调用都发生在 ArkTS UI 线程,从 UI 线程发起的 Main.immediate 派发要么内联执行、要么排入同线程队列,天然绕开鸿蒙线 Dispatchers.Main 跨线程派发的已知问题(resaca 适配记录过)。

1.4 页面边界
标题区          DECLARATIVESNACKBAR · OPENHARMONY / 版本握手行(CMP 声明式 Snackbar 状态机 · Kotlin/Native ohosArm64/ohosX64)
全局区          消息输入框 + 时长三选(Short 3s / Permanent / Custom + 毫秒输入)+ 显示 / 隐藏 / 销毁重建
局部区          标题 + 副标题输入 + 时长三选 + 显示 / 隐藏(自定义 BannerMessage 类型,顶部对齐)
替换演示        一键触发「消息 A → 800ms 后消息 B」,Hidden 间隙 500ms 在日志中可见
监视器          当前双会话状态行 + 时间戳转换日志(最新在前,上限 100 条)
关于区          版本握手、调用链分层、仓库与社区链接
浮层            底部深色 Snackbar(global)与顶部红色 Banner(local),500ms 进出场动画

页面启动时先调 snackbarVersion() 做握手,再以 50ms 间隔轮询双会话 op=state 快照驱动渲染。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 coroutines 的 ohos 制品。用 .module 元数据逐项验证:

org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2-1.0.0
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published /
            ohosArm64SourcesElements-published / ohosX64SourcesElements-published
  requires: org.jetbrains.kotlin:kotlin-stdlib:2.2.21-1.0.0

三个关键结论:其一,版本主体与上游声明逐字相同(上游就是 1.10.2),只是仓库换线加 -1.0.0 后缀——commonMain 里一行依赖坐标都不用改语义,“换线不换版”;其二,该制品的 stdlib 依赖恰好是 2.2.21-1.0.0,与已过审的 cmp_qrose(qrose / haze 线)工具链完全一致;其三,Maven Central 上的 1.10.2 没有 ohos 变体,必须走 eazytec 的 -1.0.0 线才解析得到。

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

社区已审核的两条钉法:老线 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(qrose / haze),新线 Kotlin 2.4.20-0.1.0-07 + Gradle 9.x(KStore——因为它依赖的 coroutines 1.11.0-0.1.0-07 拄着 2.4 元数据)。本仓的依赖矩阵给出不同答案:coroutines 1.10.2-1.0.0 的 stdlib 恰是 2.2.21-1.0.0,且上游源码本来就是 Kotlin 2.2.0 编的——Kotlin 钉 2.2.21-1.0.0,与上游同为 2.2 代,源码级兼容成本为零;Gradle 按老线经验升 8.14.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()
    }
}

rootProject.name = "ohos_declarativesnackbar"

include(":declarative-snackbar-core")
include(":example:nativeApp")
2.3 编译真实引擎

declarative-snackbar-core/build.gradle.kts 收敛为纯 KMP 模块:

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

kotlin {
    explicitApi()
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.coroutines.core)   // 1.10.2-1.0.0
        }
        jvmTest.dependencies {
            implementation(kotlin("test"))
            implementation(libs.kotlinx.coroutines.test)    // 1.10.2,仅 JVM 解析
        }
    }
}

上游的 explicitApi() 纪律原样保留(源码本就带显式可见性修饰符)。实测:

./gradlew :declarative-snackbar-core:compileKotlinJvm        # BUILD SUCCESSFUL
./gradlew :declarative-snackbar-core:compileKotlinOhosArm64  # BUILD SUCCESSFUL
./gradlew :declarative-snackbar-core:compileKotlinOhosX64    # BUILD SUCCESSFUL

状态机 6 个文件一个没动,全仓唯一新增的 Kotlin 源码在 example/nativeApp(桥)和 jvmTest(测试)。

2.4 测试依赖的处置:coroutines-test 只在 JVM 解析

上游没有任何测试。fork 新增的 8 项语义用例需要 kotlinx-coroutines-test(Dispatchers.setMain + 虚拟时间),它没有 ohos 制品。处置是两条纪律的组合:测试统一在 JVM 跑(jvmTest 源集 + jvmTest.dependencies),根构建全局禁用 ohos 测试任务:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

ohos 侧只承担编译与链接,语义验证全部落在 JVM——同一份 commonMain,JVM 钉住的行为就是 .so 里执行的行为。

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.ajiekcx.declarativeSnackbar.core/**(上游状态机,原样编译)
        ↓ SnackbarComponent<T> / StateFlow<SnackbarState<T>> / show / hide / onDestroy
Bridge.kt(@CName × 2,扁平 JSON 进出,双会话)
        ↓ UTF-8 JSON
N-API snackbarDispatch(request) / snackbarVersion()
        ↓
ArkUI Index.ets(50ms 轮询 + 动画 + 日志)

请求与响应:

请求 : {"op":"show","target":"global","text":"Hello from DeclarativeSnackbar","duration":"short"}
       {"op":"show","target":"local","title":"已保存","subtitle":"更改已同步到云端","duration":"permanent"}
       {"op":"show","target":"global","text":"…","duration":"custom","timeInMillis":1500}
       {"op":"hide","target":"global|local"}
       {"op":"state","target":"global|local"}
       {"op":"destroy","target":"global|local"}
       {"op":"version"}
响应 : {"ok":true,"op":"show","target":"global","visible":true,"animationMs":500,
        "text":"Hello from DeclarativeSnackbar","previousText":null}
       {"ok":true,…,"target":"local","title":"已保存","subtitle":"更改已同步到云端",
        "previousTitle":null,"previousSubtitle":null}
       {"ok":true,"op":"version","version":"0.2.1-ohos.1","upstream":"0.2.1"}
       {"ok":false,"error":"…"}

两个不对称设计:show / hide 的响应附带操作后的状态快照(一次调用即可刷新 UI,不必紧跟一次 state);state 的 previousText / previousTitle 直接来自 SnackbarState.Hidden.previousMessageContent——上游 SnackbarBox 退场动画渲染的就是它,ArkTS 侧照同样语义处理。

3.2 双会话:泛型的两边都要过桥

上游示例同时使用 SnackbarComponent<String>(全局底部)与自定义消息类型(局部、顶部对齐)。桥以两个会话复刻,T 的两边都跨 C ABI:

private val globalSession = Session<String> { SnackbarComponent() }
private val localSession = Session<BannerMessage> { SnackbarComponent(animationDuration = 500.milliseconds) }

class BannerMessage(val title: String, val subtitle: String)   // 桥层消息类型,不触碰上游源码

BannerMessage 是桥层新增类型,证明 SnackbarContent<T> 的 T 可以是任意类型——JSON 进、Kotlin 对象出,ArkTS 永远不持有 Kotlin 对象。会话层还承载生命周期策略:onDestroy() 对一个组件实例是终态(协程作用域已取消),桥的 destroy op 调用后置空会话,下次调用重建新实例——桥侧策略等价于"重新进入屏幕"。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("SnackbarDispatch")
fun snackbarDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、不支持的 op、组件构造失败(含极端情况下 Main dispatcher 不可用的异常原文),全部走同一个 ok:false 通道,ArkTS 侧统一渲染在页面顶部红条。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libdeclarativesnackbar_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosdeclarativesnackbar.so)
  │ SnackbarDispatch / SnackbarVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制状态机语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("SnackbarDispatch")
fun snackbarDispatch(request: String): String   // show / hide / state / destroy / version 一口进

@CName("SnackbarVersion")
fun snackbarVersion(): String = "0.2.1-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
SnackbarDispatchsnackbarDispatch(req)show(SnackbarMessage) 构造 + hide() + state.value 快照 + onDestroy()
SnackbarVersionsnackbarVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"snackbarDispatch", nullptr, NapiSnackbarDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"snackbarVersion", nullptr, NapiSnackbarVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosdeclarativesnackbar_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosdeclarativesnackbar_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

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

add_library(declarativesnackbar_napi SHARED napi_init.cpp)
target_link_libraries(declarativesnackbar_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosdeclarativesnackbar)

OHOS_ARCH 为空或 libohosdeclarativesnackbar.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const snackbarDispatch: (request: string) => string;
export const snackbarVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 轮询和触发操作
aboutToAppear(): void {
  this.version = snackbarVersion();            // 版本握手:来自真实 Native 调用
  this.pollTimer = setInterval(() => this.poll(), 50);
}

private poll(): void {
  const g = this.dispatch({ op: 'state', target: 'global' });
  const l = this.dispatch({ op: 'state', target: 'local' });
  this.apply(g);
  this.apply(l);
}

private showGlobal(): void {
  const req: DispatchRequest = {
    op: 'show', target: 'global',
    text: this.globalText, duration: this.globalDuration
  };
  if (this.globalDuration === 'custom') {
    req.timeInMillis = this.globalCustomMs;
  }
  this.apply(this.dispatch(req));              // 响应自带状态快照,立即刷新
}

apply 是渲染核心:按 target 分流,用 ${visible}|${text}|${previousText} 组合键去重——状态没变不重放动画;状态翻转时更新 @State 驱动浮层 translate 过渡,并写入时间戳日志。首帧快照静默同步(不动画、不记日志),页面重进不产生假转换。

5.2 页面交互

单页自上而下:全局区(消息输入 + 时长三选 + Custom 毫秒输入 + 三按钮)、局部区(标题/副标题 + 同款时长选择)、替换演示(一键「消息 A → 800ms 后消息 B」)、状态机监视器(双会话当前状态行 + 转换日志,[HH:mm:ss.SSS] 时间戳最新在前、上限 100 条)、关于区(版本握手、调用链分层、AtomGit 仓库与社区链接)。

浮层动画复刻上游 SnackbarBox 语义,两个关键实现:

  • 常驻节点 + translate 移出:底部 Snackbar 与顶部 Banner 常驻组件树,隐藏态分别停在屏幕下方(position y=100%)与上方(translate -120%),状态翻转时 .animation(500ms) 插值——节点若随状态条件挂载,会直接以最终位置出现,没有滑入动画;
  • 退场保留旧内容:visible 翻 false 时先把当前内容存入 exitText,浮层在退场动画期间渲染它(对应上游 previousMessageContent),动画结束(500ms + 余量)后清空。
5.3 对比度和错误边界

底部 Snackbar 深色(#111827)白字、顶部 Banner 红色(#DC2626)白字,语义与告警级别对齐;ok:false 时错误原文进页面顶部红条与 hilog,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosdeclarativesnackbar)+ copyKnSo*
│   └── src/ohosMain/kotlin/Bridge.kt         @CName × 2 + MiniJson + 双会话(继承 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.declarativesnackbar、versionName 0.2.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 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosdeclarativesnackbar_api.h
        ├── src/main/cpp/types/libdeclarativesnackbar_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :declarative-snackbar-core:jvmTest                                  # 8 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 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.declarativesnackbar -m entry

四、完整代码对照

4.1 整体架构

io.github.ajiekcx.declarativeSnackbar.core/**(上游状态机原样编译)
    │ SnackbarComponent<T> / SnackbarState / 三档时长 / 替换间隙 / onDestroy
Bridge.kt + MiniJson
    │ SnackbarDispatch / SnackbarVersion
libdeclarativesnackbar_napi.so
    │ snackbarDispatch / snackbarVersion
Index.ets
    │ 50ms 轮询 + JSON parse + @State 更新
    ▼
ArkUI 真机页面(单页 + 双浮层)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_declarativesnackbar、compose/android-sample 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 + coroutines 1.10.2-1.0.0 钉版
build.gradle.kts(根)版本 0.2.1-ohos.1、全局禁用 Ohos*Test* 任务
declarative-snackbar-core/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,explicitApi 保留
declarative-snackbar-core/src/jvmTest/8 项交叉验证(Dispatchers.setMain + 虚拟时间)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、双会话、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(17 节)
doc/img/snackbar-ohos-demo.png真机效果图

4.3 关键 API 对照

层次API作用
引擎SnackbarComponent<T>(animationDuration)工厂建组件,StateFlow + 三个动词
引擎show(SnackbarMessage)提交消息;Shown 时走替换(退场 → 间隙 → 新消息)
引擎hide()同步置 Hidden,保留 previousMessageContent
引擎onDestroy()取消协程作用域(终态)
引擎SnackbarDuration.Short / Permanent / Custom3000ms / 永久 / 自定义毫秒
NativeSnackbarDispatch / SnackbarVersionC ABI JSON 导出
N-APIsnackbarDispatch(req) / snackbarVersion()向 ArkTS 暴露能力
ArkTSpoll() / showGlobal() / apply(resp)轮询状态、触发操作、驱动动画与日志

4.4 ArkTS 与 Kotlin 的边界

const shown = snackbarDispatch(JSON.stringify({
  op: 'show', target: 'global',
  text: 'Hello from DeclarativeSnackbar', duration: 'short'
}));
// shown.visible === true;3000ms 后轮询将观察到 visible=false、
// previousText='Hello from DeclarativeSnackbar'(退场动画渲染它)
when (json.str("op")) {
    "show" -> globalSession.get().show(
        SnackbarMessage(
            duration = duration(json),           // Short / Permanent / Custom(200–60000)
            content = json.str("text"),
        )
    )
    "hide" -> globalSession.get().hide()
    "state" -> globalState()                     // state.value → 可见性 + 内容 + animationMs
    "destroy" -> globalSession.destroy()         // onDestroy() + 会话置空待重建
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:真编译整库而不是提取零依赖内核

core 只有 6 个文件且零 Compose 依赖、零 expect/actual,ohos 上 coroutines 制品线已齐——再抽"内核"等于手抄状态机,引入漂移风险。真编译让时长调度、替换间隙、退场保留旧内容、生命周期取消全部是上游原件。

决策 2:coroutines 1.10.2-1.0.0 换线不换版,Kotlin 钉 2.2.21-1.0.0

工具链版本跟着依赖走:coroutines -1.0.0 后缀对应 HarmonyOS Kotlin 2.2.21-1.0.0 线(.module 元数据的 stdlib 依赖可证),且上游本来就是 Kotlin 2.2.0,2.2.21 与之同代零降级——比 KStore 升 2.4.20 线少动一处工具链。Gradle 按老线经验升 8.14.1,两条工具链线的经验不能互相套用。

决策 3:根构建替换(cmp_qrose 模式),compose 渲染层与 android-sample 留盘

上游根构建(AGP / compose-compiler / CMP / vanniktech 发布 / BCV 校验)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。declarative-snackbar-compose 是 SnackbarBox 渲染层(compose.ui / compose.foundation,CMP 1.8.2),鸿蒙 CMP 工具链与之对不齐,留盘不进 settings.gradle.kts——它的存在方式只有这一种,等社区 compose.ui ohos 制品与上游 CMP 版本对齐后原样接回。

决策 4:双会话把泛型的两边都推过桥

上游示例的灵魂之一是"类型安全消息":全局 String、局部自定义类型。桥若只接 String,等于只验了泛型的一半。BannerMessage 桥层类型让 SnackbarComponent<BannerMessage> 与 SnackbarComponent<String> 并存,JSON 进、Kotlin 对象出,自定义类型跨 C ABI 的通路一次验完。

决策 5:StateFlow 轮询,而不是推送

C ABI 是同步一次一调用,StateFlow 订阅式推送过不了 C 签名(KStore 对 updates Flow 的处置相同)。但 snackbar 与存储不同:状态随时间演化(3 秒自动隐藏、500ms 替换间隙),“操作后读回"不够。处置是 50ms 轮询:观测粒度远小于最短时间行为(200ms 钳制下限),同一帧内的连续中间态被组合键去重折叠——恰好等价于"肉眼看不到的快速替换”。不引入 N-API 线程安全回表,桥保持单线程。

决策 6:测试钉住语义,而不是只验编译

上游没有测试。fork 新增 8 项 jvmTest 把状态机语义全部钉死,其中"退场间隙抢占"一条最初按直觉写成"被忽略",被测试当场纠正——上游真实行为是间隙中新消息抢占待显示消息(Hidden 分支直接 showMessage 并取消待显示协程)。没有这组测试,这个语义就只能靠读源码脑补;有了它,ohos .so 里的行为与文档断言一致。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

./gradlew :declarative-snackbar-core:jvmTest

8 项用例全部通过(Dispatchers.setMain(StandardTestDispatcher) + 虚拟时间,fork 新增):

测试用例钉住什么
showDisplaysMessageThenAutoHidesAfterShortDurationShort = 3000ms:2999ms 仍显示,3000ms 后 Hidden 且保留旧内容
permanentMessageStaysShownPermanent 常驻(60_000_000ms 虚拟推进仍 Shown)
customDurationIsHonoredCustom(1500) 毫秒级生效
hideHidesShownMessageImmediatelyKeepingPreviousContenthide() 同步置 Hidden,previousMessageContent 供退场动画
showWhileShownReplacesThroughHiddenGap替换语义:Shown(A) → Hidden(A) 499ms → Shown(B) → 3000ms → Hidden(B)
showDuringDismissingGapPreemptsPendingMessage退场间隙中 show 抢占待显示消息(上游真实行为)
showTwiceWhileShownDropsTheSecondUntilGapStartsShown 状态下连发两条:第二条被 dismissingJob 守卫丢弃
onDestroyCancelsAutoHideonDestroy() 后自动隐藏协程被取消,状态停留在 Shown

依赖换线没有 API 漂移:coroutines 1.10.2 换成 1.10.2-1.0.0 线后版本主体不变——"换线不换版"的直接证据。

6.3 HAP 验证

:declarative-snackbar-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 → arm64-v8a/libohosdeclarativesnackbar.so(≈5.3MB)+ libc++_shared.so
linkDebugSharedOhosX64   → x86_64/libohosdeclarativesnackbar.so(≈4.9MB)+ libc++_shared.so
llvm-nm -D(双 ABI): SnackbarDispatch / SnackbarVersion /
                      libohosdeclarativesnackbar_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libdeclarativesnackbar_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

6.4 功能用例

  1. 启动后标题下方版本行显示 0.2.1-ohos.1(来自 snackbarVersion() 真实 Native 调用,非静态文字);
  2. 全局区保持默认文本点「显示」:底部深色 Snackbar 以 500ms 滑入,右侧带 OPENHARMONY 角标;
  3. Short 档 3 秒后自动滑出——自动隐藏由 Kotlin 协程 delay(3000) 调度,期间监视器状态行保持 Shown,到点翻转 Hidden(previous="…");
  4. Permanent 档常驻不自动隐藏,点「隐藏」立即退场;
  5. Custom 档输入 1500ms:1.5 秒后自动隐藏,毫秒级生效;
  6. 局部区点「显示」:顶部红色 Banner(BannerMessage 自定义类型)滑入,标题副标题同屏渲染——泛型另一边过桥成功;
  7. 点替换演示按钮:日志依次出现 global: Shown(A) → global: Hidden(previous="A") → 500ms 后 global: Shown(B),间隙与 animationMs 一致;
  8. 退场期间浮层渲染的是旧内容(previousText),动画结束后清空;
  9. 点「销毁重建」:onDestroy() 后组件无响应,再次「显示」由会话重建新组件,消息正常弹出;
  10. 每步操作的时间戳日志按 [HH:mm:ss.SSS] 滚动留痕,最新在前。

6.5 验证结论

状态机引擎、coroutines 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,底部 Snackbar 与顶部 Banner 按状态机驱动弹出与退场,替换语义的时间行为在转换日志中逐条可见(见下节效果图)。此前唯一无法本机验证的环节——鸿蒙线 Dispatchers.Main.immediate 的可用性——真机运行未出现任何 Main dispatcher 错误,单线程桥设计成立。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/DeclarativeSnackbar.git
cd DeclarativeSnackbar
export JAVA_HOME="/path/to/jdk-21"
./gradlew :declarative-snackbar-core:jvmTest                                # 8 项交叉验证
./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.declarativesnackbar -m entry
hdc shell hilog | grep -E "SnackbarNapi|SnackbarPage"

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :declarative-snackbar-core 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. gradlew 在 JAVA_HOME 未设置时 shell 层直接退出——org.gradle.java.home 还没机会生效,调用前必须显式 export JAVA_HOME;
  4. Kotlin/Native 源集里 @Volatile 解析失败:桥层本就单线程(全部 NAPI 调用在 ArkTS UI 线程),删注解即可,不引入实验 API;
  5. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(报 00303038)——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  6. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  7. ArkTS 报 does not provide an export named 'snackbarDispatch':要么 import 了 KN so(应 import NAPI so——HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS),要么当前 ABI 的 NAPI so 没编出来;
  8. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  9. 根 .gitignore 的 **/build/ 盖不住 DevEco 生成的 example/harmonyApp/.idea/、oh_modules/、.hvigor/,要逐条追加,entry/libs/、*.so / *.har / *.hap 一并忽略;
  10. 上游 Dispatchers.Main.immediate 不可改也不必改:桥全程只在 ArkTS UI 线程调用,从 UI 线程发起的 Main 派发不跨线程,真机验证无异常;
  11. 浮层动画要用常驻节点 + translate 移出屏幕:节点随状态条件挂载会以最终位置出现,没有滑入动画;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前用 git update-index --skip-worktree 保护,证书绝不进入 AtomGit。

8.2 已知问题

  • declarative-snackbar-compose(SnackbarBox / noOverlap* 修饰符 / LocalMessengerOffset)不进鸿蒙构建,等社区 compose.ui ohos 制品与 CMP 1.8.2 对齐后接回;ArkTS Demo 的轮询渲染降级为参照实现;
  • StateFlow 以 50ms 快照跨桥,推送式订阅不跨 C ABI;50ms 内的连续中间态被组合键去重折叠(替换演示的 500ms 间隙远大于该粒度,不受影响);
  • 桥是单线程设计:所有 NAPI 调用必须在 ArkTS UI 线程(Demo 已如此),从工作线程调用未经验证;
  • 替换没有消息队列(上游行为):退场间隙中的新消息抢占待显示消息——jvmTest 已钉住该语义;
  • Custom 时长在桥内钳制 200–60000ms(参数防御);animationDuration 桥内固定 500ms(上游默认值);
  • android / ios 目标与发布链路剥离(源文件留在树里),本 fork 不发 Maven Central。

8.3 未来优化方向

  • 用 N-API thread-safe function 把 StateFlow 变化推到 ArkTS,替代轮询(需处理跨线程回表与生命周期);
  • 把 animationDuration / 对齐方式(顶部/底部)开放为桥参数,完整对齐 SnackbarBox 配置面;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建(build.yml 已就位,待 eazytec 仓库连通性验证);
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 declarative-snackbar-core(6 文件 commonMain)
    → 验证 coroutines ohos 制品线(1.10.2-1.0.0,换线不换版)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1(qrose 同线)
    → 根构建替换(cmp_qrose 模式,compose 渲染层与 android-sample 留盘)
    → ohosArm64 / ohosX64 KLIB(上游源码零改动、零 expect/actual)
    → C ABI(@CName × 2)+ 双会话(String / BannerMessage)
    → C++ N-API(DisposeString 纪律)
    → ArkTS 50ms 轮询 + 常驻浮层动画 + 退场保留旧内容
    → ArkUI 真机单页验收(双会话 / 三档时长 / 替换语义 / 销毁重建)

9.2 封装层次

  • 引擎层:上游原版 SnackbarComponent<T>、SnackbarState、三档时长调度、替换间隙、退场保留旧内容与生命周期取消;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、50ms 轮询、组合键去重、动画驱动、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 工具链版本跟着依赖走,不跟惯性走:coroutines 1.10.2-1.0.0 的 stdlib 恰是 2.2.21-1.0.0,与上游 Kotlin 2.2.0 同代——于是钉老线(qrose 同款)而不是新线(KStore 那是被依赖矩阵逼的),Gradle 8.14.1 同理。选线依据是 .module 元数据,不是"哪个更新";
  2. 渲染与状态机的分层是上游给的天然切缝:core 零 Compose 依赖、compose 全是渲染——整库真编译前者、留盘后者,比在两者之间找折中便宜得多。UI 库适配时先画这条线,适配面就定了;
  3. 轮询观测把状态机的时间行为变成可验收的实物:snackbar 的语义藏在时间里(3 秒自动隐藏、500ms 替换间隙),单次"操作后读回"看不见这些;50ms 轮询 + 时间戳日志让 Shown(A) → Hidden(A) → [500ms] → Shown(B) 逐条落在屏幕上,真机验收从"看着像"变成"对得上"——配合 8 项 jvmTest,语义断言两头钉死。

9.4 适配成果

当前适配已完成:

  • DeclarativeSnackbar 0.2.1 真实状态机的双 ABI Kotlin/Native 编译(上游 commonMain 6 个文件零重写,全仓零 expect/actual);
  • HarmonyOS coroutines 制品线在 snackbar 库上的完整落地(1.10.2-1.0.0,版本主体与上游逐字相同);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • 双会话 DevEco Stage ArkUI 验收页面(全局 Snackbar / 局部 Banner / 三档时长 / 替换语义 / 销毁重建 / 状态监视 / 版本握手);
  • 8 项 JVM 交叉验证全绿(fork 新增,含替换抢占语义纠偏)+ HAP 构建、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)、上游原文存档和真机效果图。

适配过程 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐