本文记录 Table-KMP 接入 OpenHarmony 的完整过程,覆盖上游 Compose 表格库盘点、交互内核提取、鸿蒙工具链对齐、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 表格 Demo、HAP 构建和设备运行验收。

Table-KMP 是基于 Compose Multiplatform 的数据表格库:Table 组合式接收 items 与 TableColumn 列定义(weight 弹性宽度 + minWidth 兜底 + @Composable 单元格工厂),TableConfig 提供 8 个行为开关与尺寸,TableColors 定义 16 色调色板,长按拖拽换位带中心点碰撞与边缘自动滚动,选择引擎支持表头全选与行复选,另有斑马纹、点击高亮、自定义滚动条与加载/空态。本次适配与存储类库(KStore)的"真编译整库"路线有本质区别:上游 table 模块 11 个源文件全部 import androidx.compose.*,没有任何纯 Kotlin 文件可以整库编译——因此适配的答案是"交互内核提取":把选择引擎、拖拽换位物理、列宽求解、外观状态机与主题/配置默认值从 Compose 源码中逐行沉淀为纯 Kotlin 内核(零第三方依赖),编译成 OpenHarmony Kotlin/Native 动态库;ArkTS Demo 复刻上游交互,所有决策仍由上游算法驱动,ArkTS 只做渲染。

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

上游仓库: mamon-aburawi/Table-KMP

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

Table-KMP 为 Compose Multiplatform 应用提供"数据表格"这一件事的完整答案:Table(items, columns, config, colors) 一行建表,行点击、多选、拖拽排序全交互覆盖,TableColumn.content 让任意模型渲染成任意组合单元格,DragDropState 用长按手势 + 中心点碰撞 + 16ms 自动滚动协程实现物理感换位,TableColors 16 个颜色参数把表头/行/复选框/滚动条全部纳入主题。上游把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套表格能力,但上游构建(Kotlin 2.3.20 / Compose 1.10.3 / material3 1.10.0-alpha05)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示表格,却无法证明公共交互内核在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证选择引擎、换位物理、列宽求解、外观解析、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.3.20 / Compose 1.10.3 / AGP 8.11.2 与鸿蒙工具链(HarmonyOS Kotlin 2.2.21-1.0.0)无法共存于一个 Gradle 构建。
全量 Compose 依赖上游 11 个源文件 100% import androidx.compose.*:7 个 @Composable(Table/TableHeader/TableBody/TableRow/DragDropState/两个滚动条工具),4 个纯类型文件也用 Compose 类型(Color/Dp/Shape/Alignment/@Composable lambda)——没有一行现成的纯 Kotlin 代码可以编译。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交互语义跨不了桥Compose 快照状态(mutableStateListOf)、LazyListState.layoutInfo、16ms 协程自动滚动循环都是 Kotlin 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:交互内核的 ohos 编译(选择/换位/列宽/外观/主题五件套)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。渲染语义完全由 ArkTS 承担,决策语义完全由上游算法承担。

1.2 库提供的能力

Demo 提供一个真实的交互表格会话,对应桥的全部入口:

  • 版本握手:version 返回 fork 版本号,来自真实 Native 调用而非静态文字;
  • 数据集:dataset 返回 16 行企业项目数据(上游 sampleProjects 原样搬运)与 6 列定义,金额列由上游 formatCurrency 函数原样格式化——ArkTS 侧不重录任何数据;
  • 列宽求解:layout 按视口宽度计算每列宽度,窄屏自动切换横向滚动(上游 totalMinWidth > maxWidth 判定);
  • 主题:theme 返回上游浅色默认 16 色 + 暗色 DarkAnalytics 预设(ARGB 十六进制),附带拖拽动画常量与配置默认值;
  • 选择引擎:select(行选/全选/清除)返回选中序列与全选态,语义与上游 mutableStateListOf 逐一对照;
  • 点击高亮:click 按 highlightClickedRow 开关决定是否更新 clickedId(上游 if (config.highlightClickedRow) 语义);
  • 拖拽换位:dragStart / swapCheck / dragEnd 完成长按拖拽的槽位交换,中心点碰撞公式与上游 DragDropState.checkSwap 一致;
  • 自动滚动:autoScroll 返回本 tick 滚动速度(15% 边缘阈值、60 上限、线性爬升);
  • 外观解析:appearance 按拖拽/选中/斑马纹/悬停分支返回容器色、内容色与缩放/阴影目标(上游 TableRow 状态机);
  • 会话重置:reset 恢复初始顺序、选择与点击状态。
