本文记录 Waterfall 接入 OpenHarmony 的完整过程,覆盖布局库盘点、纯 Compose 库的引擎抽取边界、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

Waterfall 是基于 Compose Multiplatform 的瀑布流(交错网格)布局库:给定一组条目和面板宽度,它自动测量内容高度、把每个条目贪心分配到当前最矮的列、自动撑起滚动高度。上游一共 188 行 Kotlin、两个文件,100% 是 Compose 代码——Waterfall.kt(基础版,显式 rowCount)与 WaterfallExtra.kt(可定制版,minWidth lambda 自动列数)。本次适配与 KStore 的"真编译整库"路线有本质区别:上游的公共 API 是 @Composable fun Waterfall(...),依赖 compose.runtime / foundation / material3 / ui 全家桶,社区 CMP 1.9.2 与上游 Compose 1.8.2 对不齐,Composable 编不进 ohos。因此走社区已过审的 cmp_qrose 模式(引擎抽取):把值钱的非 UI 部分——最短列贪心分配 defineYPositionRelative、自动列数、内容总高计算——抽成零依赖纯 Kotlin 的 WaterfallEngine 原样编译成 OpenHarmony KLIB,ArkTS 拿到 placements 几何数据绝对定位渲染,屏幕上每个卡片的位置都来自引擎。

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

上游仓库: SingularityIndonesia/Waterfall

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

Waterfall 为 Compose Multiplatform 应用提供"Pinterest 式交错网格"这一件事的完整答案:Waterfall(items, rowCount, gaps, contentPadding) { content() } 一个 Composable 搞定布局,条目高度不用预估——onSizeChanged 实测宽高比回填 ratios,defineYPositionRelative 用 O(n²) 贪心重放把每个条目放进当前最矮列,WaterfallExtra 再把 contentPadding / verticalGap / horizontalGap / minWidth 全部做成 (panelWidth, rowCount) -> X lambda,按面板宽度自适应列数。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套布局能力,但上游构建(Kotlin 2.2.0 / Gradle 8.7 / AGP 8.5.2 / Compose 1.8.2)不会生成 OpenHarmony 变体。

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

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.2.0 / Gradle 8.7 / AGP 8.5.2 / vanniktech 发布插件与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖形态上游 commonMain 依赖 compose.runtime / foundation / material3 / ui / resources / uiToolingPreview;社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 与上游 Compose 1.8.2 / Kotlin 2.2.0 版本矩阵对不齐,Composable 无法整体编译。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
布局语义跨不了渲染管线上游布局发生在 Compose 渲染管线内(onSizeChanged 实测高度、boundsInParent 回写总高),这套回调机制不能跨 FFI,引擎必须以"输入宽高比 → 输出几何"的纯函数形态落地。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:布局引擎的抽取与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。分配算法语义完全由上游原版逻辑负责(逐行对账,不改一条公式),ArkTS 只负责调用 Native、解析 JSON 和按坐标渲染。

1.2 库提供的能力

Demo 提供一个真实的瀑布流布局会话,对应桥接的全部入口:

  • 布局(layout):WaterfallEngine.layout(...) 一次调用返回全部条目的 placements——每个条目的列号、x / y / width / height,以及 rowCount / itemWidth / contentHeight;
  • 自动列数:rowCount = floor(panelWidth / minWidth),结果 ≤0 时兜底取 1(对应上游 WaterfallExtra 的 minWidth lambda);
  • 显式列数:rowCount > 0 直接生效,对应上游基础版;
  • 间距与内边距:verticalGap / horizontalGap / 四边 padding 逐项扣减,垂直间距在每个前驱条目后累加(首条目顶部无间距,与上游一致);
  • 重放确定性:增删条目后既有条目位置不变——O(n²) 重放语义的可见证明;
  • 版本握手:waterfallVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
rowCount / itemWidth实际使用的列数与列宽(自动档回算结果)。
contentHeight内容总高,驱动 ArkTS 滚动区域高度。
placements[]每条目 {id, column, x, y, width, height},顺序与请求一致。

引擎侧完整能力(最短列分配、宽高比兜底、自动列数、几何公式)全部在上游语义上逐行抽取进入 .so,JVM 侧保留 12 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的分配逻辑没有漂移。

1.3 实现适配

