开源鸿蒙平台 CMP 三方库「tanstack-table-kmp」适配全流程

本文记录 tanstack-table-kmp 接入 OpenHarmony 的完整过程,覆盖表格库盘点、鸿蒙工具链钉版、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

tanstack-table-kmp 是 TanStack Table v8 的 Kotlin Multiplatform 移植:无头表格引擎 :table-core(排序、筛选、分组、分页、选择、固定、可见性、列序、列宽的完整状态机)加 Compose 适配层 :table-compose(rememberTable / TableGrid / flexRender)。本次适配与社区两条既有路线都不同:qrose / haze 走"零依赖内核提取",KStore 走"kotlinx 制品线真编译"------而 table-core 的 44 个 commonMain 文件本身就是零依赖纯 Kotlin (逐文件核对 import,全库只出现 kotlin.math 与 kotlin.time),既不需要提取内核也不需要验证制品线,真实引擎(Table、ColumnDef、九大特性、行模型管线)被原样编译成 OpenHarmony 动态库 ,一个完整的 TanStack v8 状态机运行在真机上,ArkUI 六 Tab 页面作为验收宿主。真正的难点不在编译,在表格语义(交互状态机)怎么跨语言边界------答案是无状态 JSON 桥 + 状态往返 + 动作走引擎自身 accessor。

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

上游仓库: praveenshharma/tanstack-table-kmp

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

tanstack-table-kmp 为 Kotlin Multiplatform 应用提供"数据 ↔ 表格"的完整答案:TableOptions(data, columns) 一行建表,getCoreRowModel / getSortedRowModel / getFilteredRowModel / getGroupedRowModel / getExpandedRowModel / getPaginationRowModel 六级行模型管线按需接线,column.toggleSorting() / row.toggleSelected() / table.nextPage() 等语义化 accessor 驱动内部状态机,onStateChange 把每次状态迁移回调给宿主。上游把它拆成两层::table-core 是 100% common 的无头引擎,:table-compose 把引擎桥进 Compose 重组。KMP 项目由 Android、iOS 共享这套逻辑。OpenHarmony 应用同样需要这套表格能力,但上游构建(Kotlin 2.1.0 / Gradle 8.10 / AGP 8.7.3 / CMP 1.7.3)不会生成 OpenHarmony 变体。

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

障碍 具体问题
目标缺失 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 产物。
工具链不一致 上游 Kotlin 2.1.0 / Gradle 8.10 / AGP 8.7.3 与鸿蒙工具链无法共存于一个 Gradle 构建;HarmonyOS Kotlin 2.2.21-1.0.0 旧线必须配 Gradle 8.14.1。
Compose 适配层编不过 :table-compose 全部站在 androidx.compose.* 上;社区鸿蒙 CMP 是 1.9.2,与上游 CMP 1.7.3 / Kotlin 2.1 的编译器插件与依赖图对不齐。
语言边界不同 ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
表格语义跨不了桥 排序三态循环、子行级联选择、分组展开是引擎状态机语义,C ABI 只能同步一来一回------不能在 ArkTS 里重写一份状态机,否则必然漂移。
交付链路复杂 .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:无头引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。表格语义完全由上游原版实现负责,ArkTS 只负责发起动作、解析 JSON 和渲染视图模型。

1.2 库提供的能力

Demo 用六个 Tab 覆盖引擎六条特性管线,对应桥接的全部入口:

  • 基础(basic) :getCoreRowModel() 直出 10 行 6 列,无任何特性接线;
  • 排序(sorting) :8 列,点表头循环"无 → 升 → 降 → 无";status 列走自定义排序函数(按 single → complicated → relationship 业务优先级而非字典序);lastName / visits 列每 10 行制造 null 值钉住 sortUndefined = "last";rank 列 invertSorting = true(小者优先);
  • 筛选(filters) :getFilteredRowModel() + 全局筛选(输入框实时过滤)+ firstName 列筛选 + status 面值统计 chip(getFacetedUniqueValues() 带计数,点击即过滤);
  • 分页(pagination) :getPaginationRowModel(),首页/上一页/下一页/末页 + 8/10/20 页大小切换,页数与总数来自引擎;
  • 分组(grouping) :getGroupedRowModel() + getExpandedRowModel(),三级嵌套表头(Name / Info / More Info),默认按 status 分组,表头 ▢/▣ 按钮换分组列,组行展开收起,聚合格橙色------age = median、visits = sum、progress = mean;
  • 选择(selection) :enableRowSelection = true,行首勾选、全选本页、已选计数;rowSelection 按 id 存活,翻页后选择保持。