响应字段作用
ok操作是否成功,失败时 error 带原文。
order / count拖拽换位后的行序列与行数(Kotlin 会话是唯一权威)。
selected / allSelected选择引擎的完整状态,驱动表头复选框与行复选框。
widths / requiresHorizontalScroll列宽求解结果与横向滚动判定。
container / content / role外观状态机的解析结果(#AARRGGBB),ArkTS 直接渲染。
speed自动滚动速度(px/tick),正负号即方向。

内核侧完整能力(选择顺序语义、换位 clamp、中心碰撞、自动滚动阈值、斑马纹分支、16 色默认值、DarkAnalytics 预设)全部在上游源码对照下进入 .so,JVM 侧保留 27 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的交互逻辑没有漂移。

1.3 实现适配

维度要求
代码复用上游 11 个 Compose 文件 git mv 零改动隔离到 src/composeMain;选择/换位/列宽/外观/主题的算法逐行对照提取为纯 Kotlin 内核,默认值字节级一致。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);composeApp / iosApp / kotlin-js-store 留盘不进构建。
桥接稳定单个 @CName C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律交互内核零第三方依赖(纯 Kotlin 标准库)——不需要任何 kotlinx 制品线,工具链钉版即可编译。
UI 完整三 Tab 单页覆盖全部桥入口:交互表格、行为开关/主题/加载空态、版本握手与架构说明。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙社区 CMP 线与上游 Compose 1.10.3 不在同一条工具链上,@Composable 无法在 ohos 上编译;ArkTS Demo 把桥暴露的交互闭环画出来,决策全部回流 Kotlin 内核。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与 11 文件 Compose 依赖度、确定交互内核边界、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、composeMain 隔离
第 3 阶段:交互内核提取   ── 选择引擎/换位物理/列宽求解/外观状态机/主题七件套,上游公式逐行对照
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 三 Tab 表格 Demo、拖拽坐标模型、16ms 自动滚动循环
第 6 阶段:示例与验证     ── HAP、签名、设备安装、运行截图和效果验收

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
table/                               上游表格库(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
table/src/commonMain/.../core/       新增:纯 Kotlin 交互内核(7 个文件,全仓唯一新增库源码)
table/src/composeMain/               上游 11 个 Compose 文件原样隔离(git mv 零改动,不参与编译)
table/src/jvmTest/                   新增:27 项内核交叉验证测试
example/nativeApp/                   Kotlin/Native 双 ABI 动态库(Bridge + MiniJson + 会话状态)
example/harmonyApp/                  DevEco Stage、CMake N-API 和 ArkTS 表格 Demo
README.OpenHarmony.md / _CN.md       鸿蒙适配说明(中英双份)
README.upstream.md                   上游原文保存
composeApp/ iosApp/ kotlin-js-store/ 上游示例工程,留盘不进 settings

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :table 与 :example:nativeApp,rootProject.name = "ohos_tablekmp"。上游的 AGP 插件别名、compose 插件、vanniktech 发布配置、发布密钥全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 钉定)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 8.14.3 必须替换——2.2.21 线与 8.14.1 匹配)
kotlinx 制品线无——交互内核零第三方依赖,纯 Kotlin 标准库
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.tablekmp
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
上游文件Compose 依赖形态处置
Table.kt@Composable × 8;选择状态机 + 列宽判定 + 加载/空态隔离 composeMain;算法提取为 SelectionEngine + TableLayoutSolver
TableHeader.kt / TableBody.kt / TableRow.kt@Composable;material3 复选框/Card隔离 composeMain;TableRow 外观分支提取为 RowAppearance
DragDropState.kt快照状态 + LazyListState + 协程自动滚动隔离 composeMain;换位/自动滚动公式提取为 RowReorderEngine
HorizontalDraggableTool.kt / VerticalDraggableTool.kt@Composable 自定义滚动条隔离 composeMain;ArkTS 侧由系统滚动条 + 横向提示条承担
TableColors.ktColor 类型 × 16隔离 composeMain;默认值字节级镜像为 TableColorScheme(ARGB Long)
TableConfig.ktDp / Shape 类型隔离 composeMain;开关与尺寸镜像为 TableSettings(Float)
TableColumn.kt@Composable lambda / Dp / Alignment隔离 composeMain;声明式镜像为 ColumnSpec + TableAlignment
TableColumnModifier.ktModifier / RowScope.weight隔离 composeMain;宽度分支并入 TableLayoutSolver
androidMain/AndroidManifest.xmlAGP 清单删除(纯 KMP 构建无 android 目标)