维度要求
代码复用引擎语义与上游逐行对账抽取为 WaterfallEngine(唯一新增库源码);上游两个 Compose 文件原样挪入 composeMain(非 Kotlin 源集),git 识别为 100% rename,一个字未改。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 android / ios 目标、AGP、compose 插件与 Maven Central 发布链全部剥离。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律引擎零外部依赖——比 KStore 还薄,不需要任何 kotlinx ohos 制品线;工具链钉社区已过审的 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(qrose 同款)。
UI 完整单页覆盖全部桥接入口:列数 chips(自动 / 1~6)、minWidth / 间距 / 内边距滑条、换一批 / ±10 项按钮、统计行和版本握手。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。瀑布流引擎的输出本身就是几何数据(placements),ArkTS 拿到 x/y/width/height 画卡片即可,桥的两侧都没有"复刻渲染"这层损耗——这是布局库相对图表库(Vico)、条码库(qrose)适配面天然更薄的地方。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点源码(188 行 2 文件)、确认引擎/皮肤边界、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:引擎抽取与桥协议 ── defineYPositionRelative 逐行对账、placements JSON 契约、错误通道、树状 MiniJson
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 列数 chips、四个滑条、三个按钮、统计行和版本握手
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
library/src/commonMain/kotlin/     新增:WaterfallEngine.kt(引擎抽取,包 io.github.stefanusayudha.waterfall)
library/src/commonTest/kotlin/     新增:WaterfallEngineTest.kt(12 项 JVM 交叉验证)
library/src/composeMain/kotlin/    上游 Waterfall.kt + WaterfallExtra.kt(原样存档,不进构建)
example/nativeApp/                 Kotlin/Native 动态库(WaterfallBridge + MiniJson)
example/harmonyApp/                DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md     鸿蒙适配说明(中英双份)
README.upstream.md                 上游原文保存
images/ preview.gif .github/       上游遗留(Maven 发布教学截图、演示图、死 CI),留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_waterfall"。上游根构建的 AGP / compose / composeCompiler / vanniktech 发布插件别名全部随根构建替换剥离——本 fork 不发 Maven Central。上游本来就没有 example 工程和测试,example/ 与 commonTest/ 全部是本 fork 新增。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 wrapper 8.7 升级;2.2.21 旧线要求 Gradle 8.x,qrose 同款钉法)
依赖引擎零外部依赖(无 kotlinx 制品线需求,与 KStore 的关键差异)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.waterfall
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
上游文件行数依赖形态处置
Waterfall.kt144@Composable Waterfall(...) + 私有 defineYPositionRelative(贪心分配)算法抽到 commonMain;Composable 部分挪 composeMain
WaterfallExtra.kt44@Composable 重载 + 自动列数公式同上

expect 声明全库 0 处、平台 API 0 处、第三方依赖 0 个(除 compose 提供束)——上游唯一的"平台耦合"是 LocalDensity 的 px/dp 换算,而这是统一缩放,不改变分配结果(见决策 4)。抽取后的引擎核心,与上游 defineYPositionRelative 逐行对账:

// library/src/commonMain/kotlin/io/github/stefanusayudha/waterfall/WaterfallEngine.kt
fun defineYPositionRelative(
    index: Int, itemCount: Int, ratioAt: (Int) -> Float,
    itemWidth: Float, verticalGap: Float, rowCount: Int
): Pair<Int, Float> {
    val rowOccupation = FloatArray(rowCount)
    for (e in 0 until itemCount) {
        if (e == index) break                       // 只重放前驱
        val keyRatio = ratioAt(e)                   // 上游:ratios[e] ?: 1f
        var minRowIndex = 0
        var minRowValue = rowOccupation[0]
        for (i in 1 until rowCount) {               // 上游:foldIndexed 找 min
            if (minRowValue > rowOccupation[i]) { minRowIndex = i; minRowValue = rowOccupation[i] }
        }
        rowOccupation[minRowIndex] += itemWidth / keyRatio + verticalGap
    }
    // ……条目自身落在最矮列的当前占用高度上
}

foldIndexed(0 to Float.MAX_VALUE) 换成显式循环是等价改写(避免 lambda 装箱),累加顺序、比较方向、兜底行为与上游完全一致——这是"只搬语义不改语义"纪律的落点。

1.4 页面边界
标题区          Waterfall · OpenHarmony / 副标题(CMP 瀑布流布局库 · Kotlin/Native ohosArm64/ohosX64)
版本握手行      waterfallVersion() 实测值(如 version=1.1.0-ohos.1)
统计行          rowCount / itemWidth / contentHeight / items 四项实时统计
列数区          自动 / 1~6 chips(自动档启用 minWidth 滑条,显式档置灰)
滑条区          minWidth 80–400、verticalGap / horizontalGap / padding 0–24
按钮区          换一批(重随机宽高比)/ +10 项 / -10 项(10~120 界)
瀑布流区        Scroll + Stack 绝对定位,卡片位置/尺寸全部来自 placements

