开源鸿蒙平台 CMP 三方库「SwipeBox」适配全流程
本文记录
compose-swipebox-multiplatform接入 OpenHarmony 的完整过程,覆盖同类库盘点、适配路线裁决、material settle 数学逐字节提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
SwipeBox是 Compose Multiplatform 的滑动手势操作组件库:主内容可向左或向右滑动露出操作按钮区(收藏、删除等),支持双向、双按钮与文本按钮三种形态,滑动语义构建在 Material 的SwipeableState/Modifier.swipeable(实验 API)之上。本次适配与 KStore 的"真编译整库"路线有本质区别:上游依赖的 Material 组件层没有鸿蒙制品,因此走 cmp_qrose / FlexibleBottomSheet / Swipeable-KMP 同一条已审核通过的"内核提取"路线——material 的 settle 决策数学逐字节提取为纯 Kotlin 编译进鸿蒙.so,上游 Compose UI 隔离在composeMain留档,ArkUI 页面通过 NAPI 桥驱动同一份引擎重建滑动交互,锚点、阈值、速度判定与 Android 上完全一致。
项目地址: AtomGit/oh-tpc/compose-swipebox-multiplatform.git
上游仓库: KevinnZou/compose-swipeBox
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
SwipeBox 为 Compose Multiplatform 应用提供"主内容 ↔ 操作按钮"这一件事的完整答案:SwipeBox 组件一行接入,SwipeDirection(StartToEnd / EndToStart / Both)决定可滑方向,startContentWidth / endContentWidth 决定两侧行动区宽度,SwipeIcon / SwipeText / SwipeContent 三个辅助组件快速搭操作按钮,列表场景提供"同时只展开一行、滚动收回"的集成范式。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、WASM 共享。OpenHarmony 应用同样需要这套行内滑动交互,但上游构建(Kotlin 1.9.23 / Compose 1.6.1 / AGP 8.1.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以滑动,却无法证明手势决策在 OpenHarmony ARM64 设备上完成编译、链接和执行——滑出多少确认展开、松手回弹还是全开、快扫怎么判,全都得逐像素复刻且无法对账。适配需要同时验证滑动数学、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 1.9.23 / Gradle 8.2.1 / AGP 8.1.1 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | 上游依赖 compose.runtime / compose.foundation / compose.material(实验 swipeable API),社区 Maven 上的 ohos CMP 制品线是 1.9.2 基线,与上游 1.6.1 / Kotlin 1.9.23 对不齐,硬编等于整套换 runtime。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 手势语义跨不了桥 | SwipeableState 的锚点 settle、速度判定是 Compose 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:material settle 数学的纯 Kotlin 提取与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。滑动决策完全由提取的内核负责,ArkTS 只负责手势采样、插值计时和渲染。
1.2 库提供的能力
Demo 提供一个真实的行内滑动会话,对应桥接的全部入口:
- 锚点几何(create/update):按方向生成锚点集
{0, +startWidthPx, −endWidthPx},与上游SwipeBox喂给SwipeableState的 anchors map 完全一致,响应里回传minOffset/maxOffset/ 双锚位置; - 拖拽(drag):增量 px 喂引擎,
dispatchRawDelta语义 clamp 在极端锚点之间,实时返回 offset 与两侧进度; - 松手判定(end):速度 + 位置双阈值 settle 决策——上游
FixedThreshold(12.dp)与SwipeableDefaults.VelocityThreshold(125.dp)的行为逐字节还原,返回目标锚点与目标离散态; - 落锚回报(frame):settle 动画每 tick 回报插值 offset,落锚 0.5px 内离散态翻转(对应
animateTo完成); - 主动展开/收起(reveal):对应上游
swipeableState.animateTo(...)(操作按钮点击收回); - 版本握手(ping):返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应字段(全部扁平标量):
| 字段 | 作用 |
|---|---|
ok / op / error | 操作是否成功,失败时 error 带原文。 |
offset | 当前偏移 px,正 = start 侧露出,负 = end 侧露出。 |
startProgress / endProgress | 两侧行动区露出进度 0…1(底层宽度 = 进度 × 满宽)。 |
current / target | 离散态(Settled / StartRevealed / EndRevealed)的当前值与目标值。 |
currentInt / targetInt | 上游 SwipeableState<Int> 的锚点值(Settled=0;Start=1;End 在 EndToStart 下=1、Both 下=2),可与 Compose 侧日志 1:1 对照。 |
minOffset / maxOffset / startAnchor / endAnchor | 锚点边界,ArkTS 动画 clamp 用。 |
targetOffset / targetState | settle 类 op 额外携带:插值终点与将落位的离散态。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | material SwipeableState 的 settle 决策(computeTarget / findBounds / FixedThreshold / dispatchRawDelta)提取为 commonMain 纯函数,语义逐字节对齐;上游 4 个 Compose 源文件整体迁入 src/composeMain 留档不删。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 Compose sample(androidApp / desktopApp / iosApp / shared)留盘不进构建。 |
| 桥接稳定 | 单个 C ABI(SwipeBoxBridge)+ 会话式 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| UI 完整 | 单页三 Tab 覆盖全部桥接入口:基础演示五行、列表演示(互斥展开 / 滚动收回 / 删除)、关于(版本握手 + 分层说明)。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
SwipeBox本身就是 UI 组件,其可移植价值在手势决策数学;适配把决策数学留在 Kotlin,把绘制交给 ArkUI。
二、实现路线图
第 1 阶段:项目初始化 ── 同类库盘点与路线裁决、工程边界、工具链矩阵、源码依赖度盘点
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、内核编译
第 3 阶段:桥协议建模 ── 会话式 JSON 契约、锚点/进度字段、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、CMake IMPORTED 与符号纪律
第 5 阶段:ArkUI 宿主 ── 手势采样与差分估速、settle 插值、互斥展开与滚动收回
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
swipebox/ 上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
swipebox/src/commonMain/ 新增内核:SwipeBoxEngine / SwipeSettle / Config / Value / Direction
swipebox/src/composeMain/ 上游 SwipeBox + widget/ 三件套(隔离留档,不编译)
swipebox/src/commonTest/ 22 项 JVM 交叉验证(settle 决策表 + 会话语义)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
sample/ 上游 Compose 示例四件,留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :swipebox 与 :example:nativeApp,rootProject.name = "ohos_swipebox"。上游的 AGP / JetBrains Compose / Dokka / vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 钉死) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.2.1 升级;2.2.21 旧线必须 8.x) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.swipebox |
export JAVA_HOME="/path/to/jdk-21" # Windows: C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot
java -version
1.3 源码依赖度盘点
| 模块 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
swipebox | 4(SwipeBox + widget×3) | compose.material 实验 SwipeableState / swipeable,100% commonMain,零 expect/actual,零平台 API | 内核提取:滑动数学进 commonMain 纯 Kotlin;Composable 迁 composeMain 隔离 |
sample/shared 等 | — | AGP + Compose 插件 + AndroidX | 留盘不进构建 |
上游没有 expect/actual、没有平台能力调用,是纯 UI 组件——适配面天然集中在"手势语义"这一层。没有内核可整库编译(material 无 ohos 制品),也没有算法可光栅化(交互组件无静态输出),因此唯一的可移植内核就是 material SwipeableState 的决策数学。
settle 数学的字节码反证(本适配的成败手):不凭记忆移植。从本地 Gradle 缓存解包 CMP material-desktop 制品,javap 反汇编 FixedThreshold.computeThreshold 与 SwipeableKt.computeTarget,得到两个关键事实:
FixedThreshold.computeThreshold(anchor, other):
anchor + signum(other - anchor) * thresholdPx // 阈值挂"出发锚点"向目标方向固定距离
computeTarget(offset, anchorValue, anchors, thresholds, velocity, velocityThreshold):
bounds = findBounds(offset, anchors) // 围绕松手 offset 的邻近锚点对(<= 与 > 划分)
if anchorValue <= offset (向上滑):
velocity >= velocityThreshold -> upper // 快扫直接越锚
else threshold = bounds.lower + fixedPx // offset 到达才去 upper
else (向下滑):
velocity <= -velocityThreshold -> lower
else threshold = bounds.upper - fixedPx
对 SwipeBox 默认参数(FixedThreshold(12.dp)、125.dp/s),这意味着阈值挂在出发锚点向目标方向 12dp 处:从 0 滑出 12dp 即完全展开;已展开后往回拖 12px 即完全收回——对称、轻扫即响应,正是上游选 12.dp 的意图。若按"阈值挂收起锚点"的错误理解移植,EndToStart 方向会变成"滑过 80% 才展开",与 Android 实机手感完全相反。
1.4 页面边界
标题区 SWIPEBOX · OPENHARMONY / 版本握手行(KMP 滑动操作组件库 · Kotlin/Native ohosArm64/ohosX64 · <version>)
Tab 切换 基础演示 / 列表演示 / 关于
基础演示 五行:Swipe Left(60vp 单删) / Swipe Right(60vp 收藏) / Swipe Left Two Icons(120vp 双按钮)
/ Swipe Both Directions(双向 60vp) / Swipe Left Text 0(140vp"移至收藏夹/删除")
列表演示 19 行 SwipeBoxWithText 同构行:互斥展开、滚动收回、删除行
关于区 版本握手、库简介、四层架构(Kotlin 内核 → @CName → NAPI → ArkTS)、仓库与社区链接
页面启动时先调 ping 做握手,每行在 aboutToAppear 建会话、aboutToDisappear 销毁。
第 2 阶段:目标与依赖打通
2.1 制品线验证(路线裁决的依据)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有可硬编的 ohos Compose 制品。Nexus REST 检索 maven-hw 仓:
org.jetbrains.compose.material:material 1.9.2-0.2.0-01 / -04 / -100 / -test
org.jetbrains.compose.material3:material3 同上
org.jetbrains.compose.ui:ui 1.9.2-0.2.0-04
org.jetbrains.compose.runtime:runtime 1.9.2-0.2.0-04
org.jetbrains.compose:compose-gradle-plugin 1.9.2-0.2.0-04
结论:ohos CMP 制品线存在且完整,但基线是 Compose 1.9.2 + 配套 Kotlin 2.x 新线;上游是 Compose 1.6.1 + Kotlin 1.9.23 + Material2 实验 swipeable。硬编意味着把上游连根拔起换 runtime,且当时没有任何一个已审核通过的仓库走过这条路。裁决:与 KStore"制品齐备就真编译整库"相对——SwipeBox 的制品不匹配,就走 cmp_qrose 内核提取。路线选择必须基于当天的依赖可达性:KStore 的依赖(kotlinx 四件套)与上游同版本有 ohos 变体,SwipeBox 的依赖(material 1.6.1)没有。
2.2 工具链钉版(为什么是 2.2.21-1.0.0)
跟随社区已审核先例(cmp_qrose / FlexibleBottomSheet / Swipeable-KMP 全部钉此版):HarmonyOS Kotlin 2.2.21-1.0.0——ohosArm64() / ohosX64() 工厂函数与 ohosMain 源码集只存在于这个发行版,官方 Maven Central 上没有。该线要求 Gradle 8.x,wrapper 从上游 8.2.1 升到 8.14.1(与先例一致)。settings.gradle.kts 把 eazytec 双仓写在最前:
pluginManagement {
repositories {
// HarmonyOS Kotlin distribution must come BEFORE gradlePluginPortal/google,
// otherwise ohosArm64()/ohosX64() are reported as unresolved references.
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_swipebox"
include(":swipebox")
include(":example:nativeApp")
gradle.properties 三条关键新增:org.gradle.java.home 钉 JDK 21、kotlin.native.ignoreDisabledTargets=true、kotlin.native.distribution.downloadFromMaven=true(Konan 依赖从 Maven 拉)。版本 1.2.0-ohos.1。
2.3 编译提取的内核
swipebox/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
// jvm() is the cross-validation target: the commonTest suite (anchor
// geometry, the material settle decision table and the progress math) runs
// against the same machine source that is compiled into the ohos .so.
jvm()
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies { implementation(kotlin("test")) }
}
}
commonMain.dependencies 为空——内核零依赖(连协程都不用,上游 sample 的 rememberCoroutineScope 是 Compose 侧概念,不进内核)。commonMain 五个文件:SwipeDirection(上游枚举原样抽出)、SwipeBoxValue(离散态)、SwipeBoxConfig(纯 px 配置)、SwipeSettle(决策纯函数三件)、SwipeBoxEngine(会话状态机)。实测:
./gradlew :swipebox:compileKotlinOhosArm64 :swipebox:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.4 测试任务的处置:ohos 无测试运行器
该工具链没有 ohos 测试运行器,处置与先例一致:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
与 KStore"上游测试零改动"不同,本仓的 22 项测试是新写的——上游对滑动数学没有测试(它直接用 material 的),决策表是本次从字节码重建的,所以测试本身就是交付物的一部分(见 6.2)。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.kevinnzou.swipebox/**(提取的 settle 内核 + 会话引擎)
↓ SwipeBoxEngine(锚点 / clamp / settle / 进度)
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API swipeBoxBridge()
↓
ArkUI Index.ets(三 Tab)+ SwipeBoxRow.ets(行组件)
请求与响应:
请求 : {"op":"create","id":"basic0","config":{"direction":"EndToStart","endWidthPx":210,
"positionalThresholdPx":42,"velocityThresholdPx":437.5}}
{"op":"drag","id":"basic0","delta":-24.5}
{"op":"end","id":"basic0","velocity":-1200.0}
{"op":"frame","id":"basic0","offset":-80.2}
{"op":"reveal","id":"basic0","target":"Settled"}
响应 : {"ok":true,"op":"drag","id":"basic0","offset":-24.5,"startProgress":0,
"endProgress":0.153,"current":"Settled","currentInt":0,"target":"Settled",
"targetInt":0,"minOffset":-210,"maxOffset":0,"startAnchor":0,"endAnchor":-210}
{"ok":false,"error":"unknown id"}
两个设计要点:其一,会话式——每行 SwipeBox 一个字符串 id 的 HashMap<String, SwipeBoxEngine>,ArkTS 行组件生命周期与会话生命周期严格对齐(aboutToAppear 建、aboutToDisappear 销毁),列表滚动复用行时不会串会话;其二,响应全部扁平标量——ArkTS 强类型上下文会拒绝 JSON.parse 出的嵌套对象(“Unexpected Object in JSON”),连 config 都平铺在请求里、不搞嵌套回显。
3.2 方向几何与上游锚点值映射
三个方向的锚点布局逐字复刻上游 anchors map:
StartToEnd -> {0: Settled, +startW: StartRevealed} currentInt: Settled=0, Start=1
EndToStart -> {0: Settled, -endW: EndRevealed} currentInt: Settled=0, End=1
Both -> {0: Settled, +startW: Start, -endW: End} currentInt: Settled=0, Start=1, End=2
anchorInt() 保留上游 SwipeableState<Int> 的 Int 值映射,桥响应同时携带语义名(Settled/StartRevealed/EndRevealed)与 Int 值——前者给 ArkTS 状态机,后者留作与 Compose 侧日志 1:1 对账。宽度 ≤0 的方向不生成锚点(上游会把退化的 -0f 键写进 map,内核显式防御)。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("SwipeBoxBridge")
fun swipeBoxBridge(request: String): String = try {
/* op 分发:ping / create / update / drag / end / frame / reveal / reset / dispose */
} catch (t: Throwable) {
"{\"ok\":false,\"error\":\"${t.message?.escapeJson() ?: t::class.simpleName}\"}"
}
非法 JSON、缺 op / id、未知 op、未知会话 id、非法方向/目标名,全部走同一个 ok:false 通道,ArkTS 侧统一处理。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS(SwipeBoxRow.ets / Index.ets)
│ JSON string
▼
C++ N-API entry(libswipebox_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosswipebox.so)
│ SwipeBoxBridge
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让 ArkTS 复制决策语义。所以选择 C ABI + JSON——Kotlin 侧每个数值都是唯一真相源。
4.2 Kotlin/Native 导出函数
@CName("SwipeBoxBridge")
fun swipeBoxBridge(request: String): String // 9 个 op 一口进,会话式分发
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
SwipeBoxBridge | swipeBoxBridge(req) | 会话生命周期 + 锚点几何 + drag clamp + settle 决策 + 落锚回报 + 主动展开 + 版本握手 |
KStore 用 2 个 C ABI(业务 + 版本),本仓收敛为 1 个——版本握手收进 {"op":"ping"},桥面更小。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"swipeBoxBridge", nullptr, NapiSwipeBoxBridge, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosswipebox_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosswipebox_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
符号表函数名有一个隐蔽规则:KN 生成的符号表访问器叫 lib<baseName>_symbols(),baseName 是 binaries.sharedLib { baseName = "ohosswipebox" }——所以是 libohosswipebox_symbols(不是 ohosswipebox_symbols)。写错这处是链接期或运行期才爆的错,llvm-nm --defined-only libohosswipebox.so | grep symbols 一眼可见(见 8.1 踩坑 4)。
4.4 CMake 和类型声明
add_library(ohosswipebox SHARED IMPORTED)
set_target_properties(ohosswipebox PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(swipebox_napi SHARED napi_init.cpp)
target_link_options(swipebox_napi PRIVATE
"-Wl,--no-undefined"
"-Wl,-rpath,\$ORIGIN")
target_link_libraries(swipebox_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosswipebox)
OHOS_ARCH 为空或 libohosswipebox.so 缺失直接 FATAL_ERROR(提示先跑 :example:nativeApp:linkDebugSharedOhos*);$ORIGIN rpath + IMPORTED_NO_SONAME(防止 Windows 盘符写进 DT_NEEDED)+ POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const swipeBoxBridge: (request: string) => string;
example/nativeApp/build.gradle.kts 侧:ohosArm64 / ohosX64 各开 binaries.sharedLib,linkDebugShared* 任务 finalizedBy 拷贝任务,把 libohosswipebox.so 与 .konan 里对应 triple 的 libc++_shared.so 一起送进 example/harmonyApp/entry/libs/<abi>/。sharedLib 只在 nativeApp 开——库模块开会每个模块各带一份 KN 运行时。
第 5 阶段:ArkUI 宿主
5.1 会话生命周期与手势采样
aboutToAppear(): void {
// vp -> px once, exactly like the upstream composable reads LocalDensity.
this.pxPerVp = this.getUIContext().vp2px(1);
const config: BridgeConfig = {
direction: this.swipeDirection,
startWidthPx: Math.round(this.startWidthVp * this.pxPerVp),
endWidthPx: Math.round(this.endWidthVp * this.pxPerVp),
positionalThresholdPx: 12 * this.pxPerVp, // upstream FixedThreshold(12.dp)
velocityThresholdPx: 125 * this.pxPerVp, // SwipeableDefaults.VelocityThreshold
};
this.applyFrame(this.callBridge({ op: 'create', id: this.rowId, config: config }));
}
Dp→px 在会话边界一次换算(等价于 Compose 在 LocalDensity 内做的事),内核永不接触密度类型。手势侧:PanGesture 的 offsetX 是累计值,行组件记录上一采样点只发增量;PanGesture 不暴露释放速度,速度用最近两次采样的差分估算(delta/dt*1000 px/s),onActionCancel 以速度 0 走 settle 兜底。
5.2 页面交互
布局镜像上游 composable:Stack 底层 Row 两侧行动区按 progress × 满宽 裁剪展开(对应上游 startContentLiveWidth),上层主内容 translate({x: offsetPx/pxPerVp}) 平移。列表演示复刻上游 SwipeBoxListSample 的三条纪律:
- 互斥展开:
activeRowId单一状态源,新行onOpenRow翻转它,其余行@Watch收到自己不是当前行即reveal Settled; - 滚动收回:List
onDidScroll(onScroll已弃用)触发collapseOpenRow,等价上游NestedScrollConnection.onPreScroll拦截; - 删除行:数据源 splice,行组件
aboutToDisappear自动dispose会话。
Tab 切换同样收回展开行;主内容点击在展开态收回(上游 animateTo(0) 的入口语义)。
5.3 settle 插值动画
animateTo 不能逐 tick 回调,ArkTS 用 setInterval 16ms × 19 tick(≈300ms)ease-out 二次插值驱动 offset 从松手点到 targetOffset,每 tick 发 {"op":"frame"} 回报落点:
const eased: number = 1 - (1 - t) * (1 - t); // ease-out quad
const offset: number = fromPx + (targetPx - fromPx) * eased;
this.applyFrame(this.frameOp(offset)); // 引擎回 progress/current
引擎在 frameAt 里做两件事:clamp 到锚点边界;距目标锚点 0.5px 内时把 offset 吸附到锚点并翻转 currentValue = targetValue(对应 SwipeableState.animateTo 完成、currentValue 更新)。决策永远在 Kotlin,ArkTS 只做像素插值——这是"内核没漂"在交互层的直接体现。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosswipebox)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName("SwipeBoxBridge") + MiniJson(cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.swipebox、versionName 1.2.0-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/libohosswipebox_api.h
├── src/main/cpp/types/libswipebox_napi/{index.d.ts, oh-package.json5}
├── src/main/ets/component/SwipeBoxRow.ets 行组件(会话 + 手势 + 插值)
└── src/main/ets/pages/Index.ets ArkUI 三 Tab 验收页面
6.2 构建、签名和安装
./gradlew :swipebox:jvmTest # 22 项交叉验证
./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 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
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.swipebox -m entry
四、完整代码对照
4.1 整体架构
androidx.compose.material.SwipeableState(上游行为来源,字节码反证)
│ computeTarget / findBounds / FixedThreshold / dispatchRawDelta
com.kevinnzou.swipebox/**(提取的纯 Kotlin 内核)
│ SwipeBoxEngine(会话:锚点 / clamp / settle / 进度 / anchorInt)
Bridge.kt + MiniJson
│ SwipeBoxBridge(9 op 会话式 JSON)
libswipebox_napi.so
│ swipeBoxBridge
SwipeBoxRow.ets / Index.ets
│ JSON parse + @State 更新 + setInterval 插值
ArkUI 真机页面(三 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_swipebox、sample 移出 |
gradle/libs.versions.toml | 仅 Kotlin 2.2.21-1.0.0 + multiplatform 插件别名 |
build.gradle.kts(根) | 版本 1.2.0-ohos.1、全局禁用 Ohos*Test* 任务 |
swipebox/build.gradle.kts | 纯 KMP:jvm() + ohosArm64 + ohosX64,零依赖内核 |
swipebox/src/commonMain/(5 文件) | SwipeDirection / SwipeBoxValue / SwipeBoxConfig / SwipeSettle / SwipeBoxEngine |
swipebox/src/composeMain/(4 文件) | 上游 SwipeBox + widget 三件套,git rename 迁入留档不编译 |
swipebox/src/commonTest/(2 文件) | 22 项 JVM 交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、会话表、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/component/SwipeBoxRow.ets | 行组件:会话生命周期 + 手势采样 + 插值动画 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 三 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(19 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | computeSwipeSettleTarget(...) | material computeTarget 纯函数化:速度/位置双阈值 settle 决策 |
| 内核 | findAnchorBounds(offset, anchors) | material findBounds:围绕松手 offset 的邻近锚点对 |
| 内核 | calculateDispatchedSwipeDelta(...) | dispatchRawDelta 的消耗量语义(clamp 在锚点极值间) |
| 内核 | SwipeBoxEngine.onDrag / onDragEnd / frameAt | 拖拽 / 松手决策 / 落锚回报(current-target 翻转时机同 material) |
| 内核 | SwipeBoxEngine.reveal / reset / updateConfig | animateTo / 重建状态 / 几何变更对账 |
| 内核 | SwipeBoxEngine.anchorInt(value) | 上游 SwipeableState<Int> 锚点值映射(含 Both 下 End=2 的特例) |
| Native | SwipeBoxBridge | C ABI JSON 导出(9 op 会话式) |
| N-API | swipeBoxBridge(req) | 向 ArkTS 暴露能力 |
| ArkTS | SwipeBoxRow | 会话生命周期 + 手势采样 + 差分估速 + 插值驱动 |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS:一行滑动决策的全部输入输出
const resp: BridgeFrame = JSON.parse(swipeBoxBridge(JSON.stringify({
op: 'end', id: this.rowId, velocity: this.gestureVelocity // 差分估速 px/s
} as BridgeRequest)));
// resp.ok === true; resp.targetOffset === -210(完全展开) 或 0(收回)
// 随后 setInterval 插值到 resp.targetOffset,每 tick 回报 frame
// Kotlin:同一个决策在内核里的样子
val target = computeSwipeSettleTarget(
offset = engine.offset, // 松手位置
currentAnchor = anchorOf(engine.currentValue),
anchors = anchors, // {0, -210}
velocity = velocityPx,
velocityThresholdPx = config.velocityThresholdPx, // 125dp→px
positionalThresholdPx = config.positionalThresholdPx, // 12dp→px
)
targetValue = valueAt(target)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:提取手势数学内核而不是硬编 ohos Compose runtime
eazytec Maven 上确实存在完整的 ohos CMP 制品线(material / ui / runtime / gradle-plugin 1.9.2-0.2.0-04),但上游是 Compose 1.6.1 + Kotlin 1.9.23 + Material2 实验 swipeable,硬编等于换整套 runtime 且无已审核先例。路线由依赖可达性决定:KStore 的 kotlinx 依赖有同版本 ohos 变体所以真编译整库;SwipeBox 的 material 依赖对不齐所以提取内核——两种答案是同一个方法论。上游可移植的价值在"滑出 12dp 即展开、快扫即展开"这套行为语义,把它提取出来后,Compose UI 与 ArkUI 只是同一数学的两个宿主。
决策 2:settle 决策表用字节码反证而不是凭记忆移植
FixedThreshold 的阈值挂在哪个锚点、computeTarget 的分支以什么为基准,记忆里存在多个互相矛盾的版本("min+px"与"出发锚点+px"两种理解会导致完全相反的手感)。从本地 Gradle 缓存解包 CMP material-desktop 制品、javap 反汇编字节码,把两个函数的分支逻辑钉死成可断言的决策表——移植的正确性不再依赖移植者的记忆。反证结论(阈值挂出发锚点向目标方向 fixedPx)随后全部落入 JVM 测试。
决策 3:工具链 2.2.21 旧线 + Gradle 8.14.1,跟随已审核先例
cmp_qrose / FlexibleBottomSheet / Swipeable-KMP 全部钉 HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21。与 KStore(2.4.20 线 + Gradle 9.6.1)的差异是依赖决定的:本仓内核零依赖,不需要新线的 kotlinx 制品,钉已审核的旧线最稳。两条工具链线的经验不能互相套用——2.2.21 线必须 Gradle 8.x。
决策 4:单函数 JSON 会话桥 + 扁平标量响应
KStore 用 2 个 C ABI,本仓收敛为 1 个(版本握手收进 ping op);9 个 op 会话式分发,每行 SwipeBox 一个 id,生命周期与 ArkTS 行组件严格对齐。响应全部扁平标量(ArkTS 强类型上下文拒绝嵌套 JSON 对象);anchorInt 与上游 SwipeableState<Int> 的 Int 值 1:1,为对账保留。桥不引入 kotlinx-serialization,手写 MiniJson(协议只有 9 个 op,不值得拖一个运行时进 so)。
决策 5:JVM 交叉验证——而且它当场抓出了两处移植预期错
内核逻辑在 commonMain,JVM 与 ohos 跑同一份代码,22 项决策表是"内核没漂"的闸门。第一次跑 jvmTest 22 项里 2 项失败——失败的是测试预期而不是实现:一处把"从展开态拖回越过出发锚点 12px 即收回"的语义预期反了,一处把 clamp 消耗量预期成了终点坐标。这正是交叉验证的价值:把"我以为的 material 语义"和"字节码反证的 material 语义"在测试里对撞,对撞不过的立刻修正。修完后 22/22 绿。
六、测试与验证
6.1 测试环境
本次验证使用 Windows x64、JDK 21(Temurin 21.0.12)、Gradle 8.14.1、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机。
6.2 JVM 交叉验证
./gradlew :swipebox:jvmTest
22 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
SwipeSettleTest | 9 | 慢速 ±12px 边界(含等于即切换)、双向镜像、velocity ≥ ±125 快扫、压锚点单边界、跨锚点 bounds 对 |
FindAnchorBoundsTest | 3 | ≤/> 划分(offset 压锚点与上方配对)、越极端单边界 |
DispatchedSwipeDeltaTest | 1 | 拖拽消耗量语义(返回消耗而非终点) |
SwipeBoxEngineTest | 9 | 三方向锚点几何、anchorInt 映射、进度符号与除锚、拖拽 clamp、current/target 翻转时机、reveal/reset、updateConfig 对账、退化宽度 |
首跑 2 项失败的复盘见决策 5。两层测试与 ohos .so 编的是同一份 commonMain 源码——JVM 绿即证明 so 内 settle 数学没有漂移。
6.3 HAP 验证
:swipebox:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm --defined-only: SwipeBoxBridge / libohosswipebox_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libswipebox_napi.so)
CompileArkTS: finished(修复 4 编译错 2 警告后,见 8.1 踩坑 5)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
libohosswipebox.so debugShared:arm64 约 3.7MB、x86_64 约 3.4MB(含 Kotlin/Native 运行时;settle 内核本体只占极小部分,体积大头是 KN 运行时,与先例库一致)。
6.4 功能用例
- 启动后版本握手行出现
1.2.0-ohos.1(来自pingop 的真实 Native 调用,非静态文字); - 基础演示第一行向左滑出约 12vp(
FixedThreshold(12.dp)换算 px)以上松手——完全展开红色删除区;滑出不足 12vp——收回; - 快扫(差分速度超过 125dp/s 换算 px/s)即使位移很小也直接完全展开(velocity 分支);
- 已展开的行往回拖约 12vp 松手——完全收回(阈值挂出发锚点的镜像语义);
Swipe Right行向右滑露出橙色收藏区(StartToEnd),Both行双向均可展开;- 列表演示同时只有一行展开,新行展开时旧行自动收回;
- 列表滚动时展开行收回(
onDidScroll等价上游 nestedScroll 拦截); - 点"删除"按钮:该行收回、从数据源移除、会话 dispose;关于页显示版本握手与四层架构说明。
6.5 验证结论
settle 内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的行内滑动会话:版本握手、三种方向布局、双阈值 settle 决策、互斥展开与滚动收回闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-swipebox-multiplatform.git
cd compose-swipebox-multiplatform
export JAVA_HOME="/path/to/jdk-21"
./gradlew :swipebox:jvmTest # 22 项交叉验证
./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.swipebox -m entry
hdc shell hilog | grep SwipeBox
八、遗留问题与改进方向
8.1 踩坑复盘
settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Windows 下
./gradlew启动本身要JAVA_HOME——gradle.properties的org.gradle.java.home只管已启动的守护进程,wrapper 的 JVM 得自己找; - 该工具链下 KLIB 任务名是
compileKotlinOhosArm64(不是compileKlib*),拿不准先gradlew tasks; - KN 符号表访问器叫
lib<baseName>_symbols()——baseName 是ohosswipebox,符号就是libohosswipebox_symbols;libohosswipebox_api.h写错这处要等链接/运行期才爆,llvm-nm --defined-only | grep symbols是最快的自证; - ArkTS 编译器比预想严格,首编 4 错 2 警全在 .ets:
Column.alignItems只收HorizontalAlign(子代理把 Row 的VerticalAlign用到了 Column 上);EdgeEffect.Default不存在(枚举只有 None/On/Spring,回弹效果用Spring);List.onScroll已弃用(换onDidScroll双参签名);showToast要 try/catch; - Kotlin 严格空安全在 ohos 编译一样生效:桥函数签名改非空后,
ping/dispose分支还传 null 会直接编译失败——无 frame 的 op 单独走裸响应构造; - DevEco 构建会在 harmonyApp 两层目录生成
oh-package-lock.json5,属于本机状态文件,勿混入提交(gitignore 单列);so / hap /entry/libs/entry/build同理; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 交叉验证的价值在抓自己的错:首跑 2 项失败全是测试预期错(阈值出发锚点语义写反、clamp 消耗量写成终点),不是实现错——测试本身也需要被字节码事实校准;
- CMake 的 rpath 必须写
$ORIGIN且加IMPORTED_NO_SONAME TRUE,否则 Windows 盘符路径会进 DT_NEEDED,设备上 dlopen 失败; - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内保持signingConfigs: []空数组,证书绝不进入 AtomGit。
8.2 已知问题
- 上游 Compose composable 不在鸿蒙侧编译渲染(
composeMain留档),等社区compose.uiohos 制品与上游版本对齐后迁回,NAPI 桥降级为非 Compose 宿主的 fallback; - 上游
animateFloatAsState对底层行动区宽度的 spring 平滑追赶,在拖拽期间由"进度直接跟手"近似(settle 动画期间的平滑由 ArkTS 插值覆盖); - 上游
nestedScroll的onPreScroll拦截在 ArkTS 侧以onDidScroll等价实现(时机为已滚动,非滚动前); - 无 ohos 测试运行器,内核验证全部压在 JVM 交叉验证层;
- android / wasmJs / iOS 目标随 sample 一并留盘不进构建,vanniktech 发布剥离,本 fork 不发 Maven Central;
- 上游
.github/workflows/wiki.yml(Dokka → GitHub Pages)在本 fork 工具链下不适用。
8.3 未来优化方向
- 真机多设备验收矩阵与
uitest自动化截图(hdc shell uitest uiInput swipe批量过 settle 决策表); - 上游若迁移到
AnchoredDraggable(swipeable已废弃),内核SwipeSettle同步换 V2 决策(对照 FlexibleBottomSheet 的SwipeableV2.kt范本); - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建。
九、总结
9.1 核心难点回顾
上游 SwipeBox + widget(100% commonMain,Material2 实验 swipeable)
→ 同类库盘点与路线裁决(ohos CMP 制品 1.9.2 与上游 1.6.1 对不齐 → 内核提取)
→ material settle 数学字节码反证(computeTarget / findBounds / FixedThreshold)
→ 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 / Gradle 8.14.1)
→ ohosArm64 / ohosX64 KLIB + Compose 源码 composeMain 隔离
→ C ABI(@CName × 1,9 op 会话式 JSON)
→ C++ N-API(DisposeString / lib<baseName>_symbols 纪律)
→ ArkTS 手势采样 + 差分估速 + setInterval 插值
→ ArkUI 真机三 Tab 验收(方向布局 / 双阈值 settle / 互斥展开 / 滚动收回 / 删除)
9.2 封装层次
- 引擎层:material settle 决策纯函数 +
SwipeBoxEngine会话状态机(锚点几何、clamp、current/target 翻转时机、进度、anchorInt 映射); - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:行组件会话生命周期、手势增量采样与差分估速、插值动画驱动、互斥展开协调;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 路线由依赖可达性决定,不由库类型决定:同为 CMP 三方库,KStore 的 kotlinx 依赖有同版本 ohos 制品所以真编译整库,SwipeBox 的 material 依赖对不齐所以提取内核——两种答案是同一个方法论的分支;硬编 runtime 的诱惑总在,但"制品基线能否与上游对齐"才是裁决线;
- 语义移植要反证到字节码,不要凭记忆:
FixedThreshold挂出发锚点还是收起锚点,两种记忆会导致完全相反的手感;本地制品javap反汇编一次,决策表就钉死了——适配的正确性不该依赖适配者的记忆力; - 交叉验证抓的第一波错往往是自己的:22 项测试首跑挂 2 项,全是测试预期错而非实现错;测试不是适配完成后的仪式,而是把"我以为的语义"与"反证出的语义"对撞的场子——对撞不过的立刻修正,测试本身也需要被字节码事实校准。
9.4 适配成果
当前适配已完成:
- SwipeBox
1.2.0手势语义的双 ABI Kotlin/Native 编译(material settle 决策逐字节提取,上游 Compose 源文件 git rename 留档composeMain); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证双 ABI 全局 T 符号,libohosswipebox.soarm64 ≈3.7MB / x86_64 ≈3.4MB); - DevEco Stage ArkUI 三 Tab 验收页面(三种方向布局、双阈值 settle、互斥展开、滚动收回、删除行、版本握手);
- 22 项 JVM 交叉验证全绿 + 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。
参考文档
更多推荐




所有评论(0)