expect 声明全库为零——这让内核提取不需要补任何 actual,适配面集中在"算法搬运 + 类型替换"。

1.4 页面边界
标题区          TABLEKMP · OPENHARMONY / 版本徽标(1.0.1-ohos.1,来自真实 Native 调用)
Tab 切换        表格 / 设置 / 关于
表格区          表头(列宽来自 layout op)+ 16 行(状态点/优先级胶囊/进度条/金额)
                复选列(selectionEnabled)· 窄屏横向滚动 · 自定义滚动提示条
状态行          最近一次操作结果(点击/选择/换位/主题切换回显)
设置区          主题(浅色/深色)· 7 个行为开关 · 加载/空态/重置 · 布局信息
关于区          版本握手、库简介、架构分层、功能覆盖、仓库与社区链接

页面启动时先调 version 握手,再依次取 theme(含配置默认值)与 dataset。

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

2.1 根构建替换(cmp_qrose 模式)

settings.gradle.kts 把 eazytec 双仓写在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64:

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

include(":table")
include(":example:nativeApp")

版本目录收敛为一行:kotlin = "2.2.21-1.0.0"。根构建声明组与 1.0.1-ohos.1 版本,并全局禁用 ohos 测试任务(测试统一在 JVM 跑):

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

gradle.properties 钉 JDK 21 与 kotlin.native.distribution.downloadFromMaven=true;上游误提交在 gradle.properties 里的 Maven Central 账号密码与 GPG 私钥口令随替换移除(强烈建议上游作者尽快吊销轮换——它们已进入 git 历史)。

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
        jvmTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}
2.2 composeMain 隔离

上游 11 个文件用 git mv 迁到 table/src/composeMain/kotlin/com/mamon/tablekmp/,内容零改动(git 以 100% 相似度记录 rename,历史可追溯)。composeMain 不是 Kotlin 源集,永不编译;待社区 CMP 线与上游 Compose 1.10.3 / Kotlin 2.3.20 对齐后,把目录改回 commonMain 即可整体恢复。

2.3 交互内核提取(本适配的成败手)

七件套全部落在 com.mamon.tablekmp.core 包下,每个文件的 KDoc 都标注上游出处。以换位物理为例,上游 DragDropState.checkSwap:

val targetTop = initialItemTop + dragY
val clampedTop = targetTop.coerceIn(vStart, vEnd - currentItemInfo.size)
val draggedItemCenter = clampedTop + (currentItemInfo.size / 2f)
val targetItem = visibleItemsInfo.find {
    it.index != currentIndex &&
            draggedItemCenter > it.offset &&
            draggedItemCenter < (it.offset + it.size)
}
if (targetItem != null) {
    val item = localItems.removeAt(currentIndex)
    localItems.add(targetIndex, item)
    draggingIndex.value = targetIndex
}