页面启动时先调 waterfallVersion() 做握手,onAreaChange 实测面板宽度(vp)后触发首次布局。

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

2.1 依赖边界验证(零依赖是本适配的先天优势)

对 Waterfall 不存在 KStore 那样的"制品线成败手":引擎抽取后不依赖 coroutines / serialization / kotlinx-io 中的任何一个,唯一的外部需求是 Kotlin 编译器本身的 ohos 目标支持。eazytec Maven(https://maven.eazytec-cloud.com)上的 org.jetbrains.kotlin.multiplatform:2.2.21-1.0.0 已被 qrose / haze 等已过审仓库验证带 ohosArm64() / ohosX64() 目标。这也反过来说明工具链选型的分野:依赖形态决定路线——零依赖的纯算法库走 qrose 旧线即可,依赖 kotlinx 全家桶的库才需要 KStore 的 2.4.20 新线。

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

上游源码是 Kotlin 2.2.0 编的,HarmonyOS Kotlin 2.2.21-1.0.0 读 2.2 元数据无障碍——KStore 那种"2.4 元数据读不了"的强制升级在这里不存在,直接沿用社区批准的 qrose 钉法。Gradle 方面 2.2.21 旧线要求 8.x:上游 wrapper 8.7 升到 8.14.1(与 cmp_qrose 逐字节同款 wrapper,gradle-wrapper.jar / gradlew / gradlew.bat 整套替换)。

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()
    }
}
// dependencyResolutionManagement 同样置前

rootProject.name = "ohos_waterfall"

include(":library")
include(":example:nativeApp")

gradle.properties 钉 JDK 21 与版本号:org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot、version=1.1.0-ohos.1、kotlin.native.distribution.downloadFromMaven=true。gradle/libs.versions.toml 收敛到两行:Kotlin 2.2.21-1.0.0 + 一个 multiplatform 插件别名——上游的 agp / composeMultiplatform / vanniktech 别名全部删除。

2.3 编译真实引擎

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

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}          // 引擎零依赖
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

不在库模块上开 binaries.sharedLib(sharedLib 只放 example/nativeApp,否则每个模块各吐一份带运行时的 so)。实测:

.\gradlew.bat :library:compileKotlinJvm :library:jvmTest    # BUILD SUCCESSFUL(12 项全绿)

引擎与上游 Compose 文件的拆分在这一步完成:git mv 两个上游文件到 composeMain,git 提交里显示 100% rename;ohos 编译器看不到 composeMain(它不是注册过的源集),commonMain 里也没有任何 androidx.compose 引用。

2.4 测试依赖的处置:上游没有测试,新增 12 项

上游零测试,交叉验证的用例只能新写——这和 KStore"上游 51 项原测试零改动"是两种形态,但纪律相同:用例只钉上游语义,不钉实现细节。12 项 jvmTest 覆盖:自动列数 floor 语义与 ≤0 兜底、单列纵向堆叠(含 gap 累加)、双列交替归列、aspectRatio 决定高度、非法比率按 1、显式与自动列数一致性、contentHeight 公式、padding / horizontalGap 扣减、最短列优先、100 项压力均衡、退化输入拒绝。commonTest 只依赖 kotlin("test")(各平台制品齐全),ohos 侧照例禁用测试任务,测试统一 JVM 跑:

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

第 3 阶段:引擎抽取与桥协议建模

3.1 JSON 契约
io.github.stefanusayudha.waterfall(引擎抽取,上游语义逐行对账)
        ↓ WaterfallEngine.layout(ratios, rowCount, gaps, paddings)
WaterfallBridge.kt(@CName × 2,树状 JSON 进出)
        ↓ UTF-8 JSON
N-API waterfallLayout() / waterfallVersion()
        ↓
ArkUI Index.ets(Scroll + Stack 绝对定位)

请求与响应(长度单位全部 vp,与上游 Dp 语义对齐):

请求 : {"panelWidth":360,
        "items":[{"id":"item-0","aspectRatio":0.75}, ...],
        "rowCount":0,                // 0/缺省 = 自动(floor(panelWidth/minWidth),≥1)
        "minWidth":200, "verticalGap":8, "horizontalGap":8,
        "paddingStart":0, "paddingEnd":0, "paddingTop":0, "paddingBottom":0}
响应 : {"ok":true,"rowCount":3,"itemWidth":114.67,"contentHeight":1512.3,
        "placements":[{"id":"item-0","column":0,"x":0,"y":0,"width":114.67,"height":152.89}, ...]}
       {"ok":false,"error":"..."}

