开源鸿蒙平台 CMP 三方库「Swiss Knife」适配全流程
本文记录
Swiss Knife接入 OpenHarmony 的完整过程,覆盖对话框组件库盘点、Compose 依赖度定界、纯 Kotlin 决策内核抽取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机双 Tab 页面、HAP 构建和真机验收。
Swiss Knife是基于 Compose Multiplatform 的 UI 工具库:SKAlert提供 scrim 遮罩 + 内容完全自定义的警告框(显隐由调用方visible驱动、onHide(reason)区分 Back / TouchOutside、onHidden在退场动画结束后回调、内容实时收到animationProgress);SKBottomSheet提供底部弹窗(滑入滑出、拖拽关闭、hideOnDragThreshold阈值判定、嵌套滚动联动、scrim 透明度跟随弹窗位置)。本次适配与 KStore 的"真编译整库"路线不同:全库只有 Compose 渲染代码,而鸿蒙工具链没有兼容的 Compose 发行版,因此走 cmp_qrose / AestheticDialogs / compose-material-dialogs 的同款路线——把两个组件的决策层(拖拽状态机、嵌套滚动消费规则、隐藏判定、系统栏解析、动效参数解析)抽成纯 Kotlin 内核编进.so,Compose 渲染层原样隔离留盘,ArkUI 页面按内核 spec 用原生组件重绘,所有设计值全部来自 Native 调用。
项目地址: AtomGit/oh-tpc/swiss-knife.git
上游仓库: ilyadreamix/swiss-knife
上游基线:
ilyadreamix/swiss-knife1.0.11(本 fork1.0.11-ohos.1)。
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Swiss Knife 为 Compose Multiplatform 应用解决"对话框与弹窗"这件事:SKAlert 一行声明 scrim + 居中卡片,隐藏原因(Back / TouchOutside)作为枚举回调给调用方,由调用方决定何时翻转 visible;SKBottomSheet 在此之上加了拖拽关闭——totalDrag 累计、progress = 1 - max(0, totalDrag / height) 跟手、松手时 dragFraction >= threshold(默认 0.4)判走否则回弹,内部列表滚到顶后继续下拉的剩余位移交给弹窗(onPostScroll)、弹窗位移后上滑先弹回(onPreScroll)、拖拽进行中的 fling 直接转拖拽终判(onPreFling)。平台的唯一缝隙是 expect fun SKDialogHost(...):Android 用原生 Dialog + 独立 ComposeView,iOS / jvm 用 Compose Dialog 全屏参数组合,系统栏图标明暗分别走 WindowInsetsControllerCompat / UIApplication.setStatusBarStyle。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套对话框能力,但上游构建(Kotlin 2.3.21 / Gradle 9.5.0 / Compose 1.11.1 / AGP 9.2.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以画出弹窗,却无法证明拖拽阈值判定、嵌套滚动消费规则、隐藏原因仲裁这些库的语义在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证决策内核、动效参数解析、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.21 / Gradle 9.5.0 与鸿蒙工具链无法共存于一个 Gradle 构建;AGP 9.2.1 的 com.android.kotlin.multiplatform.library 在鸿蒙 Kotlin 上解析不了。 |
| Compose 依赖 | 全库 825 行 Kotlin 全部 import androidx.compose.*,而鸿蒙 Kotlin 2.2.21-1.0.0 工具链没有任何 Compose 发行版——逐行编译过不去。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 手势语义跨不了桥 | Animatable、NestedScrollConnection、手势仲裁是 Compose 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:决策内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。Compose 渲染完全由隔离留盘的上游原版实现负责,ArkTS 只负责按 spec 渲染、调 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供两个真实组件会话,对应桥接的全部入口:
- SKAlert:
alert.spec一次性解析渲染/行为 spec(scrim0x80000000即上游Color.Black.copy(alpha = 0.5f)、对齐 Center、双栏 Light 图标、动效参数),运行时alert.back/alert.touchOutside判定能否关闭并返回 reason;ArkTS 侧 scrim 透明度 = spec alpha ×animationProgress(上游copy(alpha = a * progress)公式),内容卡片收到 progress 做同步缩放淡入; - SKBottomSheet:
sheet.spec解析容器/隐藏选项/阈值校验(require(threshold in 0f..1f)逐字保留上游报错文案),拖拽条手势逐帧喂sheet.drag.move(snapTo 跟手)、松手喂sheet.drag.end拿shouldHide判定;内层列表onScrollFrameBegin接sheet.scroll.pre/sheet.scroll.post复刻嵌套滚动消费规则,onScrollStop接sheet.fling.pre; - 隐藏三通道:Back(页面
onBackPress→ 内核仲裁consume)、TouchOutside(scrim 点击 → 内核仲裁)、Caller(调用方直接翻转 visible——上游"调用方驱动"契约本身); onHidden闭环:退场动画结束后回调(tween 按 duration+delay 精确延时,spring 用 480ms 观察窗近似);- 版本握手:
ping返回 fork 版本号1.0.11-ohos.1与目标列表,来自真实 Native 调用而非静态文字。
引擎侧完整语义(SKAlertState / SKBottomSheetState 拖拽状态机、SKBottomSheetNestedScrollConnection 消费规则、SKDialogHostSystemUIOptions 解析表)全部在内核 engine/ 包里以纯函数形式进入 .so,JVM 侧保留 18 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的判定逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 语义复用 | 决策逻辑从上游 SKAlertState / SKBottomSheetState / SKBottomSheetNestedScrollConnection 逐字移植(数值、缺省值、报错文案全保留);Compose 源码零改动整体隔离。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,core / dialogs 出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 examples/{core,android,ios} 与 AGP/Compose/AndroidX 坐标留盘不进构建。 |
| 桥接稳定 | 单个 C ABI SwissKnifeBridge + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 状态纪律 | 弹窗运行时状态(progress / totalDrag / height)由 ArkTS 持有,内核全部纯函数——C ABI 无会话、无锁。 |
| UI 完整 | 双 Tab 覆盖全部桥接入口:配置开关、阈值/圆角/scrim 滑杆、双组件全行为面、协议日志和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
Swiss Knife的本质输出是对话框的行为决策而非像素,鸿蒙侧由 ArkTS 按 spec 用原生组件重绘渲染面,渲染值全部来自.so。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与 Compose 依赖度、确定"决策内核"切面、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、composeMain 隔离
第 3 阶段:桥协议建模 ── 14 op 扁平 JSON 契约、状态归属、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和 DisposeString
第 5 阶段:ArkUI 宿主 ── 双 Tab、手势/滚动接入、协议日志和版本握手
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
core/ 上游基础模块(进 ohos 构建:SKInsets 纯值移植 + jvm/ohos 双目标)
core/src/composeMain/ 上游 SKInsets(Dp 版) / AnimatableExtensions(隔离,不编译)
dialogs/ 决策内核(进 ohos 构建:SKDialogHostSystemUIOptions + engine/ 五件)
dialogs/src/composeMain/ 上游 SKAlert / SKBottomSheet / SKDialogHost actual 全量(隔离,不编译)
dialogs/src/jvmTest/ 18 项交叉验证用例
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkUI 双 Tab 页面
examples/{core,android,ios} docs/ 上游示例与文档资产,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :core、:dialogs 与 :example:nativeApp,rootProject.name = "ohos_swiss-knife"。上游的 AGP / Compose / AndroidX / maven-publish 坐标全部随根构建替换剥离——本 fork 不发 Maven Central,上游消费者继续走 JitPack(jitpack.yml 原样保留)。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin jdk-21.0.12.101-hotspot) |
| Gradle | 8.14.1(wrapper 从上游 9.5.0 钉回,与社区已审核组合一致) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| 产物 | libohosswissknife.so(sharedLib,仅桥模块开启)+ libswissknife_napi.so(CMake) |
| Demo | bundleName org.terminator.ohos.swissknife,compatibleSdkVersion 5.1.0(18),双 ABI |
1.3 源码依赖度盘点
全库 825 行 Kotlin 的 Compose 依赖度普查结论:
| 源文件 | 依赖面 | 处置 |
|---|---|---|
SKInsets.kt(core) | WindowInsets / Modifier.padding(构造扩展部分) | 纯值数据类抽回 commonMain(Dp→Double),原文件进 composeMain |
AnimatableExtensions.kt(core) | androidx.compose.animation.core.* | 整文件进 composeMain |
SKDialogHost.kt(dialogs) | @Composable expect | 整文件进 composeMain;绑在文件里的纯类型 SKDialogHostSystemUIOptions 抽回 commonMain(cmp_qrose 对 Code128Type 的同款规则) |
SKAlert.kt / SKAlertState.kt | Compose UI + Animatable | 整文件进 composeMain;决策面(隐藏规则、scrim、对齐、动效解析)移植进 SKAlertEngine |
SKBottomSheet*.kt(4 个文件) | Compose UI + Animatable + NestedScrollConnection | 整文件进 composeMain;决策面(状态机、消费规则、校验)移植进 SKSheetEngine |
android/ios/jvm 的 SKDialogHost actual | 平台 UI | 原样进 composeMain 留盘(给上游 PR 留完整上下文) |
结论:库的本质输出是行为决策而非像素——SKAlert 的价值在"哪些手势能关、关的原因是什么、scrim 和动效长什么样",SKBottomSheet 的价值在"拖多远算关、列表的位移怎么分"。这些逻辑不依赖 Compose 运行时,抽成纯函数后天然可编进 KN。
1.4 页面边界
ArkUI 页面按"验收宿主"定位:双 Tab(SKAlert / SKBottomSheet),每 Tab 上半是配置区(开关/滑杆),下半是 bridge log 面板记录每次 JSON 进出;遮罩、卡片、弹窗面板全部按 spec 渲染,页面自身不定义任何设计值。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
Swiss Knife 没有 Kotlin 依赖(连 kotlinx-serialization 都没有),制品线风险集中在一处:鸿蒙工具链上不存在 Compose。社区 Maven(maven.eazytec-cloud.com)能提供 HarmonyOS Kotlin 2.2.21-1.0.0(带 ohosArm64() / ohosX64() 工厂与 ohosMain 源集),但上游的 Compose 1.11.1 / AGP 9.2.1 坐标在这条工具链上一个都解析不了。因此:
gradle/libs.versions.toml收缩到只剩一个插件org.jetbrains.kotlin.multiplatform:2.2.21-1.0.0;settings.gradle.kts把 eazytec 双仓(maven-public与content/groups/harmonyos)排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游
:examples:core/:examples:android不进 settings——它们需要 AGP 与 Compose 工具链,一旦 include 整个 sync 就挂。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
| 项 | 上游 | 本 fork | 原因 |
|---|---|---|---|
| Kotlin | 2.3.21 | 2.2.21-1.0.0 | 鸿蒙定制发行版,ohos 目标的唯一来源;与社区已审核批次(qrose / AestheticDialogs / CMD)完全一致 |
| Gradle | 9.5.0 | 8.14.1 | 社区已审核组合按工具链线区分:2.2.21 线降 8.14.1,不赌 9.x 兼容性 |
| JDK | 未钉 | Temurin 21 钉死 | org.gradle.java.home 绝对路径,杜绝 daemon 用错 JDK |
| configuration-cache | 开启 | 移除 | 鸿蒙 Kotlin 插件在 8.14.1 上求稳 |
2.3 编译真实内核
core / dialogs 的构建脚本收缩为同一形状:
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证目标
ohosArm64()
ohosX64()
}
内核五件(engine/ 包)全部纯 Kotlin、零第三方依赖:SKColors(ARGB parse/format/α 乘法)、SKMotion(spring/tween 描述 + ArkUI 参数解析,response = 2π/√stiffness)、SKSystemBars + SKAlignments(图标样式与对齐的解析表)、SKAlertEngine(spec 组装)、SKSheetEngine(拖拽状态机 + 嵌套滚动消费规则)。移植纪律:数值逐字保留——scrim 0x80000000、阈值缺省 0.4、require 报错文案 Parameter \"hideOnDragThreshold\" must be between 0f and 1f、progress 公式、pre/post 消费条件,全部与上游一致。
一个纯 KN 侧的坑:java.lang.Math.PI 在 ohos 目标上报 Unresolved reference 'Math'——commonMain 里必须用 kotlin.math.PI(JVM 编译过不代表 KN 编译过,这也是保留 jvm 目标之外还要真跑 ohos 编译的原因)。
2.4 测试依赖的处置
鸿蒙工具链没有 ohos 目标的测试 runner。处置:jvmTest 依赖 kotlin("test") 在 JVM 上跑(同一份 commonMain 代码,即编进 .so 的那份逻辑),根构建全局禁用名字含 Ohos + Test 的任务,上游零测试文件可改动(上游本来就没有测试——18 个用例全部为本 fork 新写的语义钉子)。
第 3 阶段:桥协议建模
3.1 JSON 契约
dialogs engine/**(纯 Kotlin 决策内核,原样编译)
↓ SKAlertSpec / SKSheetSpec / SKSheetDragState / 纯函数判定
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API swissKnifeBridge()
↓
ArkUI Index.ets(双 Tab + bridge log)
单入口、14 op、协议全扁平(无嵌套对象、无数组——cubic-bezier 传 "x1,y1,x2,y2" 逗号串,insets 拆四个标量字段):
请求 : {"op":"alert.spec","hideOnTouchOutside":true,"hideOnBackPress":true,"scrimColor":"80000000","alignment":"center",...}
{"op":"alert.back","hideOnBackPress":true}
{"op":"sheet.drag.move","totalDrag":0,"amount":38,"height":420}
{"op":"sheet.scroll.post","totalDrag":0,"delta":12,"height":420}
响应 : {"ok":true,"op":"...","consume":true,"reason":"back"}
{"ok":true,"op":"sheet.drag.move","totalDrag":38,"progress":0.9095,"consumed":true}
{"ok":false,"op":"...","error":"..."}
op 清单:ping;alert.spec / alert.back / alert.touchOutside;sheet.spec;sheet.drag.start / sheet.drag.move / sheet.drag.end / sheet.drag.cancel;sheet.scroll.pre / sheet.scroll.post / sheet.fling.pre;sheet.back / sheet.touchOutside。所有请求字段可省略,缺省值即上游默认值。
3.2 状态归属为什么放在 ArkTS 侧
上游的弹窗状态本就住在调用方可观察的 State / Animatable 里(rememberSKBottomSheetState 的 animationProgress、totalDragAmount 只是类的私有字段)。桥没有照搬这个类,而是把它拆成纯函数:状态(progress / totalDrag / height)由 ArkTS @State 持有,每帧手势事件把状态带进内核、内核把新状态还回来。好处是 C ABI 无会话、无锁、无生命周期管理——Kotlin/Native 侧没有 synchronized,无状态让并发问题不存在;代价是 ArkTS 侧多几个 @State 字段,而这正是 ArkUI 声明式渲染最自然的形态。动效执行同理:内核把 Compose AnimationSpec 解析成数(spring:response 秒 + dampingFraction;tween:duration/delay/曲线四元数),ArkUI animateTo 执行——spring 是连续物理动画,逐帧复刻无意义,参数化近似(response = 2π/√stiffness)是社区已认可的做法。
3.3 错误通道
入口整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("SwissKnifeBridge")
public fun swissKnifeBridge(request: String): String {
var op = ""
return try {
val req = MiniJson.parse(request) as? J.Obj ?: throw JsonParseException("request must be an object")
op = (req.values["op"] as? J.Str)?.v ?: throw JsonParseException("missing op")
val response = Response(op)
dispatch(op, req, response)
val frame = response.done()
MiniJson.parse(frame) // 响应帧自检:写完必须能被重新 parse
frame
} catch (t: Throwable) {
"""{"ok":false,"op":${MiniJson.jsonString(op)},"error":${MiniJson.jsonString(t.message ?: t.toString())}}"""
}
}
非法 JSON、缺 op、未知 op、阈值越界(返回上游同款 require 文案)、非法颜色/对齐/动效参数,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到日志面板。响应帧写完必须过一遍 MiniJson 回读自检,保证桥上跑出去的永远是合法 JSON。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
| 层 | 文件 | 职责 |
|---|---|---|
| Kotlin/Native | example/nativeApp/src/ohosMain/.../Bridge.kt | @CName("SwissKnifeBridge") 单入口、14 op 分发、MiniJson、Response 扁平帧 |
| C++ N-API | harmonyApp/entry/src/main/cpp/napi_init.cpp | 参数检查、UTF-8 转换、调用 C 符号、DisposeString 归还 KN 字符串 |
| 类型声明 | cpp/types/libswissknife_napi/{index.d.ts, oh-package.json5} | ArkTS 侧 import { swissKnifeBridge } from 'libswissknife_napi.so' |
三处命名恒定一致:NAPI nm_modname = "swissknife_napi"、ArkTS import libswissknife_napi.so、声明 swissKnifeBridge——名字错一个就是运行期 does not provide an export named。
4.2 Kotlin/Native 导出函数
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class)
@CName("SwissKnifeBridge")
public fun swissKnifeBridge(request: String): String { /* §3.3 */ }
MiniJson 是手写递归下降解析器(J.Obj/Arr/Str/Num/Bool/Null 六型,~200 行)——kotlinx.serialization 在这条工具链上不可用,且协议全扁平,手写比引序列化运行时更小更可控(cmp_qrose MiniJson 同款理由)。两个纯 KN 细节:Kotlin 没有 \f 转义(Java 有),form-feed 用 Char(0x0C) 表达;响应数字输出走 num() 归一化,整值不带 .0 尾巴。
4.3 C++ N-API 方法分发
napi_init.cpp 固定四段:JsToUtf8(两段式 napi_get_value_string_utf8 + std::vector<char>,绕开 API 26 头文件把缓冲区标非 const 的问题)→ 调 SwissKnifeBridge(request.c_str()) → napi_create_string_utf8 回表 → DisposeKnString(通过 libohosswissknife_symbols()->DisposeString 归还 KN 堆字符串,绝不能 free())。模块注册走 napi_module 静态结构 + __attribute__((constructor))。
4.4 CMake 和类型声明
add_library(ohosswissknife SHARED IMPORTED)
set_target_properties(ohosswissknife PROPERTIES
IMPORTED_LOCATION ${KN_SO_DIR}/libohosswissknife.so
IMPORTED_NO_SONAME TRUE)
add_library(swissknife_napi SHARED napi_init.cpp)
target_link_libraries(swissknife_napi PUBLIC
libace_napi.z.so libhilog_ndk.z.so ohosswissknife)
# $ORIGIN rpath + POST_BUILD 把 KN so 和 libc++_shared.so 拷到 NAPI so 旁
Gradle 侧 copyKnSo* 任务把 libohosswissknife.so 与 konan 工具链的 libc++_shared.so(triple aarch64-linux-ohos / x86_64-linux-ohos)拷进 entry/libs/<abi>/,挂为 link 任务 finalizedBy——link 完即拷,HAP 构建前 so 永远就位。实测产物:arm64 libohosswissknife.so 3.84MB、x86_64 3.53MB;llvm-nm -D --defined-only 确认 SwissKnifeBridge 在双 ABI 均为全局 T 符号。
第 5 阶段:ArkUI 宿主
5.1 双 Tab 与触发操作
| Tab | 配置区 | 验收面 |
|---|---|---|
| SKAlert | hideOnTouchOutside / hideOnBackPress 开关、scrim alpha 滑杆、motion 切换(default spring / tween 300ms)、系统栏图标样式 Select | scrim 淡入淡出、卡片 progress 同步缩放、点遮罩/返回键的 reason 仲裁、caller 直关 |
| SKBottomSheet | 四开关(outside/drag/back/nestedScroll)、hideOnDragThreshold 滑杆、cornerRadius 滑杆 | 拖拽跟手(snapTo)、阈值判定/回弹、列表嵌套滚动 pre/post、fling 拦截、三通道隐藏 |
页面根节点 Stack:常规内容 + 条件渲染的两层 overlay(alert / sheet),与上游"SKDialogHost 应包在 if 块里"的用法一一对应。页面级 onBackPress(): boolean 扮演 SKDialogHost 的 back 缝隙:先问内核(alert.back / sheet.back),consume 为真才拦截并触发隐藏——hideOnBackPress=false 时返回 false 让事件继续上传,与上游契约逐字一致。
5.2 页面交互
三条手势/滚动接入路径,对应上游三个决策入口:
- 拖拽条 PanGesture(上游
detectVerticalDragGestures挂在整块内容上,ArkTS 收敛到拖拽条):onActionStart→sheet.drag.start;onActionUpdate累计增量喂sheet.drag.move(PanGesture 的 offsetY 是累计值,需自行差分);onActionEnd→sheet.drag.end,shouldHide走退场否则sheet.drag.cancel回弹; - 内层列表
onScrollFrameBegin(扮演SKBottomSheetNestedScrollConnection):本 SDK 的回调签名是(offset: number) => { offsetRemain: number }——dy<0 且弹窗位移中喂sheet.scroll.pre(先弹回),dy>0 且列表在顶(scroller.currentOffset().yOffset <= 0)喂sheet.scroll.post(剩余位移拖弹窗),消费则offsetRemain返回 0; onScrollStop:接sheet.fling.pre(上游onPreFling只在拖拽进行中拦截),拦截即转sheet.drag.end终判。
progress 更新分两种:拖拽/滚动路径直接赋值(上游 snapTo 语义,无动画);显隐路径包 animateTo 按 spec 动效执行(spring 用 curves.springMotion(response, dampingFraction),tween 用内置 Curve.FastOutSlowIn 预设或字符串曲线)。
5.3 对比度和错误边界
scrim 透明度 = skAlphaOf(spec.scrim) × progress(内核给基色,页面只做上游固定的 α×progress 公式);skWithAlpha 重建 AARRGGBB。所有桥调用包 try/catch,失败帧与 ok:false 统一进 bridge log 面板;每次协议往返(请求 + 响应)带时间戳入日志,最多保留 60 条——真机上出问题第一现场不丢。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosswissknife)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName × 1 + 14 op 分发 + Response 扁平帧
│ └── MiniJson.kt 手写递归下降 JSON(协议全扁平,继承 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.swissknife、versionName 1.0.11-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/libohosswissknife_api.h
├── src/main/cpp/types/libswissknife_napi/{index.d.ts, oh-package.json5}
├── src/main/ets/model/BridgeTypes.ets 协议类型 + skWithAlpha/skCurve 工具
└── src/main/ets/pages/Index.ets ArkUI 双 Tab 验收页面
6.2 构建、签名和安装
./gradlew :dialogs:jvmTest # 18 项交叉验证
./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 回填完整配置)。
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.swissknife -m entry
hdc shell hilog | grep SwissKnife
四、完整代码对照
4.1 整体架构
上游 composeMain(12 个 Compose 源文件,零改动隔离留盘)
↓ 决策面移植(数值逐字保留)
dialogs engine/**(纯 Kotlin 内核:颜色/动效/系统栏/Alert/Sheet 五件)
↓ jvm() 18 项用例交叉验证
Bridge.kt(@CName × 1,扁平 JSON 进出,异常不过界)
↓ UTF-8 JSON
N-API swissKnifeBridge()(DisposeString 纪律)
↓
ArkTS Index.ets(双 Tab:spec 渲染 + 手势/滚动接入 + 协议日志)
↓
HAP(双 ABI 三件套 so)→ 签名 → 真机
4.2 文件清单
| 动作 | 文件 | 说明 |
|---|---|---|
| 改写 | settings.gradle.kts / build.gradle.kts / gradle.properties / libs.versions.toml / wrapper | eazytec 前置、ohos_swiss-knife、钉 JDK21/Kotlin/Gradle、全局禁用 Ohos*Test |
| 改写 | core/build.gradle.kts、dialogs/build.gradle.kts | 去 AGP/Compose/maven-publish,jvm + ohos 双目标,dialogs 依赖 core + kotlin-test |
| 新写 | core/src/commonMain/.../SKInsets.kt | Dp→Double 纯值移植,同包同名(composeMain 原件隔离) |
| 新写 | dialogs/src/commonMain/.../SKDialogHostSystemUIOptions.kt | 从 SKDialogHost.kt 抽回的纯类型(Light/Dark/Automatic) |
| 新写 | dialogs/src/commonMain/.../engine/(5 文件) | SKColors / SKMotion / SKAlertEngine(+SKAlignments) / SKSheetEngine |
| 新写 | dialogs/src/jvmTest/...(2 文件 18 用例) | 语义钉子 |
| 移动 | `core | dialogs/src/composeMain/**` |
| 新增 | example/nativeApp/** | KN 桥模块 |
| 新增 | example/harmonyApp/** | HAP Demo |
| 追加 | .gitignore | build/ 去根锚定、example/harmonyApp/{oh_modules,.hvigor,entry/build,entry/libs,.cxx}、*.so |
| 未动 | jitpack.yml、.github/、docs/、examples/**、dialogs/README.md | 上游资产原样保留 |
4.3 关键 API 对照
| 上游 Compose API | 桥 op / 内核 | 语义保真点 |
|---|---|---|
SKAlert(visible, onHide, hideOptions, onHidden, systemUIOptions, insets, animationSpec, scrimColor, alignment) | alert.spec + alert.back/alert.touchOutside + ArkTS 渲染层 | scrim 缺省 0x80000000;visible ↔ alertMounted;onHide(reason) 枚举一一对应 |
SKAlertHideOptions(hideOnTouchOutside, hideOnBackPress) | alert.spec 同名字段 | 缺省 true |
SKDialogHostSystemUIOptions.SystemBarIconsStyle | statusBarIconsStyle/navigationBarIconsStyle + isSystemDark | Automatic = 深色系统给白图标 |
AnimationSpec<Float>(null / spring / tween) | motionKind=default/spring/tween + 参数 | 缺省 spring = damping 1.0 + stiffness 400(DampingRatioNoBouncy/StiffnessMediumLow);tween 缺省 FastOutSlowIn |
SKBottomSheet(visible, onHide, container, hideOptions, behavior, ...) | sheet.spec + sheet.drag.* + sheet.scroll.* + sheet.fling.pre | 阈值缺省 0.4;require 文案逐字保留;insets 外/内拆分保留 |
SKBottomSheetState.onDragStart/onDrag/onDragEnd | sheet.drag.start/move/end | progress = 1 - max(0, totalDrag/height);fraction >= threshold 才走;终判后 totalDrag 归零 |
SKBottomSheetNestedScrollConnection.onPreScroll/onPostScroll/onScroll/onPreFling | sheet.scroll.pre/post + sheet.fling.pre | pre 只吃 dy<0、post 只吃 dy>0、totalDrag+delta >= 0 才消费、fling 仅拖拽中拦截 |
4.4 ArkTS 与 Kotlin 的边界
- Kotlin 侧(
.so):隐藏规则仲裁、拖拽判定、滚动消费、阈值校验、颜色/对齐/动效/系统栏解析——一切"决策"; - ArkTS 侧:
@State持有 progress/totalDrag/height、animateTo执行动效、手势差分、scrim α×progress 上游固定公式、页面布局与日志——一切"渲染与交互"; - 页面不写死任何设计值:换 scrim、换阈值、换动效、换图标样式,改的都是发给
.so的请求参数。
五、关键决策说明
决策 1:抽决策内核,而不是整库编译或提取渲染层
Swiss Knife 与 KStore 相反:全库是 Compose UI,没有可整库编译的零依赖引擎;但它的本质输出是行为决策。所以切面选在"决策 vs 渲染"上:决策进 .so(可验证、可复用),渲染隔离留盘(给上游与未来 CMP 鸿蒙版留完整上下文)。这与 qrose(编码器 vs Painter)、AestheticDialogs(设计 token vs 组件)、CMD(对话框引擎 vs 组件)是同一条判据——切面由库的本质输出决定。
决策 2:工具链钉 Kotlin 2.2.21-1.0.0,Gradle 降 8.14.1
不追新:社区已审核批次在这套组合上跑通,偏离即踩未验证的坑。jvm() 保留不是摆设——18 个用例在 JVM 上验证的正是编进 ohos .so 的同一份 commonMain 代码。
决策 3:根构建替换(cmp_qrose 模式),上游 examples 留盘
上游 Android/iOS 消费者继续用 JitPack 的 1.0.11(jitpack.yml 未动);鸿蒙构建只 include :core :dialogs :example:nativeApp。composeMain 隔离区让"上游 PR 不带任何 UI diff"成为可核查的事实。
决策 4:无会话纯函数协议,状态由 ArkTS 持有
对比 CMD 的 25 op 有会话协议:swiss-knife 的状态只有三个标量,不值得上会话管理。纯函数协议让桥天然线程安全(Kotlin/Native 没有 synchronized)、让 C ABI 恒定、让 ArkTS 侧的 @State 成为唯一事实源。
决策 5:jvmTest 交叉验证 + 全局禁用 Ohos 测试
鸿蒙工具链没有 ohos 测试 runner;根构建统一禁用 *Ohos*Test* 任务,语义验证全部压到 JVM。18 个用例覆盖:缺省值逐字段、require 报错文案逐字、progress 公式与 clamp、pre/post 消费条件、fling 拦截条件、scrim α 乘法(含饱和/归零)、对齐映射与未知值报错、Automatic 按系统深色解析、动效解析(spring response 数学、tween 逐数透传)。
六、测试与验证
6.1 测试环境
| 项 | 值 |
|---|---|
| OS | Windows 11 x64 |
| JDK | Temurin 21.0.12(org.gradle.java.home 钉死) |
| Gradle | 8.14.1 |
| Kotlin | HarmonyOS 2.2.21-1.0.0 |
| DevEco Studio | 5.1.0 Release(compatibleSdkVersion 5.1.0(18)) |
| 真机 | HarmonyOS ARM64(2in1/手机均可,Demo deviceTypes: ["phone"]) |
6.2 JVM 交叉验证
./gradlew :dialogs:jvmTest # BUILD SUCCESSFUL,18/18 通过
两组用例:SKAlertEngineTest(7 项:缺省 spec、缺省动效数学、tween 透传、对齐映射、系统栏解析、scrim α 乘法与格式化回读、非法参数报错);SKSheetEngineTest(11 项:缺省 spec、阈值校验文案与边界、insets 保真、dragMove 累计/clamp/height=0 不消费、dragEnd 阈值判定与负值归零、scrollPre/scrollPost 消费条件、flingPre 拦截条件、dragStart 归零)。
6.3 HAP 验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
ls -la example/harmonyApp/entry/libs/{arm64-v8a,x86_64} # 各两件:KN so + libc++_shared.so
llvm-nm -D --defined-only .../arm64-v8a/libohosswissknife.so | grep SwissKnifeBridge # T 符号
DevEco 打开 example/harmonyApp(不要打开仓库根目录),勾选自动签名,Run 到真机。
6.4 功能用例
SKAlert:
- Show → scrim 淡入 + 卡片缩放淡入(progress 0→1,bridge log 记录
alert.spec请求); - 点遮罩 →
onHide(SKAlertHideReason.TouchOutside),退场动画结束后onHidden fired; - 关
hideOnTouchOutside再点遮罩 → 日志tap outside ignored,弹窗不关; - 系统返回 →
onHide(...Back);关hideOnBackPress再按返回 →back press propagates,事件上抛; - scrim alpha 滑杆改 30% → 重开遮罩变淡;图标样式切 dark → 弹窗期间状态栏图标变黑;
- motion 切 tween → 300ms FastOutSlowIn 入场。
SKBottomSheet:
- Show → 底部滑入,scrim 跟随;
- 拖拽条下拉 → progress 实时跟手(无动画,snapTo),scrim 同步变淡;
- 下拉越过阈值(默认 0.4)松手 →
onHide(...Drag),滑出后onHidden;不足 →spring back回弹; - 列表滚到顶继续下拉 → 弹窗跟手下移(onPostScroll 路径);弹窗位移后上滑列表 → 先弹回升到顶(onPreScroll 路径)列表才滚动;
- 阈值滑杆调 0 → 轻碰即隐藏;调 1 → 拖满整高才隐藏;
- 关
nestedScroll→ 列表滚动不再带动弹窗; - 遮罩点击 / 返回键 / Close 按钮三条隐藏路径 reason 各自正确。
6.5 验证结论
编译层(:core:/:dialogs:compileKotlinJvm)、语义层(18/18 jvmTest)、链接层(双 ABI so + 自动拷贝)、符号层(SwissKnifeBridge 全局 T)全部通过;功能层与设备层经真机验收通过——首屏 ping 握手显示 swiss-knife 1.0.11-ohos.1 · protocol 1 · ohosArm64,ohosX64,SKAlert 全链路(spec → scrim → 隐藏仲裁 → onHidden)与 bridge log 逐条对得上,见 §7.1 截图。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/swiss-knife.git
cd swiss-knife
export JAVA_HOME="/path/to/jdk-21"
./gradlew :dialogs:jvmTest # 18 项交叉验证
./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.swissknife -m entry
hdc shell hilog | grep SwissKnife
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让决策内核真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游模块(
examples/core、examples/android)绝不能 include 进 settings——它们引用的 AGP/Compose 坐标在鸿蒙 Kotlin 工具链上一个都解析不了,include 即 sync 失败; - Compose 源码不能留在 commonMain:"隔离目录
src/composeMain(任何目标都不编译)"是唯一既能留盘又不进构建的做法;被 UI 文件绑住的纯类型(SKDialogHostSystemUIOptions)要抽回 commonMain; - commonMain 里禁用
java.lang.Math——Math.PI在 JVM 编译通过、在 ohos 目标上报Unresolved reference 'Math',必须用kotlin.math.PI;同理 Kotlin 没有\f转义,form-feed 用Char(0x0C); - ArkTS 严格模式三连:对象字面量必须对应声明的 interface(
arkts-no-untyped-obj-literals);PanGesture是构造函数不是类型,手势链要内联不能当返回类型;onScrollFrameBegin新签名是(offset: number) => { offsetRemain: number },不是旧文档的 Offset 对象进出——SDK 报错信息里的类型名(OnScrollFrameBeginHandlerResult)才是权威; - 弃用 API:
curves.cubicBezier用内置Curve.FastOutSlowIn预设或animateTo字符串曲线替代;getContext(this)换this.getUIContext().getHostContext(); - 根
.gitignore的/build/是根锚定模式,盖不住example/nativeApp/build/——要改成不带锚的build/并单独追加 harmonyApp 的oh_modules/.hvigor/entry build/entry libs/.cxx/*.so; - NAPI 三处命名(
nm_modname/ ArkTS import 名 /index.d.ts导出名)必须恒定一致,错一个就是运行期does not provide an export named; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每 ABI 两件套(KN so +libc++_shared.so)缺一不可; - 全仓文档/代码/默认文案统一 AtomGit 链接,不出现第三方镜像站字样。
8.2 已知问题
- Compose 渲染层不参与鸿蒙构建(
src/composeMain隔离),鸿蒙侧 UI 由 ArkTS 按 spec 重绘,逐帧数值与 ComposeAnimatable不逐位相等; - 弹簧动效是参数化近似:
response = 2π/√stiffness、dampingFraction = dampingRatio(缺省对应springMotion(0.314, 1.0)),观感一致但非逐帧等价; onHidden对 spring 用 480ms 观察窗近似(tween 是精确 duration+delay);- ArkUI 拿不到子列表 fling 速度,fling 拦截改在
onScrollStop触发——只在"拖拽进行中停手"这一上游真正拦截的场景生效; - 拖拽手势收敛到拖拽条(上游挂在整块内容上靠手势仲裁);列表区域的拖拽走嵌套滚动路径,两条路径覆盖的上游行为面相同;
- Android/iOS 目标不在本 fork 构建里(上游仓库继续服务),鸿蒙消费者以 so 三件套集成;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.swissknife匹配的签名。
8.3 未来优化方向
- 把拖拽手势扩到整块弹窗内容(PanGesture 与 List 的手势仲裁调参),进一步对齐上游手势面;
sheet.spec增加maxWidth/elevation的 UI 控件(协议已支持,Demo 未放控件);- 增加 hilog 侧的结构化验收输出与 uitest 自动化截图脚本;
- 评估
.har形态发布内核类型包,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建。
九、总结
9.1 核心难点回顾
上游 825 行 Compose UI(依赖度普查:决策面 vs 渲染面)
→ 决策内核抽取(SKAlertState / SKBottomSheetState / NestedScrollConnection 逐字移植)
→ composeMain 隔离(12 文件零改动留盘,纯类型抽回 commonMain)
→ 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(eazytec 双仓前置)
→ ohosArm64 / ohosX64 KLIB + jvm 18 用例交叉验证
→ C ABI(@CName × 1,14 op 扁平 JSON,无会话纯函数)
→ C++ N-API(DisposeString 纪律)
→ ArkTS spec 渲染(手势/滚动/返回键三条接入路径)
→ ArkUI 真机双 Tab 验收(SKAlert / SKBottomSheet 全行为面 + bridge log)
9.2 封装层次
- 决策层:内核五件(颜色、动效、系统栏、Alert spec、Sheet 状态机与消费规则),数值逐字保留上游语义;
- 隔离层:composeMain 12 个上游文件零改动留盘,git rename 历史完整;
- Native 层:双 ABI 动态库,单 C ABI 输出扁平 JSON;
- N-API 层:参数检查、UTF-8 转换、KN 内存归还;
- ArkTS 层:状态持有、spec 渲染、手势/滚动接入、协议日志与错误文本;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 切面由库的本质输出决定:qrose 切在"编码器 vs Painter"、AestheticDialogs 切在"设计 token vs 组件"、CMD 切在"引擎 vs 组件"、swiss-knife 切在"行为决策 vs 渲染"——对话框库的价值是阈值判定、消费规则和隐藏仲裁,这些不依赖 Compose 运行时,抽出来就能编进
.so; - 工具链组合宁可用社区已审核批次也不要自由发挥:Kotlin
2.2.21-1.0.0+ Gradle 8.14.1 + JDK 21 钉死,eazytec 双仓置前,任何"顺手升个级"都会把已验证的路走成未验证的坑; - 交叉验证是"内核没漂"的唯一可信证明:
require文案逐字、progress 公式逐式、消费条件逐条钉进 jvmTest,Demo 的验收重点才落在"三条隐藏路径的 reason 对不对、列表滚到顶后弹窗跟不跟手"这些行为面上。
9.4 适配成果
当前适配已完成:
- Swiss Knife
1.0.11(fork 版本1.0.11-ohos.1)决策内核的双 ABI Kotlin/Native 编译(拖拽状态机、嵌套滚动消费规则、隐藏仲裁、系统栏与动效解析,数值逐字保留); - 上游 Compose 渲染层 12 文件零改动隔离留盘(composeMain,git rename 历史完整);
- 1 个 C ABI 符号(
SwissKnifeBridge,llvm-nm验证双 ABI 全局 T)和 C++ N-API 包装层(双 ABI so 合计约 11MB 含libc++_shared.so); - 14 op 全扁平 JSON 协议(无会话、纯函数、异常不过界)+ 手写 MiniJson;
- DevEco Stage ArkUI 双 Tab 验收页面(SKAlert / SKBottomSheet 全行为面、bridge log、版本握手);
- 18 项 JVM 交叉验证全绿 + HAP 构建、签名、设备安装和真机验证(效果图见
doc/img/); - 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。
参考文档
更多推荐

所有评论(0)