响应包含:

字段 作用
ok 操作是否成功,失败时 error 带原文。
headerGroups 多级表头(分组 Tab 三级),每个 header 带 label / colSpan / canSort / sortDir / canGroup / isGrouped。
rows 最终行模型的每一行:id / depth / isGrouped / subRowCount / canExpand / isSelected 与逐格 cells(kind = grouped / aggregated / placeholder / plain)。
pagination / selectedCount / faceted 分页视图、选择计数、面值表。
state / status 完整引擎状态回显(下一轮请求原样回传)与引擎侧状态栏摘要。

引擎侧完整能力(列可见性、列顺序、列/行固定、列宽、footer)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 29 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的表格逻辑没有漂移。

1.3 实现适配

维度 要求
代码复用 table-core 的全部 44 个 commonMain 文件原样编译,不提取、不重写、零新增 Kotlin 源文件(引擎侧一行不改)。
平台目标 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块纯 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离 根构建替换为极简 ohos 构建(cmp_qrose 模式);table-compose / sample / iosApp(纯 CMP/AGP)留盘不进构建。
桥接稳定 单个 C ABI 入口 TableRender + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
语义不漂移 桥无状态,动作经引擎自身 accessor 执行;排序循环、级联选择、分组展开的语义只有上游一份实现。
UI 完整 单页六 Tab 覆盖全部桥接入口:表头点击排序、筛选输入、面值 chip、翻页、分组展开、行选择。
仓库规范 项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。表格是数据引擎而非图像:ArkTS 拿到的不是 qrose 那样的 PNG 位图,而是引擎算好的结构化 行/列模型,用原生 ArkUI 组件渲染并继续可交互------点击表头直接回到引擎的 toggleSorting。

二、实现路线图

text 复制代码
第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认引擎纯度、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模     ── 无状态 JSON 契约、状态往返、动作表、MiniJson
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 六 Tab、表头/行渲染、控件区、状态栏
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
text 复制代码
table-core/                           上游无头引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64,零源码改动)
table-compose/                        上游 Compose 适配层(2 文件,纯 androidx.compose,留盘不进 settings)
sample/  iosApp/                      上游 Android/iOS 演示(纯 CMP/AGP/Xcode,留盘不进构建)
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
docs/                                 上游文档与本篇文章 + img/ 效果图

构建拓扑走 cmp_qrose 模式 (根构建即 ohos 构建):settings.gradle.kts 只 include :table-core 与 :example:nativeApp,rootProject.name = "ohos_tanstack_table"。上游的 dokka / vanniktech 发布插件、AGP / Compose 插件链、foojay resolver、iOS framework 构建段全部随根构建替换剥离------本 fork 不发 Maven Central。

1.2 工具链矩阵
项目 配置
Kotlin Multiplatform HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK 21(org.gradle.java.home 指向 Temurin jdk-21.0.12.101-hotspot)
OpenHarmony 目标 ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle 8.14.1(上游 wrapper 8.10 升级;2.2.21 旧线必须配 8.14.1,qrose 先例)
引擎依赖 零(见 1.3 源码依赖度盘点------这是本批适配库里唯一的零依赖引擎)
DevEco product 5.1.0(18) 兼容(hvigor modelVersion 5.1.0)
DevEco Studio 5.1.0 Release
ABI arm64-v8a + x86_64
Bundle org.terminator.ohos.table
powershell 复制代码
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version   # gradlew 启动器自身也需要 JAVA_HOME,org.gradle.java.home 只管 daemon
1.3 源码依赖度盘点
模块 文件数 依赖形态 处置
table-core commonMain 44 全库 import 只有 kotlin.math.* / kotlin.time.*,零第三方、零 UI、零 expect/actual、零协程 整体编译(jvm + 双 ohos,零源码改动)
table-core commonTest 8 kotlin("test") 保留,JVM 跑交叉验证
table-compose 2 androidx.compose.runtime/foundation/ui 留盘不进构建
sample / iosApp --- AGP / CMP / material3 / XcodeGen 留盘不进构建