一个不对称设计:请求里 items[] 只带 id + aspectRatio(宽/高),条目本体留在 ArkTS 侧;响应里 placements[] 按 id 回传几何——上游"泛型条目 + 测量回填"的模型跨桥后变成"标识 + 宽高比进,几何出",content 渲染完全归调用方。

3.2 引擎语义为什么这样落

桥内布局这样开:

val layout = WaterfallEngine.layout(
    panelWidth = panelWidth,
    ratios = ratios,                        // aspectRatio 数组,非法值桥内兜底 1
    rowCount = explicit,                    // null = 自动走 minWidth
    minWidth = root.num("minWidth", 200f),
    verticalGap = root.num("verticalGap", 0f),
    /* ……padding 四边 */
)

宽度数学镜像上游 Composable:availableWidth = panelWidth - paddingStart - paddingEnd - horizontalGap * (rowCount - 1)、itemWidth = availableWidth / rowCount、x = paddingStart + column * (itemWidth + horizontalGap)、y = paddingTop + 列占用、height = itemWidth / ratio、contentHeight = max(y + height) + paddingBottom(对应上游 spacerHeight = itemRect 最大 bottom + bottom padding)。

两个语义决策:其一,上游的"测量"变桥的"输入"——上游靠 onSizeChanged 实测子内容回填 ratios,桥把这一步交给调用方(图片流用原生宽高比,文本卡片先量后传,或预估比率先布局再重调);其二,O(n²) 重放复杂度原样保留——任何"优化"都会改变浮点累加顺序,和上游逐位对不齐;100 项约 1.5 万次列比较,毫秒级。

3.3 错误通道

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