内核 RowReorderEngine 逐行对应:clampedTop() 复刻 coerceIn,draggedCenterY() 复刻中心计算,findSwapTarget() 复刻碰撞判定,applySwap() 复刻 removeAt + add。自动滚动同样逐行:阈值 (vEnd - vStart) * 0.15f、上限 60f、线性爬升 (1f - 距离/阈值)、tick delay(16)——协程循环由宿主 16ms 定时器承担,速度决策仍在 Kotlin。

选择引擎照搬 Table.kt:全选是 addAll(filter { !contains })、反选是 removeAll(localItems)、行选是 add/remove、全选态是 isNotEmpty() && all { contains } }——连"有序列表 + 首次出现移除"的列表语义都原样保留。列宽求解照搬 totalMinWidth = ΣminWidth + 选择列 + 2×padding 与 > maxWidth 判定,权重分支复刻 RowScope.weight() 的"剩余空间按权重分配"。外观状态机照搬 TableRow 的 when 分支序(拖拽 → 选中 → 斑马纹奇数行 → 默认)与动画目标(scale 1.04 / elevation 12dp)。主题镜像把 16 个默认色转成 ARGB Long 字节级一致(含 .copy(alpha = 0.6f) → 0x99 前缀),暗色预设取自上游 Demo 的 DarkAnalytics 场景原值。

实测:

./gradlew :table:compileKotlinJvm :table:jvmTest      # BUILD SUCCESSFUL, 27/27
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

第 3 阶段:桥协议建模