table-core 的纯度值得单独说明:对 44 个文件全量 grep ^import,除自身包外只出现 kotlin.math.floor/max/min/round 与 kotlin.time.TimeMark/TimeSource 六个符号------没有协程、没有序列化、没有 expect/actual。这意味着 KStore 适配里的"制品线验证"步骤在本库整体不存在 :不存在"某个 kotlinx 依赖没有 ohos 变体"的可能,引擎直接编。全库唯一的接缝在 :table-compose(rememberTable 把 Table 桥进 Compose 重组),而它恰好是 Compose 依赖最重、当前最编不过 ohos 的部分------上游的引擎/渲染分层设计让鸿蒙适配面天然最小。

1.4 页面边界
text 复制代码
标题区          TANSTACK-TABLE-KMP · OPENHARMONY / 副标题(table-core 无头引擎 · Kotlin/Native ohosArm64/ohosX64)
状态行          引擎侧 status 摘要(如 "grouping: status · 3 groups on page")
Tab 切换        基础 / 排序 / 筛选 / 分页 / 分组 / 选择
控件区          随 Tab 变化:重置排序、筛选输入框、面值 chip、翻页钮、分组钮、选择钮、刷新数据
表头区          多级表头,列头可点击排序(▲/▼ 指示),分组 Tab 带 ▢/▣ 分组切换徽标
行区            展开箭头 / 勾选框前导列 + 逐格渲染(绿=分组键、橙=聚合、灰=占位)
页脚            仓库与许可证行

页面启动时 aboutToAppear 即发起首次 tableRender 调用,基础 Tab 数据直达。

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

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

本库引擎零依赖,没有 KStore 那种"coroutines 制品线反推 stdlib 版本"的约束,直接沿用社区早期批准的钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款,唯一带 ohosArm64() / ohosX64() 的发行版)。这条旧线的代价是 Gradle 必须配 8.14.1(上游 wrapper 8.10 升一级),并保留 kotlin.native.ignoreDisabledTargets=true 等三个 KN 开关。

settings.gradle.kts 把 eazytec 仓库写在最前:

kotlin 复制代码
pluginManagement {
    repositories {
        // HarmonyOS Kotlin fork (2.2.21-1.0.0) --- must resolve before the portals.
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        mavenCentral()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        mavenCentral()
    }
}

rootProject.name = "ohos_tanstack_table"

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

gradle/libs.versions.toml 从 34 行裁到 4 行------整个插件链只剩一个 kotlinMultiplatform 别名;AGP / Compose / dokka / vanniktech 全部随根构建替换消失。

2.2 编译真实引擎

table-core/build.gradle.kts 从 92 行(含 AGP namespace、androidTarget、三个 iOS 目标、Maven Central 发布块)收敛为纯 KMP 模块:

kotlin 复制代码
plugins {
    alias(libs.plugins.kotlinMultiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()          // 交叉验证 + 跑上游 29 项单测
    ohosArm64()
    ohosX64()

    sourceSets {
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

实测:

text 复制代码
:table-core:compileKotlinJvm               BUILD SUCCESSFUL
:table-core:compileKotlinOhosArm64         BUILD SUCCESSFUL
:table-core:jvmTest                        29 项全绿

引擎的 44 个 commonMain 文件一个没动------本 fork 引擎侧的新增源文件数是 0。

2.3 测试依赖的处置

commonTest 对 ohos 测试编译可见,但 ohos 测试执行器未接线。处置与 KStore 相同:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:

kotlin 复制代码
subprojects {
    // The ohos test runners are not wired up in this fork; only JVM tests run.
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

上游测试文件零改动保留,ohos 侧只承担编译与链接。

第 3 阶段:桥协议建模(本适配的核心设计)

3.1 JSON 契约
text 复制代码
io.github.tanstacktable.core/**(上游引擎,原样编译)
        ↓ Table / ColumnDef / 九大特性 / 行模型管线
Bridge.kt(@CName × 1,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API tableRender()
        ↓
ArkUI Index.ets(六 Tab)

请求与响应:

text 复制代码
请求 : {"tab":"sorting","state":{...},"action":{"type":"toggleSort","columnId":"age"},"dataSeed":0}
       state  = 上一次响应回显的 state,原样回传;首次为 null
       action = 可选,每请求至多一个(20 种,见下表)
响应 : {"ok":true,"tab":"...","engine":"...","headerGroups":[...],"rows":[...],
        "pagination":{...},"selectedCount":N,"faceted":{...},"state":{...},"status":"..."}
       {"ok":false,"error":"..."}
3.2 无状态桥 + "两阶段表"(为什么这么设计)

表格是有状态引擎,但桥不持有任何 Table 实例。每个请求走两阶段:

kotlin 复制代码
// 阶段一:变体表------state = 请求携带态,onStateChange 捕获引擎算出的新状态
var captured: TableState? = null
val mutationTable = buildTable(tab, data, state,
    onStateChange = { updater -> captured = functionalUpdate(updater, state) })
applyAction(mutationTable, action)        // column.toggleSorting / table.nextPage / row.toggleSelected ...
captured?.let { state = it }

// 阶段二:干净表------用新状态重建,序列化视图模型
val table = buildTable(tab, data, state, onStateChange = null)
return render(tab, table, state)

这么做的依据和收益:引擎 setter 走 makeStateUpdater → setState → options.onStateChange,并不回写 options.state;所有行模型 memo 的 deps 读 table.getState()(即 options.state)。两阶段重建(100 行数据成本微秒级)换来三件事------零隐藏状态 (ArkTS 崩了重来、热重载、多实例互不影响)、不依赖 memo 失效细节 、状态即数据往返 (响应里的 state 就是下一轮请求的 state,ArkTS 不需要理解内部结构)。

3.3 动作表:语义留在引擎侧

20 种动作全部是引擎自身 accessor 的薄封装:

type 引擎调用
toggleSort / resetSorting column.toggleSorting(null, false) / table.setSorting([])
firstPage / previousPage / nextPage / lastPage 同名 accessor
setPageIndex / setPageSize 同名 setter
toggleGroup / clearGrouping column.toggleGrouping() / table.setGrouping([])
toggleExpand / toggleExpandAll / collapseAllGroups row.toggleExpanded(null) / toggleAllRowsExpanded(null) / setExpanded({})
toggleRowSelected / toggleAllPageRows / clearSelection row.toggleSelected(null, null) / toggleAllPageRowsSelected(null) / setRowSelection({})
setGlobalFilter / clearGlobalFilter table.setGlobalFilter(v / null)
setColumnFilter / clearColumnFilters column.setFilterValue(v) / setColumnFilters([])

排序的"无 → 升 → 降 → 无"循环、sortDescFirst、子行级联选择、toggleAllPageRows 的半选态,这些语义上游已实现且有单测------在 ArkTS 重写一份必然漂移,所以一个都不重写。

3.4 视图模型序列化

行模型逐行序列化,cells[].kind 四态对齐上游 Grouping 示例的着色语义:

text 复制代码
grouped      分组键格(绿)    ← cell.getIsGrouped(),值取 row.groupingValue
aggregated   聚合格(橙)      ← cell.getIsAggregated(),值取 aggregationFn 计算结果
placeholder  占位格(灰)      ← cell.getIsPlaceholder(),值恒为空串
plain        普通格          ← flexRender 等价解析(cell 模板是 lambda 则调用,否则原样)

JSON 解析/序列化用手写 MiniJson(递归下降,~230 行),不引 kotlinx.serialization------与 qrose 同款取舍,.so 保持零第三方依赖。

3.5 错误通道

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

kotlin 复制代码
@CName("TableRender")
fun tableRender(request: String): String = try {
    val req = MiniJson.parse(request) as? Map<*, *> ?: emptyMap<String, Any?>()
    MiniJson.write(handleRequest(req))
} catch (t: Throwable) {
    MiniJson.write(mapOf("ok" to false, "error" to (t.message ?: t.toString()), "engine" to ENGINE_VERSION))
}

非法 JSON、未知 tab、未知 action、getRow 找不到行(分组重算后旧 id 消失),全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。

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

4.1 三层桥接
text 复制代码
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libtanstacktable_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libtanstacktable.so)
  │ TableRender / libtanstacktable_symbols
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;表格引擎的输出本来是结构化模型,JSON 是它的自然序列化形式,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
kotlin 复制代码
@CName("TableRender")
fun tableRender(request: String): String   // tab 分发 + action + render 一口进
C ABI 符号 ArkTS 方法 覆盖内核
TableRender tableRender(req) 建表(options.state)+ 20 种动作 + 六级行模型序列化
libtanstacktable_symbols (N-API 内部) KN 运行时字符串释放接口

llvm-nm -D 实测双符号均为全局 T。

4.3 C++ N-API 方法分发
cpp 复制代码
napi_property_descriptor desc[] = {
    {"tableRender", nullptr, NapiTableRender, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

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

4.4 CMake 和类型声明
cmake 复制代码
add_library(tanstacktable SHARED IMPORTED)
set_target_properties(tanstacktable PROPERTIES
  IMPORTED_LOCATION ${KN_SO}          # entry/libs/<OHOS_ARCH>/libtanstacktable.so
  IMPORTED_NO_SONAME TRUE)

add_library(tanstacktable_napi SHARED napi_init.cpp)
target_link_libraries(tanstacktable_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  tanstacktable)

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

typescript 复制代码
export const tableRender: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 发起调用与状态往返
typescript 复制代码
private call(action?: Action): void {
  const body: string = JSON.stringify({
    tab: this.tab,
    state: this.tabState[this.tab] ?? null,   // 上一轮响应的 state,原样回传
    action: action ?? null,
    dataSeed: this.dataSeed
  });
  const raw: string = tableRender(body);
  const parsed: Resp = JSON.parse(raw) as Resp;
  if (!parsed.ok) { this.statusText = `失败: ${parsed.error ?? 'unknown'}`; return; }
  this.tabState[this.tab] = parsed.state;     // 存下新一轮回显
  this.headerGroups = parsed.headerGroups;
  this.rows = parsed.rows;
  this.pagination = parsed.pagination;
  this.statusText = parsed.status;
}

每个 Tab 一份独立 tabState,切 Tab 不串状态;dataSeed 驱动确定性数据重生成("刷新数据"按钮)。

5.2 页面交互

六 Tab 共用一套表格渲染器(多级表头 + 行 + 前导列),差异只在控件区:排序 Tab 是重置钮 + 表头点击;筛选 Tab 是全局/列两个输入框 + 面值 chip(带计数,激活变黑);分页 Tab 是四向翻页钮 + 页大小钮;分组 Tab 是全部展开/全部收起/清除分组 + 表头 ▢/▣ 徽标;选择 Tab 是勾选框前导列 + 全选本页 + 已选计数。列宽固定 118vp、横向滚动,与上游 sample 的 horizontalScroll 行为一致;格子按 kind 四色着色,分组键格显示 值 (叶子数)。

5.3 对比度和错误边界

表头深灰底、分组行灰底、分组键绿、聚合橙、占位灰字;ok:false 时错误原文进状态行,hilog 同步留痕;Native 调用本身抛错同样被 try/catch 收进状态行。

第 6 阶段:示例与验证

6.1 工程结构
text 复制代码
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=tanstacktable)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/io/github/tanstacktable/ohos/
│       ├── Bridge.kt                         @CName("TableRender") + 两阶段表 + 20 动作
│       └── MiniJson.kt                       手写 JSON(继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.table、versionName 0.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/libtanstacktable_api.h
        ├── src/main/cpp/types/libtanstacktable_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 六 Tab 验收页面
6.2 构建、签名和安装
powershell 复制代码
.\gradlew.bat :table-core:jvmTest                                          # 29 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录)
# File → Project Structure → Signing Configs 勾选自动生成,然后:
hdc install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.table -m entry

签名材料由 DevEco 写入本机,仓库内 build-profile.json5 的 signingConfigs 保持空数组,证书绝不进入 AtomGit。

四、完整代码对照

4.1 整体架构

text 复制代码
io.github.tanstacktable.core/**(上游引擎原样编译)
    │ Table<TData> / ColumnDef / TableState / 九大 TableFeature
    │ 行模型管线:core → filtered → sorted → grouped → expanded → pagination
Bridge.kt + MiniJson
    │ TableRender(无状态,状态往返,动作走引擎 accessor)
libtanstacktable_napi.so
    │ tableRender
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(六 Tab:基础/排序/筛选/分页/分组/选择)

4.2 文件清单

文件 职责
settings.gradle.kts 极简 ohos 构建、eazytec Maven 置前、ohos_tanstack_table、Compose 模块移出
gradle/libs.versions.toml 只剩 HarmonyOS Kotlin 2.2.21-1.0.0 一个版本号
build.gradle.kts(根) 版本 0.1.0-ohos.1、全局禁用 Ohos*Test* 任务
table-core/build.gradle.kts 纯 KMP:jvm + ohosArm64 + ohosX64(引擎源码零改动)
example/nativeApp/build.gradle.kts 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt 1 个 C ABI、两阶段表、20 动作、MiniJson、错误通道
nativeApp/.../MiniJson.kt 手写递归下降 JSON 解析/序列化
harmonyApp/.../cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.ets ArkUI 六 Tab 验收页面
README.OpenHarmony_CN.md 鸿蒙适配主文档(24 节)

4.3 关键 API 对照

层次 API 作用
引擎 createTable(TableOptions) 建表;options.state 直接驱动行模型(getState() = options.state)
引擎 getCoreRowModel() 等 6 个 行模型管线,按 Tab 差异化接线
引擎 column.toggleSorting / getIsSorted 排序循环与指示(响应 sortDir)
引擎 row.toggleSelected / getIsSelected / getIsSomeSelected 选择与半选态
引擎 row.getIsGrouped / groupingValue / subRows 分组行模型
引擎 cell.getIsGrouped / getIsAggregated / getIsPlaceholder 聚合格三态(响应 kind)
引擎 column.getFacetedUniqueValues 面值统计(响应 faceted)
引擎 functionalUpdate + onStateChange 两阶段表的状态捕获通道
Native TableRender C ABI JSON 导出
N-API tableRender(req) 向 ArkTS 暴露能力
ArkTS call(action) 发起动作并把响应灌回 @State

4.4 ArkTS 与 Kotlin 的边界

typescript 复制代码
// 点 Age 表头:回传 res.state + 动作,引擎完成"无→升→降→无"循环
const raw = tableRender(JSON.stringify({
  tab: 'sorting',
  state: res.state,
  action: { type: 'toggleSort', columnId: 'age' }
}));
res = JSON.parse(raw);
// res.headerGroups[0].headers[2].sortDir === 'asc',res.rows 已按新序重排
kotlin 复制代码
// 桥侧:动作经引擎 accessor,桥不理解排序规则
"toggleSort" -> table.getColumn(str(action["columnId"]) ?: "")?.toggleSorting(null, false)

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

五、关键决策说明

决策 1:引擎零改动整体编译

table-core 的 44 个文件是本批适配库里唯一的零依赖引擎------没有 kotlinx 制品线问题,没有 expect/actual 要补,不需要 qrose 式内核提取,也不需要 KStore 式换线验证。上游的引擎/渲染分层(table-core / table-compose)让鸿蒙适配面天然最小:编不过 ohos 的部分恰好全部在渲染层,而渲染层恰好可以整体留盘。

决策 2:无状态桥 + 状态往返

表格引擎有状态,但状态可以是数据 而不是会话 :每个响应回显完整 state,下个请求原样回传。跨语言的生命周期管理、线程归属、崩溃恢复问题全部消失,代价只是每请求重建一张表(100 行数据微秒级)。这比在桥里维护 Table 实例注册表(按 id 持有、失效、淘汰)简单一个数量级,而且 ArkTS 侧始终持有唯一 UI 真源。

决策 3:动作走引擎自身 accessor

排序三态循环含 sortDescFirst / invertSorting / sortUndefined 的分支、toggleAllPageRowsSelected 的半选计算、分组行的展开拍平------这些语义上游已实现且被 29 项单测钉住。桥里 20 种动作全是单行薄封装,一行语义都不重写。

决策 4:jvm() 保留 + 交叉验证

引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;jvmTest 跑的就是上游原测试文件,是"内核没漂"的闸门。怀疑鸿蒙侧行为异常时,先用相同数据在 JVM 复现------数据生成规则(makeData)与上游 sample 同型,可平移成单测。

决策 5:根构建替换(cmp_qrose 模式),Compose 模块留盘

上游根构建(AGP / Compose / dokka / vanniktech / iOS framework)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。table-compose / sample / iosApp 留盘不进 settings.gradle.kts:源码零改动地等社区 CMP 工具链与 HarmonyOS Kotlin 对齐后接回,届时桥降级为无 Compose 场景的永久通路。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

powershell 复制代码
.\gradlew.bat :table-core:jvmTest

29 项用例全部通过(上游原测试文件,零改动):

测试类 用例数 钉住什么
RowSelectionTest 13 选择状态机:单选/级联/半选/过滤后保持
RowPinningTest 7 行固定:top/bottom 拍平与 id 稳定
RowSelectionReactTest 4 React 式状态更新语义
CoreEngineTest 3 核心行模型与列解析
GetGroupedRowModelTest 1 分组/聚合行模型
ColumnVisibilityTest 1 列可见性

6.3 HAP 验证

text 复制代码
:table-core:compileKotlinJvm / jvmTest:                    BUILD SUCCESSFUL(29 项全绿)
:table-core:compileKotlinOhosArm64:                        BUILD SUCCESSFUL
:example:nativeApp:compileKotlinOhosArm64:                 BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64:         BUILD SUCCESSFUL
llvm-nm -D:                                                TableRender / libtanstacktable_symbols 均为全局 T 符号
copyKnSoArm64 / copyKnSoX64:                               entry/libs 双 ABI 三件套回填
BuildNativeWithCmake + CompileArkTS + PackageHap:          BUILD SUCCESSFUL
install bundle successfully / start ability successfully   真机 arm64

libtanstacktable.so debugShared 未 strip 时 arm64 约 7.2MB、x86_64 约 6.6MB(含 Kotlin/Native 运行时与完整引擎);libc++_shared.so arm64 约 1.85MB。

6.4 功能用例

  1. 启动进入基础 Tab,10 行 6 列直出,状态栏 10 rows · core row model;
  2. 排序 Tab 点 Age 表头三次:无 → ▲ → ▼ → 无,状态栏同步 sort: age asc/desc/none;点 Status 列按 single → complicated → relationship 排(自定义排序函数生效);每 10 行的 null lastName 始终沉底(sortUndefined = "last");
  3. 筛选 Tab 全局框输入 ta,行数实时收敛,状态栏 N/100 rows pass · global='ta';status 面值 chip 显示三个值的计数,点击激活变黑即列过滤,再点取消;
  4. 分页 Tab 四向翻页 + 8/10/20 页大小切换,状态栏 page k/N · pageSize n · 100 rows,首页在第一页时置灰;
  5. 分组 Tab 默认 grouping: status,三个组行(绿键格 + (N) 叶子数);展开后子行出现;聚合格橙色------age = median、visits = sum、progress = mean;点其他表头 ▣ 换分组列,清除分组 回到平铺;
  6. 选择 Tab 行首勾选,状态栏 N selected;翻页后选择保持;全选本页只作用于当前页;
  7. 每步交互状态栏都显示引擎侧 status 摘要------数据来自真实 Native 调用而非静态文字。

6.5 验证结论

表格引擎、行模型管线、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,六个 Tab 的排序循环、筛选面值、分页、分组聚合与行选择全部由真机上的上游原版引擎驱动(见下节效果图)。

七、运行效果

7.1 真机截图

7.2 命令速查

powershell 复制代码
git clone https://atomgit.com/oh-tpc/tanstack-table-kmp.git
cd tanstack-table-kmp
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :table-core:compileKotlinOhosArm64 :table-core:compileKotlinOhosX64     # klib
.\gradlew.bat :table-core:jvmTest                                                    # 29 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco 打开 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.table -m entry
hdc shell hilog | findstr TableNapi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :table-core 真正编译成 ohosArm64 / ohosX64 产物;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. org.gradle.java.home 只管 Gradle daemon,gradlew 启动器自己也要 JAVA_HOME ------Windows 下不设环境变量,wrapper 第一行就报 JAVA_HOME is not set;
  4. 2.2.21-1.0.0 旧线必须配 Gradle 8.14.1(上游 8.10 会升一级),与 KStore 的 2.4.20 线留 9.x 是两条经验,不能互相套用;
  5. ArkTS 的 Text 组件没有 .alignItems() ------垂直居中用固定 height + lineHeight 同值实现,直接链式调用会在 CompileArkTS 报 Property 'alignItems' does not exist on type 'TextAttribute';
  6. ArkTS 接口必须把用到的响应字段全部显式声明 :漏一个(facets)就是两条编译错误------arkts-no-any-unknown 与 Property does not exist 连锁,补上类型即消;
  7. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  8. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  9. DevEco 打开工程后会生成 .clangd / .clang-tidy / .idea / .cxx,根 .gitignore 的 /build/ 盖不住嵌套目录------entry/libs/、.hvigor/、oh_modules/ 要单独追加,否则混进提交;
  10. build-profile.json5 的 signingConfigs 仓库内保持空数组,签名配置由 DevEco 勾选自动生成后回填本机,证书绝不进入 AtomGit;
  11. CAdapter 生成的 api.cpp 会有 non-void function does not return 类 clang 警告,qrose 同款,无害忽略;
  12. ArkTS 里 Record 的字段访问用 ['status'] 而不是 .status,严格模式不认动态属性名。

8.2 已知问题

  • 鸿蒙侧没有 :table-compose,UI 由 ArkTS 原生渲染;等社区 CMP 工具链与 HarmonyOS Kotlin 对齐后接回(源码留盘零改动);
  • 引擎的列可见性、列顺序、列/行固定、列宽、footer 未做 Demo 界面------桥的 state 已含这些切片,扩展只加 ArkTS 控件即可;
  • 上游 13 个示例屏取 6 个特性面做鸿蒙验收,其余依赖 Compose 交互或与已覆盖面重复;
  • createdAt 的日期格式化用纯公历算法手写(common 无 java.time,不引 kotlinx-datetime);
  • libtanstacktable.so debugShared 约 7.2MB(含 KN 运行时 + 完整引擎),无按需裁剪;
  • ohos 测试执行器未接线,*Ohos*Test* 任务禁用,引擎正确性靠 29 项 JVM 单测兜底。

8.3 未来优化方向

  • 桥协议扩展:列可见性 / 列序 / 固定 / footer 的 ArkTS 控件(state 切片已就绪);
  • 按需出 so:只链接用到的特性管线,压缩产物体积;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和 uitest 自动化截图脚本。

九、总结

9.1 核心难点回顾

text 复制代码
上游 table-core(44 文件零依赖纯 common)
    → 引擎纯度盘点(零 expect / 零协程 / 零 Compose,无需制品线验证)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
    → 根构建替换(cmp_qrose 模式,Compose 模块留盘)
    → ohosArm64 / ohosX64 动态库(引擎源码零改动)
    → 无状态桥(状态往返 + 两阶段表)+ 20 动作走引擎 accessor
    → C ABI(@CName)+ C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机六 Tab 验收(排序/筛选/分页/分组聚合/行选择)

9.2 封装层次

  • 引擎层:上游原版 Table、ColumnDef、九大特性与六级行模型管线;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理六 Tab 页面状态、动作分发、JSON 解析与视图渲染;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 引擎/UI 分层的库,适配面由分层质量决定 :TanStack v8 移植把全部数据逻辑收在零依赖的 table-core,渲染收在 table-compose------前者零改动直接编,后者整体留盘,边界一开始就是清楚的。适配前先做源码依赖度盘点(import 全量 grep),比适配中试错便宜得多;
  2. 有状态引擎跨桥,把状态当数据而不是会话:无状态桥 + 状态往返让跨语言生命周期管理归零;两阶段表(先变体捕获、后干净渲染)绕开 memo 失效细节,代价可忽略;
  3. 语义只在引擎一份:排序循环、级联选择、分组展开全部经引擎 accessor 触发,ArkTS 不重写任何一行状态机------29 项 JVM 单测钉住的语义,就是真机上跑的语义。

9.4 适配成果

当前适配已完成:

  • tanstack-table-kmp 0.1.0 真实引擎的双 ABI Kotlin/Native 编译(上游 commonMain 44 文件零重写、零新增引擎源码);
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 六 Tab 验收页面(基础、排序、筛选面值、分页、分组聚合、行选择);
  • 29 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份、24 节主文档)、本篇文章和本地效果图。

适配过程 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。

参考文档

相关推荐
沐曦股份MetaX1 小时前
沐曦股份亮相CCF第三届算力活动:重磅发布Token算力普惠计划,赋能科研新时代
人工智能·科技·ai·开源·gpu算力
贾伟康2 小时前
【共创稿事节】食材百科页的懒加载改造:LazyForEach、IDataSource 与缓存窗口的三次迭代
性能优化·harmonyos·arkts·lazyforeach·列表加载
SZYSK3 小时前
一套人员主数据,三套事务:通行、消费、人证比对如何分事件类型
物联网·算法·harmonyos
贾伟康4 小时前
【共创稿事节】一个 UserDataManager 管八类数据:Preferences 持久化的克制用法
java·服务器·前端·harmonyos·arkts·鸿蒙
万物智能信息科技4 小时前
六轴传感器Motion驱动框架开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
驱动开发·华为·开源·harmonyos·鸿蒙
分布式存储与RustFS4 小时前
在 1Panel 应用商店部署 RustFS:安装、配置与验证步骤
云原生·开源·对象存储·分布式存储·s3·rustfs·性能基准
Bigger4 小时前
开源了一个"先预览、能反悔"的文件整理工具,聊聊背后的工程细节
rust·开源·app
龙九山5 小时前
我的 10 个 npm 插件,差点被 AI 判了死刑
前端·开源
GitFun5 小时前
电脑空间总不够?实测120张挖出2.58MB重复
开源·工具