@CName("WaterfallLayout")
fun waterfallLayout(request: String): String {
    return try {
        /* MiniJson 解析 → 参数校验 → WaterfallEngine.layout → JSON 拼装 */
    } catch (t: Throwable) {
        """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
    }
}

非法 JSON、panelWidth 非正、items 非数组/为空、可用宽度耗尽(rowCount 与 padding/gap 把面板吃光),全部走同一个 ok:false 通道,ArkTS 侧统一渲染,HAP 不因坏输入崩溃。

3.4 MiniJson 升级为树状递归下降

qrose / KStore 的 MiniJson 是平坦 kv 解析器(按逗号切顶层字段),Waterfall 的请求里 items 是数组、每个元素又是 {id, aspectRatio} 对象——两层嵌套,平坦解析器结构性不够用。桥内新写树状递归下降解析器(对象 / 数组 / 字符串带转义 / 数字 / 布尔 / null),仍然零依赖、百来行:为一个解析器引 kotlinx-serialization 会多出一个 KN 依赖与版本对齐变量,工具链本来就紧。语义约定:布尔只认小写 true / false(ArkTS JSON.stringify 正好产出)、数字统一 Float、未知字段忽略(向前兼容)。

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

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

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只传几个整数又会让 ArkTS 复刻分配算法,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("WaterfallLayout")
fun waterfallLayout(request: String): String   // 布局主入口,一次返回全部 placements

@CName("WaterfallVersion")
fun waterfallVersion(): String                 // 版本握手
// {"ok":true,"version":"1.1.0-ohos.1","upstream":"1.1.0","engine":"defineYPositionRelative"}
C ABI 符号ArkTS 方法覆盖内核
WaterfallLayoutwaterfallLayout(req)rowCountFor 自动列数 + defineYPositionRelative 贪心分配 + 全套几何公式
WaterfallVersionwaterfallVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"waterfallLayout", nullptr, NapiWaterfallLayout, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"waterfallVersion", nullptr, NapiWaterfallVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

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

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

add_library(waterfall_napi SHARED napi_init.cpp)
target_link_libraries(waterfall_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoswaterfall)

OHOS_ARCH 为空或 libohoswaterfall.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。IMPORTED_NO_SONAME TRUE 防止 Windows 绝对路径被写进 RUNPATH。

export const waterfallLayout: (request: string) => string;
export const waterfallVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 测量面板宽度和触发布局
aboutToAppear(): void {
  this.regenerate(30);          // 初始 30 项,随机宽高比 0.60–1.80
  this.loadVersion();           // waterfallVersion() 版本握手
}

Stack({ alignContent: Alignment.TopStart }) {
  ForEach(this.placements, (p: PlacedItem, index: number) => {
    Column() { /* #序号 / aspectRatio 大字 / atomgit 短链 */ }
      .position({ x: p.x, y: p.y })     // 引擎算出的绝对坐标
      .width(p.width)
      .height(p.height)
      .backgroundColor(this.palette[index % this.palette.length])
  }, (p: PlacedItem) => p.id)
}
.width('100%')
.height(this.contentHeight)             // 引擎算出的内容总高驱动滚动
.onAreaChange((oldArea: Area, newArea: Area) => {
  const w: number = Number(newArea.width);
  if (w > 0 && w !== this.panelWidth) {
    this.panelWidth = w;
    this.relayout();
  }
})

relayout() 组装请求(JSON.stringify 一个普通对象)→ waterfallLayout(body) → JSON.parse → ok 时把 placements / rowCount / itemWidth / contentHeight 写进 @State。任何控件变更(chips / 滑条 / 项数 / 面板宽)都走同一条路径。

5.2 页面交互

控制区自上而下:列数 chips(自动 / 1~6,自动档启用 minWidth 滑条、显式档 .enabled(false) 置灰)、minWidth / verticalGap / horizontalGap / padding 四条 Slider、换一批 / +10 项 / -10 项三按钮(10~120 界,达界置灰)。+10 / -10 保留既有条目只增删尾部——这是重放确定性的刻意设计:追加条目后既有卡片纹丝不动,上游"每个条目只看它前面的条目"的语义肉眼可验。瀑布流卡片从 8 色深色板按序取色,内部三段式(#序号 / 宽高比数值 / atomgit.com/oh-tpc/Waterfall 短链),圆角 8、clip 裁剪。

5.3 对比度和错误边界

统计行用等宽字体逐项回显 rowCount / itemWidth / contentHeight / items,与画面互相印证;ok:false 时错误原文进状态行,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行,不崩溃。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoswaterfall)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── WaterfallBridge.kt                @CName × 2 + 树状 MiniJson
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.waterfall、versionName 1.1.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/libohoswaterfall_api.h
        ├── src/main/cpp/types/libwaterfall_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
.\gradlew.bat :library:jvmTest                                            # 12 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# 用 DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),构建 HAP;
# 或命令行:
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 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.waterfall -m entry

四、完整代码对照

4.1 整体架构

io.github.stefanusayudha.waterfall(引擎抽取,上游语义逐行对账)
    │ rowCountFor / defineYPositionRelative / layout
WaterfallBridge.kt + MiniJson(树状)
    │ WaterfallLayout / WaterfallVersion
libwaterfall_napi.so
    │ waterfallLayout / waterfallVersion
Index.ets
    │ JSON parse + @State 更新 + Stack 绝对定位
    ▼
ArkUI 真机页面(Scroll + Stack 瀑布流)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_waterfall
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 一个插件别名(上游 agp/compose/publish 别名全删)
build.gradle.kts(根)group io.github.stefanusayudha、版本 1.1.0-ohos.1、全局禁用 Ohos*Test* 任务
library/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,零依赖,commonTest 只挂 kotlin-test
library/src/commonMain/.../WaterfallEngine.kt引擎(唯一新增库源码):贪心分配 + 自动列数 + 全套几何公式
library/src/commonTest/.../WaterfallEngineTest.kt12 项 JVM 交叉验证(上游零测试,新写)
library/src/composeMain/kotlin/上游两文件原样存档(git 100% rename,一字未改)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../WaterfallBridge.kt2 个 C ABI、树状 MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(23 节)

4.3 关键 API 对照

层次API作用
引擎WaterfallEngine.rowCountFor(panelWidth, minWidth)自动列数:floor + ≤0 兜底 1(上游 WaterfallExtra 公式)
引擎WaterfallEngine.defineYPositionRelative(...)最短列贪心重放:返回 (column, top)(上游私有函数)
引擎WaterfallEngine.layout(...)一次全量布局:placements + rowCount / itemWidth / contentHeight
引擎WaterfallPlacement / WaterfallLayout条目几何 / 整体结果数据类
NativeWaterfallLayout / WaterfallVersionC ABI JSON 导出
N-APIwaterfallLayout(req) / waterfallVersion()向 ArkTS 暴露能力
ArkTSrelayout()组请求 → 调桥 → 刷 @State
ArkTSonAreaChange 测量面板宽(vp)作为 panelWidth 输入(对应上游 onSizeChanged)

4.4 ArkTS 与 Kotlin 的边界

const raw = waterfallLayout(JSON.stringify({
  panelWidth: 360,
  items: [
    { id: 'item-0', aspectRatio: 0.75 },
    { id: 'item-1', aspectRatio: 1.33 }
  ],
  rowCount: 0, minWidth: 200,
  verticalGap: 8, horizontalGap: 8,
  paddingStart: 0, paddingEnd: 0, paddingTop: 0, paddingBottom: 0
}));
const res = JSON.parse(raw);
// res.ok === true;res.placements[0] = {id:'item-0', column:0, x:0, y:0, width:114.67, height:152.89}
val layout = WaterfallEngine.layout(
    panelWidth = 360f,
    ratios = floatArrayOf(0.75f, 1.33f),
    rowCount = null, minWidth = 200f,
    verticalGap = 8f, horizontalGap = 8f,
    paddingStart = 0f, paddingEnd = 0f, paddingTop = 0f, paddingBottom = 0f
)
// layout.rowCount == 1(floor(360/200)=1);layout.placements 按 id 对应回 ArkTS

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

五、关键决策说明

决策 1:引擎抽取而不是真编译整库(与 KStore 相反的取舍)

KStore 整库真编译的前提是"上游 commonMain 是纯 Kotlin + kotlinx 依赖有 ohos 制品线"。Waterfall 上游 100% 是 Compose 代码,公共 API 就是 @Composable,上游 Compose 1.8.2 / Kotlin 2.2.0 与社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 版本矩阵对不齐——强行对齐等于再打一套 Compose 发行版。而值钱的非 UI 部分(分配算法)不含任何 Compose 类型,抽出来就是零依赖纯 Kotlin。依赖形态决定路线:引擎可分离且零依赖 → 抽取(qrose / Waterfall);引擎依赖有 ohos 制品线 → 整库(KStore)。

决策 2:工具链钉 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(qrose 同款)

上游 Kotlin 2.2.0 与 2.2.21 同代,元数据无障碍;零依赖引擎没有 KStore 那种"stdlib 2.4 元数据反推钉版"的强制因素,直接沿用社区已过审的钉法最稳。Gradle 8.14.1 是 2.2.21 线的要求(上游 8.7 wrapper 整套替换),两条工具链线的经验不互相套用。

决策 3:根构建替换(cmp_qrose 模式),上游目标与发布链剥离

上游根构建(AGP 8.5.2 / compose / composeCompiler / vanniktech Maven Central 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建;android / ios 目标与发布、dokka 式外围全部剥离——本 fork 不发 Maven Central,交付物是 klib + so + HAP。上游 images/(Maven 发布教学截图)、preview.gif、.github/workflows(jvm/ios 死 CI)留盘不进构建语义。

决策 4:引擎输出 placements,ArkTS 绝对定位渲染;测量变输入

桥不传渲染语义只传几何:x/y/width/height 是纯数,卡片长什么样归 ArkTS。上游"onSizeChanged 实测高度回填 ratios"的机制不能跨 FFI,桥把测量交给调用方——图片流用原生宽高比,文本卡片先量后传或预估再重调(全量重放语义下重调代价可忽略)。引擎全程 Float、单位 vp:上游是 px 测量后 / density 回 dp,统一缩放不改变分配结果(缩放不变性),这是"引擎没漂"的数学根据。

决策 5:jvmTest 交叉验证 + 上游文件零改动

引擎逻辑在 commonMain,JVM 与 ohos 编同一份代码;12 项 jvmTest 是"引擎没漂"的闸门。上游两个 Compose 文件 git mv 到 composeMain、提交里 100% rename、一字未改——这既是"只加不改"纪律(便于日后向 https://github.com/SingularityIndonesia/Waterfall 提 ohos target PR 时 diff 里只有新增),也是接回路线的保障:等社区 compose.ui ohos 制品与上游 CMP 版本对齐,把 composeMain 挪回 commonMain、恢复 compose 依赖声明即可,ArkTS 渲染届时降级为 fallback。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

.\gradlew.bat :library:compileKotlinJvm :library:jvmTest

12 项用例全部通过(上游零测试,用例新写、只钉上游语义):

用例钉住什么
自动列数 floor 语义(360/200→1、1000/200→5、719/200→3)WaterfallExtra 截断公式
自动列数 ≤0 兜底取 1takeIf { it > 0 } ?: 1
单列纵向堆叠 y 逐条累加(含 verticalGap)列占用累加顺序
双列交替归列(0,1,0,1)最短列贪心
aspectRatio 决定高度(0.5 → 2 倍宽)maxWidth / keyRatio
非法 / 缺失比率按 1(正方形)ratios[e] ?: 1f 兜底
显式列数与自动列数一致性两条路径汇合同一几何
contentHeight = max(y+height) + paddingBottom上游 spacerHeight 语义
padding / horizontalGap 逐项扣减(164 = (360-24-8)/2)availableWidth 公式
最短列优先(矮列先接下一项)贪心核心
100 项压力:三列均有条目、列高均衡重放复杂度下的稳定性
退化输入拒绝(rowCount ≤0、宽度耗尽抛 IAE)错误通道前置

6.3 so / HAP 验证

:library:compileKotlinJvm: BUILD SUCCESSFUL
:library:jvmTest: BUILD SUCCESSFUL(12 项)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: WaterfallLayout / WaterfallVersion / libohoswaterfall_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libwaterfall_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

libohoswaterfall.so(debugShared、未 strip)arm64-v8a 约 3.47 MB、x86_64 约 3.20 MB,含 Kotlin/Native 运行时;每个 ABI 目录三件套(libwaterfall_napi.so / libohoswaterfall.so / libc++_shared.so)由 link 任务 finalizedBy 的拷贝任务自动回填。链接期 CAdapter 胶水有两条已知 clang 警告(api.cpp non-void 控制流),系 HarmonyOS Kotlin 生成物,不影响业务。

6.4 功能用例(真机实测)

  1. 启动后版本握手行显示 version=1.1.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 首屏 30 项自动布局:onAreaChange 实测面板宽后渲染,卡片按 placements 绝对定位、无重叠无漏项;
  3. 自动模式(minWidth=200)下列数随面板宽自适应:真机竖屏 rowCount=1、itemWidth=309.71vp(截图实测值);
  4. 连点 +10 项到 50 项:统计行 items=50、contentHeight=14413.53vp(截图实测值),既有卡片位置纹丝不动——重放确定性成立;
  5. 切换 2 / 3 / 4 列:rowCount / itemWidth 统计同步变化,卡片重排即时;
  6. 滑 verticalGap / horizontalGap / padding:间距与内边距逐项扣减,contentHeight 随之单调变化;首条目顶部无间距(与上游累加顺序一致);
  7. 换一批:宽高比重随机,列高均衡(最短列贪心的可见结果);
  8. 非法输入(如空 items)走 ok:false 状态行,不崩溃。

6.5 验证结论

布局引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的瀑布流布局会话:版本握手、自动/显式列数、间距内边距扣减、增删条目的重放确定性与内容总高驱动滚动(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/Waterfall.git
cd Waterfall
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :library:compileKotlinJvm :library:jvmTest                            # 12 项交叉验证
.\gradlew.bat :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.waterfall -m entry
hdc shell hilog | findstr Waterfall

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让布局引擎真正编译成 ohosArm64 / ohosX64 动态库——屏幕上每个坐标都要能追到 WaterfallEngine;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 2.2.21 工具链线要求 Gradle 8.x:上游 wrapper 8.7 必须整套升到 8.14.1(jar + gradlew 脚本一起换,别只改 distributionUrl);
  4. 本机全局 Gradle init 脚本会吞掉 eazytec 仓库:~/.gradle/init.d/ 下的镜像加速脚本(重排 pluginManagement 仓库、前置 aliyun)在 remove + re-add 往返中会把 URL 型 maven 仓库丢掉,eazytec 两个仓库正好是 URL 型——现象是插件解析列表里根本没有 eazytec。仓库文件保持过审标准形状不动,构建时临时禁用该 init 脚本即可;
  5. ArkTS @State 成员不能与基类通用属性重名:状态变量命名 padding 会撞 CustomComponent 的 padding() 方法签名,编译报 10505001 Property 'padding' ... Type 'number' is not assignable to type '(value: Length | Padding ...) => CommonAttribute'——改名 panelPadding 解决;width / height / margin 等通用属性名同理要避开;
  6. Kotlin 默认包(无 package 声明)的类型无法从命名包引用:上游两文件在默认包,桥代码在 example.nativeapp 包里引用不到——新引擎文件必须挂正规包(io.github.stefanusayudha.waterfall,上游 group 坐标);
  7. MiniJson 必须树状:请求里 items 是数组嵌套对象,qrose / KStore 的平坦 kv 解析器结构性不够,递归下降重写仍然零依赖;
  8. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可(真机是 x86_64);
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. ohos 测试任务在根构建全局禁用(工具链对 ohos 测试执行支持不全),交叉验证统一走 jvmTest;
  11. build-profile.json5 的 signingConfigs 留空数组且 product 引用 "default":勾选 DevEco 自动签名后由 IDE 回填带 material 的完整配置,证书绝不进入仓库;
  12. HAP 打包用 DevEco 自带 JBR,系统 JAVA_HOME 指向 JDK 8 会让 hvigorw assembleHap 半路报不相干的错。

8.2 已知问题

  • 鸿蒙侧无 Composable:上游两个文件在 composeMain 存档,等社区 compose.ui ohos 制品与上游 CMP 版本对齐后接回,届时 ArkTS 绝对定位渲染降级为 fallback;
  • O(n²) 重放复杂度原样保留(上游同款):n ≤ 数百无压力,上万会退化,列表场景应分页 / 增量布局;
  • 引擎 Float / vp 与上游 px / density 换算在统一缩放下等价,但逐位浮点结果不承诺与某台设备的 px 序列完全一致;
  • 上游 RTL / LTR padding 混用(Start 取 LayoutDirection.Rtl、End 取 Ltr,疑为笔误)未复刻,引擎用朴素 start / end 语义——有记录的偏差,上游修正后引擎无需改动;
  • 上游 scrollState 外部传入语义不跨桥(Demo 内部管理滚动),需要时可加字段不破坏现有调用;
  • android / ios 目标与 Maven Central 发布链剥离(上游文件留在树里),本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.waterfall 匹配的签名。

8.3 未来优化方向

  • 跟踪社区 compose.ui ohos 制品与 CMP 版本对齐进度,条件满足即把 composeMain 接回 commonMain;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查(llvm-nm 断言 WaterfallLayout 为 T 符号)和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
  • 与上游 JVM 端 Compose 实测做一轮像素级对账(同输入同参数,比对每个条目的 x/y/宽高),作为"语义一致"的最终凭据。

九、总结

9.1 核心难点回顾

上游 Waterfall.kt + WaterfallExtra.kt(188 行,100% Compose)
    → 盘点引擎/皮肤边界(defineYPositionRelative 是纯算法)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(qrose 同款)
    → 根构建替换(cmp_qrose 模式,AGP/compose/发布链剥离)
    → 引擎抽取 WaterfallEngine(逐行对账)+ composeMain 存档(git 100% rename)
    → ohosArm64 / ohosX64 KLIB + 12 项 jvmTest 交叉验证
    → C ABI(@CName × 2)+ 树状 MiniJson + placements JSON 契约
    → C++ N-API(DisposeString 纪律)
    → ArkTS Scroll + Stack 绝对定位
    → ArkUI 真机验收(自动/显式列数 / 间距扣减 / 增删重放确定性 / 版本握手)

9.2 封装层次

  • 引擎层:WaterfallEngine——最短列贪心分配、自动列数、宽高比兜底、全套几何公式(上游语义逐行对账);
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、测量面板宽度、组请求、按 placements 绝对定位渲染;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. UI 库适配的正确切面是引擎不是皮肤:布局库值钱的是分配算法而非 Composable 外壳。引擎抽出后零依赖、可测、跨端可对账,桥的两侧没有"复刻渲染"的损耗——Waterfall 是 CPF-KMP-CMP 这批适配里桥最薄的一个;
  2. 工具链选择跟着依赖形态走:零依赖纯算法库用 qrose 旧线(Kotlin 2.2.21 + Gradle 8.14.1)即可;依赖 kotlinx 全家桶的库才需要 KStore 的 2.4.20 新线 + 制品线验证。路线选择必须基于库本身的依赖形态,而不是把某一个仓的经验当成通用答案;
  3. 交叉验证 + 重放确定性是布局库最硬的验收:12 项 jvmTest 钉住语义,"+10 项后既有卡片纹丝不动"把上游 O(n²) 重放语义变成肉眼可验的 UI 行为——布局库没有视觉近似可言,坐标差一个 gap 就看得见,这正是它比图表/条码库更适合逐条对账的原因。

9.4 适配成果

当前适配已完成:

  • Waterfall 1.1.0 布局引擎的双 ABI Kotlin/Native 编译(引擎与上游逐行对账,上游 Compose 文件零改动、git 100% rename 存档于 composeMain);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证 WaterfallLayout / WaterfallVersion / libohoswaterfall_symbols 全局 T 符号);
  • 树状 MiniJson 解析器(适配 items 数组契约,零依赖);
  • DevEco Stage ArkUI 验收页面(列数 chips、四滑条、换一批 / ±10 项、统计行、版本握手,全部由引擎实时驱动);
  • 12 项 JVM 交叉验证全绿 + 双 ABI so(arm64 约 3.47 MB / x86_64 约 3.20 MB)+ HAP 构建、签名、真机安装与运行;
  • AtomGit 项目文档、OpenHarmony README(中英双份、中文主文档 23 节)和本地效果图。

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

更多推荐