3.1 JSON 契约
com.mamon.tablekmp.core/**(交互内核,ohos 编译)
        ↓ SelectionEngine / RowReorderEngine / TableLayoutSolver / RowAppearance
Bridge.kt(@CName × 1,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API tableKmpBridge(request)
        ↓
ArkUI Index.ets(三 Tab 单页)

12 个 op 一口进:

op请求要点响应要点
version—fork / 上游 / 工具链 / 目标
dataset—6 列定义 + 16 行(金额已格式化)
layoutviewportWidth / selectionEnabled / contentPaddingwidths[] / requiresHorizontalScroll / totalMinWidth
themedark16 色 + 斑马纹 + 状态点 + 拖拽常量 + 配置默认值
selectaction: row/all/clear + id / checkedselected[] / count / allSelected
clickid / highlightEnabledclickedId(-1 = 无)
dragStartindexdraggingIndex
swapCheck拖拽几何六元组 + rowCount / rowHeight / rowSpacingswapped / 新 order
autoScroll拖拽几何 + 视口speed(0 = 安全区)
dragEnd—最终 order
appearanceindex + 四个状态布尔 + darkcontainer / content / scale / elevation / role
reset—恢复初始会话

两个不对称设计:其一,appearance 的 selected 是点击高亮而不是复选框——上游 TableBody 传的是 isSelected = config.highlightClickedRow && internalClickedItem == item,复选状态走独立的 isRowChecked 通道,桥忠实保留这条双通道语义;其二,swapCheck 只让 ArkTS 上报几何事实(初始顶边、累计位移、视口边界、滚动偏移),clamp、中心计算、碰撞判定、序列交换全部在 Kotlin——ArkTS 是传感器,Kotlin 是决策者。

3.2 会话状态

数据、显示顺序、选择集、点击高亮、拖拽槽位全部持有在 Kotlin 侧 TableSession:示例数据是上游 sampleProjects 原样列表,金额列由上游 formatCurrency 原样产出,ArkTS 不重录任何数据或格式化逻辑。

3.3 错误通道

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

@CName("TableKmpBridge")
fun tableKmpBridge(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、未知 op、缺参,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。

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

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

ArkTS 不能直接 import Kotlin/Native 的 .so——鸿蒙 CAdapter 不把 @CName 业务符号导出给 ArkTS,所以必须手写 CMake NAPI 包装层。

4.2 Kotlin/Native 导出函数
@CName("TableKmpBridge")
fun tableKmpBridge(request: String): String   // 12 op 一口进

llvm-nm -D 验证双 ABI 均导出 TableKmpBridge 与 libohostablekmp_symbols 全局 T 符号。

4.3 C++ N-API 包装

napi_init.cpp 复刻社区已审模式:napi_typeof 校验字符串、OH_LOG_INFO 打字节数、调 KN 函数并包装返回;KN 返回的字符串必须经 libohostablekmp_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohostablekmp_api.h 手写声明 ExportedSymbols 结构与业务符号,不拖生成头。

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

add_library(tablekmp_napi SHARED napi_init.cpp)
target_link_libraries(tablekmp_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohostablekmp)

OHOS_ARCH 为空或 libohostablekmp.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录。ArkTS 侧类型声明一行:

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

第 5 阶段:ArkUI 宿主

5.1 握手与拖拽流
aboutToAppear(): void {
  this.loadVersion();     // 版本握手:来自真实 Native 调用
  this.applyTheme(false); // 主题 + 配置默认值
  this.loadDataset();     // 16 行数据 + 列定义
}

拖拽流是本 Demo 的交互核心:

长按 500ms   → dragStart(index),记录 initialTop = index×stride − scrollOffset
pan 移动     → swapCheck(draggingIndex, initialTop, dragY, 视口, scrollOffset, rowCount, rowHeight, rowSpacing)
               swapped=true → 按响应 order 重排,re-anchor:initialTop 重算、dragY 清零
16ms 定时器  → autoScroll(...) 得 speed ≠ 0 → scroller 滚动 speed → 立即再 swapCheck
抬手        → dragEnd → 最终顺序落定

拖拽中的行以 1.04 缩放 + 阴影 + 置顶渲染(动画目标即 RowAppearance.DRAG_SCALE / DRAG_ELEVATION),换位判定与自动滚动速度全部来自 Kotlin 响应。

5.2 页面交互

表格 Tab 自上而下:标题与版本徽标、表头(列宽来自 layout)、行区(状态点 / 优先级胶囊 / 线性进度条 / 右对齐金额)、状态行。窄屏自动横向滚动并显示自定义滚动提示条;开启多选后表头全选与行复选全部由 select op 驱动。设置 Tab 提供主题切换、7 个行为开关、加载/空态/会话重置与布局信息面板。关于 Tab 展示版本握手行、库简介、ArkTS → NAPI → C ABI → Kotlin 内核 → ArkUI 渲染 分层说明与仓库 / 社区链接。

5.3 对比度和错误边界

主操作用主题色,破坏性操作语义对齐;ok:false 时错误原文进状态行与 hilog;Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。配色全部来自 theme op,仅拖拽阴影遮罩与进度条强调色为固定值。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohostablekmp)+ copyKnSo*
│   └── src/ohosMain/kotlin/Bridge.kt         @CName + MiniJson + TableSession 会话
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.tablekmp、versionName 1.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/libohostablekmp_api.h
        ├── src/main/cpp/types/libtablekmp_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 表格 Demo(三 Tab)
6.2 构建、签名和安装
./gradlew :table:jvmTest                                          # 27 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
          :example:nativeApp:linkDebugSharedOhosX64               # 双 ABI so 回填 entry/libs

cd example/harmonyApp
ohpm install
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.tablekmp -m entry

四、完整代码对照

4.1 整体架构

com.mamon.tablekmp.core/**(交互内核,ohos 编译)
    │ SelectionEngine / RowReorderEngine / TableLayoutSolver
    │ RowAppearance / TableColorScheme / TableSettings / ColumnSpec
example/nativeApp Bridge.kt + MiniJson + TableSession
    │ TableKmpBridge(12 op JSON)
libtablekmp_napi.so
    │ tableKmpBridge(request)
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 表格 Demo(三 Tab 单页)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_tablekmp
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单条目
build.gradle.kts(根)版本 1.0.1-ohos.1、全局禁用 Ohos*Test* 任务
table/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64
table/src/composeMain/**上游 11 文件零改动隔离
table/src/commonMain/.../core/交互内核七件套(上游公式逐行对照)
table/src/jvmTest/**27 项交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、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/pages/Index.etsArkUI 表格 Demo
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
内核SelectionEngine.selectAll / onRowChecked / isAllSelected选择状态机(上游 Table.kt 语义)
内核RowReorderEngine.findSwapTarget / applySwap中心碰撞换位(上游 checkSwap)
内核RowReorderEngine.autoScrollSpeed15% 边缘阈值 + 60 上限速度决策
内核RowReorderEngine.itemGeometry由 rowHeight/spacing/scrollOffset 合成 LazyList 几何
内核TableLayoutSolver.solve / requiresHorizontalScroll列宽分配与横向滚动判定
内核RowAppearance.role / containerColor / contentColor外观分支与动画目标
内核TableColorScheme / TableSettings16 色与 8 开关默认值镜像(含 DarkAnalytics)
NativeTableKmpBridgeC ABI JSON 导出(12 op)
N-APItableKmpBridge(req)向 ArkTS 暴露能力
ArkTSrunSwapCheck / autoScrollTick / onRowCheck上报事实、渲染决策

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(tableKmpBridge(JSON.stringify({
  op: 'select', action: 'row', id: 7, checked: true
}))) as SelectionResp;
// res.selected = [7]; res.allSelected = false → 表头复选框熄灭、第 7 行复选框点亮
"row" -> session.selection.onRowChecked(json.num("id").toInt(), json.bool("checked"))
// 上游 Table.kt:if (checked) item?.let { selectedItems.add(it) } else selectedItems.remove(item)

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

五、关键决策说明

决策 1:提取交互内核,而不是等待 CMP 对齐

社区 CMP 线停在 Kotlin 2.2.21 可编译的版本,上游已经走到 Compose 1.10.3 / Kotlin 2.3.20,两条线短期内无法交汇。选择把"可决策的算法"提取成零依赖纯 Kotlin,而不是把整个库冻结降级——composeMain 里的上游原件一行没动,社区线追上后改回目录名即可整体恢复。

决策 2:算法语义逐行对照,27 项测试做闸门

提取最大的风险是"搬运时改了语义"。处置是每个内核文件的公式都与上游片段逐一对照(clamp、碰撞、阈值、分支序、默认值),再用 27 项 jvmTest 钉死:JVM 与 ohos 跑同一份内核代码,测试全绿即"内核没漂"。

决策 3:状态全在 Kotlin 会话,数据 typed once

顺序、选择、点击、拖拽槽位的唯一权威是 Kotlin 侧 TableSession;示例数据与金额格式化用上游原件,ArkTS 不重录。ArkTS 崩溃或页面重建后一次 dataset + select 查询即可恢复全部状态。

决策 4:忠实保留 isSelected 与 isRowChecked 双通道

上游行外观的 isSelected 是点击高亮(highlightClickedRow && clickedItem == item),复选框是独立通道。桥的 appearance.selected 与 select 两个 op 分开承载这条语义,避免"合并成一个选中"造成的静默行为漂移。

决策 5:根构建替换(cmp_qrose 模式)

上游根构建(AGP / compose / vanniktech 发布 / 桌面打包)对鸿蒙没有保留价值,整个替换为极简 ohos 构建;composeApp / iosApp 留盘不进 settings.gradle.kts——它们的存在方式只有这一种。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / ohpm / hdc)与 HarmonyOS 设备(arm64)。

6.2 JVM 交叉验证

./gradlew :table:jvmTest

27 项用例全部通过:

测试类用例数钉住什么
TableLayoutSolverTest6totalMinWidth 求和、选择列宽度差、横向滚动判定、窄屏冻结 minWidth、权重分配精确值、固定+弹性混排
SelectionEngineTest5行选顺序、全选只追加未选、反选只移除可见行、空表全选态、部分选中
RowReorderEngineTest8clamp 双向、中心偏移、换位触发/不触发、removeAt+add 语义、滚动偏移下的几何合成、自动滚动线性爬升(含浮点边界教训 800f*0.15f=120.000004f)、上游常量钉版、三连换位全流程
RowAppearanceTest8分支序、拖拽回落、斑马纹 ARGB、明暗选中色、拖拽>悬停>底色的优先级、内容色切换、1.04/12 动画目标、默认调色板与 hex

6.3 双 ABI 与 HAP 验证

:table:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 → arm64-v8a/libohostablekmp.so 3,835,976 字节(约 3.7MB debug 未 strip)+ libc++_shared.so 1,850,184
linkDebugSharedOhosX64   → x86_64/libohostablekmp.so 3,538,136 字节(约 3.4MB)+ libc++_shared.so 1,731,432
llvm-nm -D: TableKmpBridge / libohostablekmp_symbols 均为全局 T 符号
CompileArkTS: finished
BuildNativeWithCmake: finished(双 ABI 编出 libtablekmp_napi.so)
PackageHap: finished
SignHap: finished
BUILD SUCCESSFUL

entry-default-unsigned.hap 约 6.7MB(7,067,195 字节),内含双 ABI 各三件套(libtablekmp_napi.so / libohostablekmp.so / libc++_shared.so)。

6.4 功能用例

  1. 启动后关于页出现版本握手 1.0.1-ohos.1 / 上游 1.0.1 / Kotlin 2.2.21-1.0.0(来自真实 Native 调用,非静态文字);
  2. 表格页渲染 16 行数据,状态点配色正确(Active 绿 / Done 蓝 / Failed 红 / Pending 黄),金额列格式化为千分位;
  3. 窄视口下仅显示前三列,requiresHorizontalScroll=true,表格可横向滚动并显示滚动提示条;
  4. 设置页开启多选后出现 50vp 复选列;勾选单行 → 状态行回显"已选 N 行";表头全选 → 16 行全亮;
  5. 开启点击高亮后点击行 → 该行以 selectedRowContainer/Content 重绘;关闭开关点击 → clickedId 不变(上游语义);
  6. 开启拖拽后长按 500ms 拖动 → 拖拽行 1.04 缩放置顶,跨过邻行中心即换位,顺序实时更新;拖到视口边缘 15% 区域触发自动滚动;
  7. 切换深色主题 → 整页切换 DarkAnalytics 调色板,行高 65 / 边框 1 生效;
  8. 加载开关 → 表格区覆盖 LoadingProgress;清空数据 → 居中"暂无数据";重置会话 → 顺序/选择/高亮全部复原。

6.5 验证结论

交互内核、双 ABI 动态库、C ABI、N-API、ArkTS 页面和 HAP 已完成链路验证。应用在 HarmonyOS 设备上成功安装并进入前台,页面展示了真实的表格会话:版本握手、数据渲染、列宽自适应、选择、点击高亮、拖拽换位与主题切换闭环(见下节效果图)。

七、运行效果

7.1 运行截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/Table-KMP.git
cd Table-KMP
export JAVA_HOME="/path/to/jdk-21"
./gradlew :table:jvmTest                                          # 27 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
          :example:nativeApp:linkDebugSharedOhosX64               # 双 ABI so
cd example/harmonyApp
ohpm install
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.tablekmp -m entry
hdc shell hilog | grep TableKmp

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让交互内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Gradle wrapper 必须换到 8.14.1——HarmonyOS Kotlin 2.2.21 线与 8.14.1 匹配,上游 8.14.3 起不动;
  4. ArkTS 的 @Builder 不允许调用闭包参数渲染子组件——卡片外壳必须改成 @Component + @BuilderParam + 尾随闭包;
  5. 自定义组件属性名不能撞内置属性:tabIndex、borderColor 都是 CustomComponent 保留名,撞名报"is not assignable to the same property in base type";
  6. 本 SDK 的 Toggle 构造只收对象字面量 Toggle({ type, isOn }),双位置参数形式报 “Expected 1 arguments”;
  7. GestureMode 没有 Begin 成员;长按后拖拽用 GestureMode.Parallel + 处理器内按 draggingIndex >= 0 门控;
  8. GestureEvent 的位移是 offsetY 字段,不是 offset.y;
  9. hvigor 打包器是 Java 工具:PATH 上没有 java 时报 00308018 spawn java ENOENT,把 JDK 21 加进 PATH 即可;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor 拒绝(00303038)——仓库内留空数组,勾选自动签名后由 IDE 回填;
  13. 浮点边界:800f * 0.15f = 120.000004f,自动滚动的边界断言不能恰好压边,测试取离边值;
  14. 上游 gradle.properties 里有误提交的 Maven Central 密码与 GPG 私钥口令——fork 已移除,建议上游吊销轮换。

8.2 已知问题

  • composeMain 里的上游 Compose 源码待社区 CMP 线与上游对齐后整体恢复编译;
  • 上游 TableColumn 的 @Composable content lambda、Dp/Shape/Alignment 类型无法下沉,ohos 侧以 ColumnSpec + 声明式描述替代;
  • Compose 快照状态(mutableStateListOf 的响应式推送)不跨 FFI:桥是同步一问一答,ArkTS 以响应驱动重绘;
  • hoverEnabled / rippleEffectEnabled 在触屏设备无实际效果(保留开关与上游语义);
  • debug so 未 strip(arm64 约 3.7MB),HAP 内 strip 后显著更小;
  • android / ios / js / wasmJs 目标剥离(源文件留在树里),本 fork 不发 Maven Central。

8.3 未来优化方向

  • 社区 CMP 线对齐后把 composeMain 改回 commonMain,恢复整库编译;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 双 ABI 编译、llvm-nm 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
  • 把上游 5 个 Demo 场景(EnterpriseGrid / TaskBoard / DarkAnalytics / AdminDashboard / AllUsersDirectory)全部移植到 ArkTS 场景切换。

九、总结

9.1 核心难点回顾

上游 table(11 文件 100% Compose 依赖)
    → 确定交互内核边界(选择/换位/列宽/外观/主题)
    → 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 + Gradle 8.14.1)
    → composeMain 零改动隔离 + 内核七件套逐行对照提取
    → ohosArm64 / ohosX64 KLIB + 27 项 jvmTest 交叉验证
    → C ABI(@CName × 1)+ 12 op JSON 协议
    → C++ N-API(DisposeString 纪律)
    → ArkTS 拖拽坐标模型 + 16ms 自动滚动
    → ArkUI 三 Tab 表格 Demo(选择/换位/主题/加载空态/版本握手)

9.2 封装层次

  • 内核层:上游算法语义的选择引擎、换位物理、列宽求解、外观状态机与主题/配置默认值;
  • Native 层:双 ABI 动态库,单 C ABI 输出 JSON,会话状态唯一权威;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、手势上报、响应渲染、日志与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和设备运行。

9.3 三条经验

  1. 交互库的适配面 = 算法内核 + 桥 + 宿主渲染:判定标准是"决策是否仍在 Kotlin"。Table-KMP 没有一行现成纯 Kotlin 代码,但换位公式、选择语义、宽度分支这些"决策"可以被完整搬运——搬运完的 ArkTS 只剩传感器和渲染器两个角色;
  2. 交叉验证是"内核没漂"的唯一可信证明:27 项 jvmTest 跑的是与 ohos .so 完全相同的内核代码,公式对照 + 测试钉版双保险,提取才敢声称"语义一致";
  3. 零依赖内核让制品线障碍消失,但换来提取纪律:不需要验证任何 kotlinx ohos 制品是本仓的运气;代价是每个公式、每个默认值、每条分支序都要与上游逐一对照并写进 KDoc——提取的工时花在"对答案"上,而不是写代码上。

9.4 适配成果

当前适配已完成:

  • 上游 table 库交互内核的双 ABI Kotlin/Native 编译(Compose 源码零改动隔离,内核七件套公式逐行对照);
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三 Tab 验收页面(选择、拖拽换位、自动滚动、列宽自适应、主题、点击高亮、加载/空态、版本握手);
  • 27 项 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

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