本文记录 scaffolds 接入 OpenHarmony 的完整过程,覆盖折叠 Scaffold 组件库盘点、纯 Kotlin 折叠引擎提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 三 Tab 真机页面、HAP 构建和真机验收。

scaffolds 是发布在 Maven Central(com.dontsaybojio:scaffolds:0.0.1)的 Compose Multiplatform 折叠脚手架库:CollapsingLayout 把可折叠头叠在身体内容上接好 nested scroll,SimpleScaffold 给它套上 Material3 Scaffold,CollapsibleScaffold 再叠上背景色插值、视差、淡出、90% 阈值收起标题、圆角内容卡与 Tab 条拖拽。本次适配与 KStore 的"真编译整库"路线有本质区别:scaffolds 的对外 API 100% 是 Compose UI(三个 @Composable,一个非 UI 字节都没有),而鸿蒙线上没有 Compose 制品——因此走 cmp_qrose / swipe 的既定路线:把三个组件脚下的折叠状态机(内部 NestedScroll 连接、exit-until-collapsed 语义、视差/淡出/颜色插值、Tab 条拖拽映射)逐行抽成纯 Kotlin 的 CollapseEngine,编译成 OpenHarmony KLIB 与双 ABI 动态库;Compose UI 原样隔离进 composeMain 留给未来 CMP 工具链;ArkUI Demo 重建同样的三段式界面,每一帧都由引擎返回值驱动,ArkTS 侧没有第二份折叠数学。

项目地址: AtomGit/oh-tpc/scaffolds-kmp.git

上游仓库: delacrixmorgan/scaffolds-kmp

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

scaffolds 为 Compose Multiplatform 应用提供"滚动折叠头"这一件事的完整答案:CollapsingLayout 用一个内部 NestedScrollConnection 实现上滑折叠、到底停住、下滑展开、扩到顶停住;SimpleScaffold 把它接进 Material3 Scaffold 的 padding 体系;CollapsibleScaffold 直接驱动 TopAppBarDefaults.exitUntilCollapsedScrollBehavior(),叠出收起式大标题的完整手感。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、WASM 共享。OpenHarmony 应用同样需要这套交互,但上游构建(Kotlin 2.3.20 / Compose Multiplatform 1.10.3 / AGP 9.1.1 / Gradle 9.4.1)不会生成 OpenHarmony 变体,社区 Maven 上也没有 compose.material3 的 ohos 制品。

如果只把示例页面改成 ArkTS,页面可以滚动,却无法证明折叠状态机在 OpenHarmony ARM64 设备上完成编译、链接和逐帧执行。适配需要同时验证折叠引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.3.20 / Gradle 9.4.1 与鸿蒙 Kotlin 插件(2.2.21-1.0.0 旧线)无法共存于一个 Gradle 构建,旧线不支持 Gradle 9。
依赖矩阵上游库依赖 compose.material3 / material-icons-extended,ohos 制品线没有 Compose;引擎必须零第三方依赖。
库形态特殊对外 API 全部是 @Composable,没有可独立编译的内核——必须先从 Compose 类型里把状态机剥出来。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
手势语义跨不了桥NestedScrollConnection 的 pre/post 次序、listAtTop 门是 Compose 协议概念,ArkUI 的 onScrollFrameBegin 需要逐帧换算对齐。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:折叠状态机的纯 Kotlin 提取(逐行对应上游源码)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。上游三个 Compose 组件源码一行不改地保留在 composeMain;ArkTS 只负责调 Native、解析 JSON 和用引擎返回值刷 UI。

1.2 库提供的能力

