本文记录 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 / targetStatesettle 类 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin 21.0.12,org.gradle.java.home 钉死)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 8.2.1 升级;2.2.21 旧线必须 8.x)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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 源码依赖度盘点
模块文件依赖形态处置
swipebox4(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 方法覆盖内核
SwipeBoxBridgeswipeBoxBridge(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.kt1 个 C ABI、会话表、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/component/SwipeBoxRow.ets行组件:会话生命周期 + 手势采样 + 插值动画
harmonyApp/.../ets/pages/Index.etsArkUI 三 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 / updateConfiganimateTo / 重建状态 / 几何变更对账
内核SwipeBoxEngine.anchorInt(value)上游 SwipeableState<Int> 锚点值映射(含 Both 下 End=2 的特例)
NativeSwipeBoxBridgeC ABI JSON 导出(9 op 会话式)
N-APIswipeBoxBridge(req)向 ArkTS 暴露能力
ArkTSSwipeBoxRow会话生命周期 + 手势采样 + 差分估速 + 插值驱动

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 项用例全部通过:

测试类用例数钉住什么
SwipeSettleTest9慢速 ±12px 边界(含等于即切换)、双向镜像、velocity ≥ ±125 快扫、压锚点单边界、跨锚点 bounds 对
FindAnchorBoundsTest3≤/> 划分(offset 压锚点与上方配对)、越极端单边界
DispatchedSwipeDeltaTest1拖拽消耗量语义(返回消耗而非终点)
SwipeBoxEngineTest9三方向锚点几何、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. 启动后版本握手行出现 1.2.0-ohos.1(来自 ping op 的真实 Native 调用,非静态文字);
  2. 基础演示第一行向左滑出约 12vp(FixedThreshold(12.dp) 换算 px)以上松手——完全展开红色删除区;滑出不足 12vp——收回;
  3. 快扫(差分速度超过 125dp/s 换算 px/s)即使位移很小也直接完全展开(velocity 分支);
  4. 已展开的行往回拖约 12vp 松手——完全收回(阈值挂出发锚点的镜像语义);
  5. Swipe Right 行向右滑露出橙色收藏区(StartToEnd),Both 行双向均可展开;
  6. 列表演示同时只有一行展开,新行展开时旧行自动收回;
  7. 列表滚动时展开行收回(onDidScroll 等价上游 nestedScroll 拦截);
  8. 点"删除"按钮:该行收回、从数据源移除、会话 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 踩坑复盘

  1. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  2. Windows 下 ./gradlew 启动本身要 JAVA_HOME——gradle.properties 的 org.gradle.java.home 只管已启动的守护进程,wrapper 的 JVM 得自己找;
  3. 该工具链下 KLIB 任务名是 compileKotlinOhosArm64(不是 compileKlib*),拿不准先 gradlew tasks;
  4. KN 符号表访问器叫 lib<baseName>_symbols()——baseName 是 ohosswipebox,符号就是 libohosswipebox_symbols;libohosswipebox_api.h 写错这处要等链接/运行期才爆,llvm-nm --defined-only | grep symbols 是最快的自证;
  5. ArkTS 编译器比预想严格,首编 4 错 2 警全在 .ets:Column.alignItems 只收 HorizontalAlign(子代理把 Row 的 VerticalAlign 用到了 Column 上);EdgeEffect.Default 不存在(枚举只有 None/On/Spring,回弹效果用 Spring);List.onScroll 已弃用(换 onDidScroll 双参签名);showToast 要 try/catch;
  6. Kotlin 严格空安全在 ohos 编译一样生效:桥函数签名改非空后,ping/dispose 分支还传 null 会直接编译失败——无 frame 的 op 单独走裸响应构造;
  7. DevEco 构建会在 harmonyApp 两层目录生成 oh-package-lock.json5,属于本机状态文件,勿混入提交(gitignore 单列);so / hap / entry/libs / entry/build 同理;
  8. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. 交叉验证的价值在抓自己的错:首跑 2 项失败全是测试预期错(阈值出发锚点语义写反、clamp 消耗量写成终点),不是实现错——测试本身也需要被字节码事实校准;
  11. CMake 的 rpath 必须写 $ORIGIN 且加 IMPORTED_NO_SONAME TRUE,否则 Windows 盘符路径会进 DT_NEEDED,设备上 dlopen 失败;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内保持 signingConfigs: [] 空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • 上游 Compose composable 不在鸿蒙侧编译渲染(composeMain 留档),等社区 compose.ui ohos 制品与上游版本对齐后迁回,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 三条经验

  1. 路线由依赖可达性决定,不由库类型决定:同为 CMP 三方库,KStore 的 kotlinx 依赖有同版本 ohos 制品所以真编译整库,SwipeBox 的 material 依赖对不齐所以提取内核——两种答案是同一个方法论的分支;硬编 runtime 的诱惑总在,但"制品基线能否与上游对齐"才是裁决线;
  2. 语义移植要反证到字节码,不要凭记忆:FixedThreshold 挂出发锚点还是收起锚点,两种记忆会导致完全相反的手感;本地制品 javap 反汇编一次,决策表就钉死了——适配的正确性不该依赖适配者的记忆力;
  3. 交叉验证抓的第一波错往往是自己的: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.so arm64 ≈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。

参考文档

Logo

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

更多推荐