Demo 提供三个 Tab,分别对应三个上游组件的真实折叠会话:

  • CollapsingLayout(内部连接模式):List.onScrollFrameBegin 逐帧把增量喂给 collapsing 动作,折叠头位移完全由引擎返回的 offset 驱动——上滑折叠、到底停住、下滑展开、扩到顶停住;
  • SimpleScaffold(exit-until-collapsed 模式):固定顶栏 + 折叠头,折叠在 pre-scroll 消费、展开只在列表到达顶部的 post-scroll 消费、heightOffset 永不出界;
  • CollapsibleScaffold(全功能模式):每帧调 present 取整页外观——背景色按通道插值(#6750A4 → #E8DEF8)、视差位移、透明度淡出、collapsedFraction ≥ 0.9 时收起标题出现、圆角内容卡、Tab 条拖拽也能驱动折叠;
  • 版本握手:启动时 info 动作返回 fork 版本与 Kotlin 版本,状态行显示 scaffolds 0.0.1-ohos.1 · kotlin 2.2.21-1.0.0,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
consumed / offset / heightOffset本次消费量与折叠位移(Compose 符号约定,负 = 折叠),ArkTS 据此决定列表剩余滚动量。
collapsedFraction / translationY / alpha / background / titleVisiblepresent 动作返回的一帧外观:折叠比例、视差位移、淡出系数、插值背景色、收起标题可见性。

引擎侧完整能力(clamp 区间、pre/post 方向门、0.9f 标题阈值、0.3f 视差、0.6f/2f 拖拽系数、ARGB 通道插值)全部在 CollapseEngine 里逐行对应上游源码,JVM 侧保留 24 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的折叠逻辑没有漂移。

1.3 实现适配

维度要求
代码复用上游三个 Compose 组件原样 git mv 进 composeMain,一行不改;新增 CollapseEngine(纯 Kotlin、零第三方依赖)与 24 项 JVM 测试。
平台目标lib 模块只声明 jvm() + ohosArm64 + ohosX64,JVM 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sample / androidApp / iosApp 留盘不进 settings.gradle.kts。
桥接稳定使用 1 个 C ABI(6 个 action)和 UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
引擎纪律引擎无状态:调用方(ArkTS 或 JVM 测试)持有 offset,每个手势事件喂回来;上游魔数(0.9f / 0.3f / 0.6f / 2f)原样透出,不偷偷"优化"。
UI 完整三 Tab 覆盖全部桥接入口:内部连接、exit-until-collapsed、全功能外观、Tab 条拖拽、版本握手与状态行。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。scaffolds 的渲染面留在 composeMain 不参与 ohos 编译;ArkTS Demo 是把桥暴露的折叠状态机画出来,视觉参数全部来自引擎。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认"纯 Compose UI 库"的适配路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降到 8.14.1
第 3 阶段:引擎提取与桥协议 ── CollapseEngine 逐行移植、6 个 action 的扁平 JSON 契约、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 三 Tab、onScrollFrameBegin 逐帧驱动、状态行与版本握手
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
lib/                                  上游 KMP 库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
lib/src/commonMain/                   新增:CollapseEngine(折叠状态机纯 Kotlin 移植,全 fork 的核心新代码)
lib/src/composeMain/                  上游三个 Compose 组件原样迁入(ohos 不编,留给未来 CMP 工具链)
lib/src/jvmTest/                      新增:CollapseEngineTest,24 项交叉验证
sample/ androidApp/ iosApp/           上游 Demo 工程,留盘不进 settings
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 三 Tab 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :lib 与 :example:nativeApp,rootProject.name = "ohos_scaffolds"。上游的 AGP / Compose Multiplatform / compose compiler / hot-reload / vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / swipe 同款旧线)
JDK21(org.gradle.java.home 指向 Eclipse Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(从上游 9.4.1 降级:2.2.21 旧线不支持 Gradle 9)
依赖引擎零第三方依赖(commonMain.dependencies {} 刻意留空)
DevEco product5.1.0(18) 兼容(实测 DevEco 26 命令行可打 HAP)
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.scaffolds
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块源码形态依赖处置
lib/src/commonMain(上游)3 个 @Composable + 空壳 expectCompose material3 / foundation整体迁入 composeMain(ohos 不编)
lib/src/commonMain(新增)CollapseEngine 单文件零依赖、纯 kotlin.math新增编译(jvm + 双 ohos)
sample / androidApp / iosAppCompose Demo + navigationCompose 全家桶留盘不进构建

上游的 expect/actual 全部是空包声明壳(无真实 expect 声明),不需要补任何平台 actual——这一点和 KStore 的 8 行 dispatcher actual 不同,scaffolds 的全部新增 Kotlin 源码只有引擎与测试。

与 KStore 的关键差异在盘点阶段就已注定:KStore 的 commonMain 是纯 Kotlin 引擎,制品线一齐就能真编译整库;scaffolds 的 commonMain 是 Compose UI,社区 Maven 没有 Compose ohos 制品,抽状态机引擎是唯一能"真编译、真执行"的路线——再等 CMP 工具链等于不做适配。

1.4 页面边界
状态行          版本握手行(scaffolds 0.0.1-ohos.1 · kotlin 2.2.21-1.0.0,aboutToAppear 调 info)
                + 操作行(最近一次引擎调用,如 "exit pre ok · consumed=-12.00 heightOffset=-92.00")
Tab 1           CollapsingLayout:渐变折叠头(220vp)+ 40 行列表,内部连接模式
Tab 2           SimpleScaffold:固定顶栏(56vp)+ 折叠头(160vp)+ 40 行列表,exit-until-collapsed 模式
Tab 3           CollapsibleScaffold:动态背景顶栏 + 视差折叠头(260vp)+ 圆角内容卡
                (Tab 条可拖拽驱动折叠 + Overview/Stats/About 三个子列表),全功能模式

页面启动时先调 info 做版本握手,状态行随每次引擎调用实时刷新 consumed / heightOffset / fraction,真机上可直接读出折叠进度。

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

2.1 路线判定(本适配的成败手)

KStore 类库先验证 kotlinx 制品线,scaffolds 这一步要验证的是反面:社区 Maven 上有没有 Compose 的 ohos 制品。结论是没有——org.jetbrains.compose.material3:material3、org.jetbrains.compose.ui:ui 均无 ohosArm64 变体。这决定了两条纪律:

  1. lib 模块的 commonMain.dependencies {} 刻意留空,引擎一个第三方依赖都不许有;
  2. 上游 Compose 源码整体迁 composeMain(不是删除)——它仍是 API 的对照物,也是未来 CMP 工具链到齐后的接回对象。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

按 cmp_qrose 审核模式钉 HarmonyOS Kotlin 2.2.21-1.0.0。这条旧线有一个硬约束:不支持 Gradle 9,而上游 wrapper 是 9.4.1,必须降到 8.14.1——两条工具链线的经验不能互相套用(KStore 那篇的"2.4.20 线沿用 Gradle 9"在 2.2.21 线不成立)。

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_scaffolds"

include(":lib")
include(":example:nativeApp")
2.3 编译折叠引擎

lib/build.gradle.kts 收敛为纯 KMP 模块:

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

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

    sourceSets {
        // ohos 线没有 Compose 制品,引擎必须零依赖(cmp_qrose 模式)
        commonMain.dependencies {}
        jvmTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

CollapseEngine 按上游源码逐行移植,每处都写了出处注释:

// CollapsingLayout 内部 NestedScrollConnection:calculateOffset 的 clamp 逐位一致
fun collapsePreScroll(offset: Float, delta: Float, headerHeight: Float): ScrollStep
fun collapsePostScroll(offset: Float, delta: Float, headerHeight: Float): ScrollStep
// exitUntilCollapsed:折叠吃 pre-scroll、展开只吃列表到顶的 post-scroll
fun exitPreScroll(heightOffset: Float, delta: Float, heightOffsetLimit: Float): ExitStep
fun exitPostScroll(heightOffset: Float, delta: Float, heightOffsetLimit: Float, listAtTop: Boolean): ExitStep
// CollapsibleScaffold 每帧外观:0.9f 阈值、0.3f 视差、1-f 淡出、ARGB 通道插值
fun present(...): Presentation
fun lerpArgb(startArgb: Long, stopArgb: Long, fraction: Float): Long
// Tab 条拖拽:dragY * 0.6f 进滚动、dragY * 2f 出甩动速度(上游魔数原样透出)
fun tabDragScrollDelta(dragY: Float): Float
fun tabFlingVelocity(dragY: Float): Float

实测:

./gradlew :lib:compileKotlinJvm                 # BUILD SUCCESSFUL
./gradlew :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64   # BUILD SUCCESSFUL

上游三个 Compose 组件文件一个没动(git mv 保止单 100% 相似度),commonMain 新增的只有引擎与测试。

2.4 测试策略:期望值手工推导自上游源码

引擎改写不得引入漂移,交叉验证的期望值必须来自上游。24 项 jvmTest 的断言全部按 composeMain 源码手工推导,例如:

测试钉住什么
internalPreScrollStopsAtHeaderHeightoffset=-200, delta=-50, headerHeight=220 → consumed=-20, offset=-220,代进上游 (oldOffset + delta).coerceIn(-headerHeight, 0f) 得同一组值
exitPostScrollExpandsOnlyWhenListAtToplistAtTop=false 时消费 0,true 时全量消费——post-scroll 的到顶门
collapsedTitleAppearsAtNinetyPercent0.89f 不出现、0.9f 出现——上游 derivedStateOf { collapsedFraction < 0.9f } 的字面量
lerpArgbInterpolatesEachChannel#6750A4 → #E8DEF8 中点四通道逐个断言——与 androidx.compose.ui.graphics.lerp 同步
tabDragMultipliersMatchUpstream0.6f / 2f 两个魔数单独成测,防止"顺手"改掉

第 3 阶段:桥协议建模

3.1 JSON 契约
com.dontsaybojio.scaffolds.CollapseEngine(纯 Kotlin,原样编译)
        ↓ 折叠状态机 / 视差 / 插值
Bridge.kt(@CName × 1,扁平 JSON 进出,6 个 action)
        ↓ UTF-8 JSON
N-API scaffoldStep()
        ↓
ArkUI Index.ets(三 Tab,逐帧驱动)

单一入口按 action 分发:

请求 : {"action":"info"}
       {"action":"layout","headerHeight":220}
       {"action":"collapsing","phase":"pre","offset":-30,"delta":-50,"headerHeight":220}
       {"action":"exit","phase":"post","heightOffset":-100,"delta":50,"heightOffsetLimit":-220,"listAtTop":true}
       {"action":"present","heightOffset":-110,"heightOffsetLimit":-220,"contentHeight":220,
        "parallaxFactor":0.3,"expandedColor":"#6750A4","collapsedColor":"#E8DEF8"}
       {"action":"tabDrag","dragY":-20}
响应 : {"ok":true,"consumed":-50,"offset":-80}                          (collapsing)
       {"ok":true,"consumed":50,"heightOffset":-50,"collapsedFraction":0.23}(exit)
       {"ok":true,"collapsedFraction":0.5,"translationY":-33,"alpha":0.5,
        "background":"#FFA7A0CE","titleVisible":false}                  (present)
       {"ok":false,"error":"..."}

两个刻意设计:数值全部两位定点输出(consumed=-12.0 而非 -1.2E1),避免科学计数法过桥;颜色收 #RRGGBB / #AARRGGBB 字符串、回 #AARRGGBB,比裸 long 不易错。phase 由 ArkTS 按增量符号路由(负 = pre = 折叠方向),与 NestedScrollConnection 的派发次序一一对应。

3.2 桥粒度为什么按组件语义切

qrose 按"编码器 + 样式模型"切桥,swipe 按"一步手势"切桥,scaffolds 的对齐物是三个组件各自的状态机语义:collapsing 对应 CollapsingLayout 的内部连接、exit 对应 TopAppBarScrollBehavior、present 对应 CollapsibleScaffold 的每帧外观、tabDrag 对应 Tab 条手势映射。ArkTS 不需要知道 clamp 区间或阈值细节,只需要拿到"下一步状态"和"这一帧长什么样"——桥上没有第二个折叠实现,手感漂移就无从谈起。

引擎是无状态的:offset 由 ArkTS 持有、每个手势事件喂回来。这与上游"每次滚动事件读旧值算新值"语义等价,也让 JVM 测试与 ArkTS Demo 走同一套调用面。

3.3 错误通道

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

@CName("ScaffoldStep")
fun scaffoldStep(request: String): String = try {
    /* action 分发 */
} catch (t: Throwable) {
    err(t.message ?: t::class.simpleName ?: "error")
}

未知 action、非法数值、缺字段,全部走同一个 ok:false 通道,ArkTS 状态行统一渲染 失败: ...,不崩 HAP。

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

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

HarmonyOS Kotlin 的 CAdapter 不会把 @CName 函数挂到 ArkTS 模块上——libohosscaffolds.so 里能 nm 到 ScaffoldStep,但 ArkTS import 它只能看到 stdlib 类型,所以必须用 CMake 再包一层 NAPI so(qrose / swipe / Flippable 同款结论)。

4.2 Kotlin/Native 导出函数
@CName("ScaffoldStep")
fun scaffoldStep(request: String): String   // 6 个 action 一口进
C ABI 符号ArkTS 方法覆盖内核
ScaffoldStepscaffoldStep(req)CollapseEngine 全部 6 组折叠数学 + 版本握手
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"scaffoldStep", nullptr, NapiScaffoldStep, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

回调统一三步:napi_typeof 校验必须是 string、先量长度再开 std::vector<char> 缓冲读 UTF-8(API 26 头文件把缓冲区标成非 const,std::string::data() 直接传编不过)、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosscaffolds_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosscaffolds_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与一个业务符号),不拖整份生成头。

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

add_library(scaffolds_napi SHARED napi_init.cpp)
target_link_libraries(scaffolds_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosscaffolds)

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

export const scaffoldStep: (request: string) => string;

copyKnSo* Gradle 任务把 linkDebugSharedOhosArm64 / OhosX64 产物和 konan 目录里对应 triple 的 libc++_shared.so 一起回填到 entry/libs/<abi>/(已 gitignore),不要手工从别的工程借 so。

第 5 阶段:ArkUI 宿主

5.1 逐帧驱动:onScrollFrameBegin 对齐 NestedScroll

ArkUI 没有 NestedScroll 协议栈,用 onScrollFrameBegin 复现:回调给出本帧列表想滚的 offset(正 = 内容上滚),换算成 Compose 符号 delta = -offset 调引擎,把返回的 consumed 从剩余量里扣掉:

.onScrollFrameBegin((offset: number, _state: ScrollState) => {
  const delta = -offset;                    // Compose 约定:负=折叠
  if (delta < 0) {
    const consumed = this.stepCollapse(this.t1Offset, delta);   // pre:折叠优先
    if (consumed === null) { return { offsetRemain: offset }; }
    return { offsetRemain: offset + consumed };
  }
  if (!this.t1AtTop) { return { offsetRemain: offset }; }       // 列表没到顶,头不抢
  const consumed = this.stepCollapse(this.t1AtTop ? this.t1Offset : this.t1Offset, delta);
  ...
  return { offsetRemain: consumed >= delta ? 0 : offset + consumed };
})

注意本 SDK 的回调签名是 (offset: number, state: ScrollState)——第一个参数就是数值本身,不是带 scrollOffset 字段的事件对象(踩坑见 8.1)。listAtTop 由 onScrollIndex((first) => first === 0) 维护,对应 post-scroll"内层列表已消费完"的语义。

5.2 页面交互

Tab 1 自上而下:渐变折叠头(槽位高度 = 220 + offset,内容 translate 上移 + clip,等价上游 layout modifier 的"槽位收缩 + 内容照画")、40 行列表。Tab 2 把折叠头换成青绿色块并叠 opacity(1 - fraction),顶栏固定不动——SimpleScaffold 与 CollapsibleScaffold 的结构差异一目了然。Tab 3 整页背景色来自 present 返回的插值色,展开头叠视差 translate 与淡出,收起标题按 titleVisible 出现,圆角内容卡压在头底部 16vp 上,Tab 条挂 PanGesture:onActionUpdate 把增量喂给 tabDrag + exit,onActionEnd 把 flingVelocity 显示到状态行(exit-until-collapsed 无 snap,停在当前位置,与上游一致)。顶部状态行每次引擎调用后刷新 action ok · consumed=… offset/fraction=…。

5.3 单位纪律和错误边界

ArkTS 的 vp 数值直接当引擎数值用,不做 px 换算——换算会把 headerHeight 和 clamp 区间一起放大,折叠会在半路卡住。所有调桥包 try/catch,ok:false 的 error 原文进状态行;Native 调用本身抛错(理论不该发生)同样被收进状态行,不崩 HAP。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosscaffolds)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 1 + MiniJson(扁平 JSON,cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.scaffolds、versionName 0.0.1-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohosscaffolds_api.h
        ├── src/main/cpp/types/libscaffolds_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 三 Tab 验收页面
6.2 构建、签名和安装
./gradlew :lib:jvmTest                                            # 24 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

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

命令行打 HAP 时 hvigor / node / ohpm / java 都取自 DevEco 安装目录(PackageHap 要起 JVM,缺 java 会报 spawn java ENOENT,踩坑见 8.1):

set PATH=E:\DE\DevEco Studio\jbr\bin;E:\DE\DevEco Studio\tools\node;E:\DE\DevEco Studio\tools\ohpm\bin;%PATH%
set DEVECO_SDK_HOME=E:\DE\DevEco Studio\sdk

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

四、完整代码对照

4.1 整体架构

lib/src/composeMain/**(上游三个 Compose 组件,原样保留不编译)
    │ SimpleScaffold / CollapsibleScaffold / CollapsingLayout
lib/src/commonMain/.../CollapseEngine.kt(新增,纯 Kotlin 逐行移植)
    │ collapsePreScroll / collapsePostScroll / exitPreScroll / exitPostScroll
    │ collapsedFraction / present / lerpArgb / tabDrag*
Bridge.kt + MiniJson
    │ ScaffoldStep(info / layout / collapsing / exit / present / tabDrag)
libscaffolds_napi.so
    │ scaffoldStep
Index.ets
    │ JSON parse + @State 更新(offset 由 ArkTS 持有、逐帧回喂)
    ▼
ArkUI 真机三 Tab 页面(折叠头 / 顶栏 / 视差 + 插值 + 收起标题)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_scaffolds、上游三 Demo 工程移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单插件钉版
gradle/wrapper/gradle-wrapper.propertiesGradle 降到 8.14.1(旧线不支持 Gradle 9)
build.gradle.kts(根)版本 0.0.1-ohos.1、全局禁用 Ohos*Test* 任务
lib/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,commonMain.dependencies {} 零依赖
lib/src/commonMain/.../CollapseEngine.kt折叠状态机移植(全 fork 的核心新代码,每处注明上游出处)
lib/src/jvmTest/.../CollapseEngineTest.kt24 项交叉验证(期望值手工推导自上游源码)
lib/src/composeMain/**上游三个组件原样迁入(git mv,留给未来 CMP 工具链)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、6 个 action、MiniJson、两位定点格式化、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 三 Tab 验收页面(逐帧驱动)
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
引擎collapsePreScroll / collapsePostScrollCollapsingLayout 内部连接一步(clamp [-headerHeight, 0])
引擎exitPreScroll / exitPostScrollexit-until-collapsed 一步(post 只在 listAtTop 时消费)
引擎collapsedFractionheightOffset / heightOffsetLimit,与 TopAppBarScrollBehavior 一字不差
引擎present一帧外观:fraction / translationY / alpha / 插值背景 / 0.9f 标题阈值
引擎tabDragScrollDelta / tabFlingVelocityTab 条 0.6f / 2f 映射
NativeScaffoldStepC ABI JSON 导出(6 action 一口进)
N-APIscaffoldStep(req)向 ArkTS 暴露能力
ArkTSonScrollFrameBegin → stepCollapse / stepExit / applyPresent逐帧驱动并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(scaffoldStep(JSON.stringify({
  action: 'exit', phase: 'pre',
  heightOffset: this.t2HeightOffset, delta: -12,
  heightOffsetLimit: -160, listAtTop: false
})));
// res = {ok:true, consumed:-12, heightOffset:-92, collapsedFraction:0.575}
// 列表 offsetRemain = offset + consumed;折叠头槽位高度 = 160 + heightOffset
"exit" -> when (json.str("phase")) {
    "post" -> CollapseEngine.exitPostScroll(heightOffset, delta, limit, json.bool("listAtTop"))
    else   -> CollapseEngine.exitPreScroll(heightOffset, delta, limit)
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;ArkTS 侧没有第二份 clamp / 阈值 / 插值实现。

五、关键决策说明

决策 1:抽状态机引擎而不是等 Compose 制品

KStore 抽内核是因为当年 ohos 上没有 kotlinx 制品,后来制品齐了就改成真编译;scaffolds 的处境是它的镜像——对外 API 100% 是 Compose UI,而 Compose 的 ohos 制品至今不存在。折叠手感全在 clamp、阈值、系数这些细节里,在 ArkTS 里重写一份等于第二个实现,上游改一个系数就是一次失联。把状态机逐行抽成 CollapseEngine(每处注明上游出处、24 项测试钉住期望值),是当前工具链下唯一能"真编译、真执行、可验收"的路线。composeMain 原样保留,社区 CMP 的 compose.ui ohos 制品出现后接回 commonMain,引擎降级成 fallback。

决策 2:工具链按 qrose 旧线钉版,Gradle 主动降级

按 cmp_qrose 审核模式钉 HarmonyOS Kotlin 2.2.21-1.0.0。这条线不支持 Gradle 9,上游 wrapper 9.4.1 必须降到 8.14.1——不能套用 KStore 那篇"2.4.20 线沿用 Gradle 9"的经验,两条工具链线各管各的。eazytec 双仓在 pluginManagement 与 dependencyResolutionManagement 里都排最前,否则配置期直接 Unresolved reference: ohosArm64。

决策 3:根构建替换(cmp_qrose 模式),上游 Demo 工程留盘

上游根构建(AGP / Compose Multiplatform / compose compiler / hot-reload / vanniktech 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample / androidApp / iosApp 依赖 Compose 与 navigation,鸿蒙线上没有制品,留盘不进 settings.gradle.kts——源码保留是为了上游对照,不要拿它们在鸿蒙上 sync。

决策 4:引擎无状态 + 单 C ABI 六 action

offset 由调用方持有、每个手势事件喂回来,与上游"每次滚动读旧值算新值"语义等价,也让 JVM 测试与 ArkTS Demo 走同一套调用面。桥不按像素切而按组件语义切(collapsing / exit / present / tabDrag),ArkTS 拿到的是"下一步状态 + 这一帧外观",桥上没有第二个折叠实现。数值两位定点、颜色十六进制串,过桥零歧义。

决策 5:jvmTest 交叉验证 + 真机手感两道闸

折叠逻辑在 commonMain,JVM 与 ohos 跑同一份代码;24 项 jvmTest 的期望值全部手工推导自 composeMain 的上游源码(含 lerpArgb 四通道中点、0.9f 阈值边界、0.6f/2f 魔数),是"引擎没漂"的闸门。交叉验证之外,真机手感是第二道闸:折叠曲线应与 Android 上的上游一致——若某次"优化"让鸿蒙的折叠和上游不一样,那是回归,不是特性。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Eclipse Temurin,org.gradle.java.home 钉版)、DevEco Studio 26 命令行工具(hvigor 6.26.8 / ohpm / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :lib:jvmTest

24 项用例全部通过(期望值手工推导自上游源码):

测试组用例数钉住什么
limit / fraction6heightOffsetLimit = -headerHeight、fraction 的 0/1/中点、零 limit 守卫、clamp 边界
内部连接7pre 只吃负 delta、到底停住、post 只吃正 delta、扩到顶停住、clamp 边界
exit-until-collapsed6pre 折叠不过 limit、post 只在 listAtTop 消费、扩到顶停住
外观5视差/淡出公式、0.9f 阈值边界、lerpArgb 四通道中点、present 全量快照
Tab 条拖拽20.6f / 2f 两个魔数双向断言

上游 Compose 源码一行未动(git mv 相似度 100%),引擎期望值与上游逐位对齐——这正是"引擎没漂"的直接证据。

6.3 HAP 验证

:lib:compileKotlinJvm / OhosArm64 / OhosX64: BUILD SUCCESSFUL
:lib:jvmTest: 24/24 通过
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ScaffoldStep 为全局 T 符号
BuildNativeWithCmake / BuildNativeWithNinja: finished(双 ABI 编出 libscaffolds_napi.so)
CompileArkTS: finished
PackageHap / SignHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 6.7 MB,内含双 ABI 各三件套(libscaffolds_napi.so / libohosscaffolds.so / libc++_shared.so);HAP 内 strip 后 KN so arm64 约 1.9MB、x86_64 约 1.9MB(debugShared 未 strip 时分别约 3.6MB / 3.3MB,含 Kotlin/Native 运行时)。

6.4 功能用例

  1. 启动后状态行出现 scaffolds 0.0.1-ohos.1 · kotlin 2.2.21-1.0.0(info 动作来自真实 Native 调用,非静态文字);
  2. Tab 1 手指上滑,折叠头跟手收起,状态行滚动刷新 collapsing pre ok · consumed=… offset=…;折叠到 0 后列表继续滚,手指下滑列表先滚回顶部、再继续滑折叠头展开(post 方向门);
  3. Tab 2 折叠全程顶栏固定不动,展开只在列表到顶后发生;折叠头透明度随 fraction 淡出;
  4. Tab 3 整页背景色从 #6750A4 平滑插值到 #E8DEF8,展开头视差上移 + 淡出,折叠到 90% 时收起标题 “Aerith Gainsborough” 出现;
  5. Tab 3 圆角内容卡压在头底部,Tab 条上拖拽可直接驱动折叠,松手显示 tabDrag end · flingVelocity=…(无 snap,停在当前位置,与上游 exit-until-collapsed 一致);
  6. 三个子页(Overview / Stats / About)切换后列表独立滚动,折叠状态跨子页保持;
  7. 每步操作的操作行按引擎返回值实时刷新,异常路径(如未知 action)显示 失败: … 不崩 HAP。

6.5 验证结论

折叠引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的折叠会话:版本握手、内部连接折叠、exit-until-collapsed、视差/插值/收起标题与 Tab 条拖拽闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/scaffolds-kmp.git
cd scaffolds-kmp
export JAVA_HOME="/path/to/jdk-21"
./gradlew :lib:jvmTest                                                          # 24 项交叉验证
./gradlew :lib:compileKotlinOhosArm64 :lib:compileKotlinOhosX64                 # klib
./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.scaffolds -m entry
hdc shell hilog | grep Scaffolds

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让折叠引擎真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 2.2.21-1.0.0 旧线不支持 Gradle 9:上游 wrapper 9.4.1 直接 settings 脚本编译失败,wrapper 必须降到 8.14.1——首次构建若复用了旧版本 daemon 还会报一批 RepositoryHandler 假错,--no-daemon 或重启 daemon 即消;
  4. ArkUI onScrollFrameBegin 的回调签名是 (offset: number, state: ScrollState),第一个参数就是数值本身——按"事件对象带 scrollOffset 字段"的写法会吃 16 个 ArkTS 编译错(Property 'scrollOffset' does not exist on type 'number'),写之前先查本机 SDK 的 scroll.d.ts;
  5. 命令行 hvigorw assembleHap 的 PackageHap 要起 JVM:PATH 里没有 java 会报 spawn java ENOENT(错误码 00308018),把 DevEco 自带的 jbr/bin 加进 PATH 即解;系统 JDK 8 会让 hvigor 半路换一种不相干的报错;
  6. HarmonyOS CAdapter 不会把 @CName 业务函数挂到 ArkTS 模块,必须 CMake 包一层 NAPI so;nm_modname、ArkTS import 名、index.d.ts 三个名字有一个对不上,表现都是"能安装、一滚动就崩";
  7. ArkTS 的 vp 数值直接当引擎数值用,不要做 px 换算——换算会把 headerHeight 与 clamp 区间一起放大,折叠在半路卡住;
  8. exit-until-collapsed 的展开必须带 listAtTop 门(onScrollIndex 维护),否则列表没滚到顶折叠头就抢滚动,手感立刻变糊;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. build-profile.json5 的 signingConfigs 保持空数组且 product 不引用签名,勾选自动签名后由 IDE 回填带 material 的完整配置,提交前用 git update-index --skip-worktree 保护,证书绝不进入 AtomGit;
  12. 根构建的 **/build/ 与 .idea/* 盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.hvigor/、.idea/、oh_modules/、entry/build/、entry/libs/、.cxx/ 要单独追加 gitignore,否则混进提交。

8.2 已知问题

  • 鸿蒙上没有 Compose 渲染:三个 @Composable 在 composeMain 不参与编译,当前 Demo 是 ArkUI 重建 + 引擎驱动;上游 Material3 的水波纹、状态栏 inset 等平台细节不在复刻范围;
  • onScrollFrameBegin ≠ NestedScroll:按帧给增量、没有"子列表先吃、父布局后吃"的协议栈,listAtTop 由 onScrollIndex 维护,极限快速甩动下可能有一帧偏差,视觉上不可辨;
  • 引擎无 snap:exit-until-collapsed 上游就没有回弹吸附,折叠停在手指离开的位置,Demo 如实表现,不要当成缺陷;
  • lerpArgb 按通道算完四舍五入到 0–255 再输出十六进制,与 Compose 浮点 Color 有 ±1/255 的差异,JVM 测试以引擎自己的值为准;
  • android / ios / wasmJs / desktop 目标剥离(源文件留在树里),Compose / AGP / maven-publish 插件一并剥离,本 fork 不发 Maven Central;
  • 上游 sample / androidApp / iosApp 留盘不进构建,不要拿它们在鸿蒙上 sync;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.scaffolds 匹配的签名。

8.3 未来优化方向

  • 社区 CMP 的 compose.ui ohos 制品出现后,把 composeMain 接回 commonMain,三个组件在 ComposeArkUIViewController 里直接跑,引擎降级成 fallback;
  • 用 uitest + hdc 脚本化三个 Tab 的自动化截图(展开态 / 折叠中 / 折叠完成),补全效果图矩阵;
  • 把 tabDrag 的甩动速度接到 ArkUI 惯性滚动上,进一步对齐上游 onPostFling 手感;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵。

九、总结

9.1 核心难点回顾

上游三个 Compose 组件(100% UI,无内核可编译)
    → 判定路线:Compose ohos 制品不存在 → 抽折叠状态机(cmp_qrose / swipe 模式)
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 主动降 8.14.1)
    → 根构建替换,composeMain 隔离上游 UI
    → CollapseEngine 逐行移植 + 24 项 JVM 交叉验证
    → C ABI(@CName × 1,6 action)+ 扁平 JSON
    → C++ N-API(DisposeString 纪律)
    → ArkTS onScrollFrameBegin 逐帧驱动
    → ArkUI 真机三 Tab 验收(内部连接 / exit-until-collapsed / 全功能外观)

9.2 封装层次

  • 引擎层:CollapseEngine 零依赖纯函数,clamp / 阈值 / 魔数与上游逐位对齐;
  • 桥接层:单 C ABI 六 action、MiniJson、两位定点格式化、异常不跨 FFI;
  • Native 层:双 ABI 动态库 + CMake NAPI 包装,$ORIGIN 三件套同目录;
  • ArkTS 层:offset 持有、逐帧回喂、listAtTop 门、状态行与错误文本;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 库形态决定适配路线:纯 Kotlin 内核的库(KStore)制品齐了就真编译整库,100% Compose UI 的库(scaffolds)只能抽状态机引擎 + ArkUI 重建 UI——路线选择基于"对外 API 里有多少非 UI 字节",而不是统一答案;
  2. 工具链经验按插件线隔离:2.2.21 旧线必须配 Gradle 8.14.1,2.4.20 线可用 Gradle 9.x,两篇适配文章的 Gradle 结论互相不可套用;ArkUI 回调签名以本机 SDK 的 .d.ts 为准,不要凭印象写;
  3. 交叉验证 + 手感双闸:引擎类适配的验收底线是"同一输入产出同一位移"——24 项测试钉住数学,真机手感钉住曲线;只扫出"能折叠"不算验收,能折叠只证明状态机没糊,不证明和上游逐位一致。

9.4 适配成果

当前适配已完成:

  • scaffolds 0.0.1(0.0.1-ohos.1)折叠引擎的双 ABI Kotlin/Native 编译(上游 Compose 源码零改动隔离进 composeMain,新增 CollapseEngine 单文件引擎);
  • 折叠状态机全量落地:内部 NestedScroll 连接、exit-until-collapsed、视差/淡出/ARGB 插值、0.9f 收起标题阈值、Tab 条 0.6f/2f 拖拽映射;
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三 Tab 验收页面(内部连接折叠、exit-until-collapsed、全功能外观、Tab 条拖拽、版本握手、状态行);
  • 24 项 JVM 交叉验证全绿(期望值手工推导自上游源码)+ HAP 构建(双 ABI 六件套 so,约 6.7MB)、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