开源鸿蒙平台 CMP 三方库「table」适配全流程
本文记录
table(Data Table for Compose Multiplatform)接入 OpenHarmony 的完整过程,覆盖响应式表格库盘点、源码集手术(composeMain 隔离)、语义内核提取、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
table是 Compose Multiplatform 的 Material 3 数据表格库:tableColumnsDSL 声明列(表头、单元格、可排序、过滤器),rememberTableState管理排序 / 过滤 / 分组 / 编辑状态,行块(row blocks)提供"分组不拆散"的排序与粘性分组表头,table-format在其上叠条件格式规则模型,table-paging接外部分页器。本次适配与 KStore 的"真编译整库"路线相反,走的是社区早期路线(qrose / haze / PreCompose / voyager 的"语义内核提取"):表格库九成源码是 Compose UI(上游 Compose 1.12 / Kotlin 2.4,社区 CMP 只有 1.9.2 / HarmonyOS Kotlin 2.2.21-1.0.0),但它的持久价值是纯 Kotlin 的表格语义——14 个过滤约束操作符、四类过滤器的输入解析引擎、发射协议、三态排序循环、行块排序 / 过滤算法、条件格式规则模型——这些被逐字提取到commonMain原样编译成 OpenHarmony KLIB,Compose UI 隔离进不参与编译的composeMain,ArkUI 页面作为验收宿主。一句话:行集、行序、单元格样式全部由 Kotlin 侧的库语义决定,ArkTS 只做渲染和输入搬运——它连"算错一个顺序"的机会都没有。
项目地址: AtomGit/oh-tpc/table.git
上游仓库: White-Wind-LLC/table
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
table 为 Compose Multiplatform 应用提供"数据 ↔ 表格"这一件事的完整答案:tableColumns<Person, PersonField, Unit> 一行声明列模型,column(...) 里 header / cell / sortable() / filter(TableFilterType.TextTableFilter()) 各就各位;Table composable 配 rememberTableState 渲染出 Material 3 观感的表格;点表头在三态间循环排序,过滤面板把输入解析成规范状态,行块让"按部门分组"的表格排序不拆散块、过滤整块存活;table-format 用 TableFormatRule 描述"满足条件的行 / 格长什么样";table-paging 把外部分页器接进表体。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套表格语义,但上游构建(Kotlin 2.4.20 / Compose 1.12.0 / AGP 9.4.1 / Gradle 9.6.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以画出表格线,却无法证明过滤约束语义、行块算法在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证语义内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| UI 占九成 | Table composable、过滤面板、格式对话框、手势体系全部依赖 Compose 1.12 / Kotlin 2.4;社区 CMP 是 1.9.2,强行对齐等于再打一套 Compose 发行版。 |
| 依赖矩阵 | table-paging 依赖外部分页器 ua.wwind.paging:paging-core(Kotlin 2.4 元数据、无 ohos 变体);date 过滤引擎依赖 kotlinx-datetime(社区 Maven 上无 ohos 制品)。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 表格语义跨不了桥 | 排序三态循环、FilterEmission 发射协议是 Kotlin 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:表格语义内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。语义完全由上游原版实现负责(resolve 函数逐字提取,上游测试原样盯着),ArkTS 只负责调用 Native、解析 JSON 快照和渲染。
1.2 库提供的能力
Demo 提供一个真实的表格会话(24 行 × 7 列员工数据),对应桥接的全部入口:
- 排序(sort):点表头走
nextSortOrder三态循环(无 → 升 → 降 → 无),换列回到升序——与上游TableState.setSort同一条规则; - 文本过滤(filterText):输入先过
resolveTextFilter——空串清空、IS_NULL无值也生效、其余按约束应用; - 数字过滤(filterNumber):
resolveNumberFilter+NumberInputFormat——半填区间、倒置区间、不可解析输入一律FilterEmission.Invalid:不发射、ok:false回错、HAP 不崩,和上游"输入留在原地等用户改"同一契约; - 枚举 / 布尔过滤(filterEnum / filterBoolean):
resolveEnumFilter(空选清空、IN/NOT_IN 反转)与resolveBooleanFilter(三态); - 行块分组(group):
sortedWithinRowBlocks块内排序、块不拆散,filteredWholeRowBlocks整块存活,粘性组头由 ArkUIListItemGroup承担; - 条件格式(format):三条
TableFormatRule规则(score ≥ 90整行绿底加粗、age > 49单元格橙底斜体、active = false整行灰色删除线)开关可切。
响应是一个全量快照:
| 字段 | 作用 |
|---|---|
ok / op / error | 操作是否成功、回显本次 op、失败时带原文。 |
columns[] | 列 schema:key、类型、constraints[](来自库的 FilterConstraints,与上游各过滤器默认操作符一字不差)、枚举 options。 |
sort | 当前排序 {key, order} 或 null。 |
filters[] | 每列生效的约束与值、active 判定(上游 isActive() 语义)。 |
groups[] | 分组块:key、标题、行数、起始下标——ArkTS 只管按段渲染。 |
rows[] | 每行 cells + 行级 style + 格级 cellStyles(来自 TableCellStyleConfig 的 Int 颜色与文本样式)。 |
total / visible | 数据总量与当前可见行数。 |
引擎侧的行匹配谓词、列比较器、规则求值在 example/nativeApp(上游本来就把它们放在消费方——sample 的 PersonFilterMatcher / PersonSorter / DefaultFormatRulesProvider),JVM 侧保留 58 项测试交叉验证,证明 ohos .so 里执行的表格语义没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 语义函数逐字提取(函数体零改动),commonMain 不见一行 androidx.compose;Compose UI 105 + 20 个文件 git mv 进 composeMain 隔离,不提取、不重写、不删除。 |
| 平台目标 | 根构建声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);table-paging(外部分页器元数据不兼容)留盘不进构建。 |
| 桥接稳定 | 单个 C ABI(@CName("TableEngine"))+ UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | commonMain 零外部依赖——语义内核只吃 Kotlin 标准库,工具链得以钉在社区已审的 2.2.21-1.0.0 线上。 |
| UI 完整 | 单页覆盖全部桥接入口:三态排序表头、四类过滤控件、生效过滤 chips、行块粘性分组、条件格式开关、Reset。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
Tablecomposable 及其手势 / 布局体系留在composeMain等社区 CMP ohos 制品补齐后回接;当前 ArkTS Demo 是语义内核的验收宿主。
二、实现路线图
第 1 阶段:项目初始化 ── 逐文件依赖度盘点(112+26+6)、语义内核清单、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:源码集手术 ── composeMain 隔离、注解剥离、类型下沉、resolve 函数逐字提取公开
第 4 阶段:原生桥接 ── @CName C ABI、表格引擎、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 排序表头、四类过滤控件、粘性分组、条件格式渲染
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
table-core/src/commonMain/ 语义内核(过滤引擎、排序循环、行块算法、UiString 键)——进 ohos 构建
table-core/src/composeMain/ 上游 Compose UI(101 个 commonMain 文件 + 4 个 jvmMain actual,ohos 不编)
table-core/src/jvmMain/ DateFilterResolve(kotlinx-datetime,暂无 ohos 制品,JVM 交叉验证用)
table-format/src/commonMain/ TableFormatRule / TableCellStyleConfig / 样式与对齐枚举——进 ohos 构建
table-format/src/composeMain/ 格式对话框 UI(20 个文件,ohos 不编)
table-paging/ 依赖外部分页器(Kotlin 2.4 元数据),留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + TableEngine + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份,中文版 23 节主文档)
build-logic/ config/ docs/ scripts/ 上游工程遗留,留盘不进构建
table-sample(-android)/ 上游示例,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :table-core、:table-format 与 :example:nativeApp,rootProject.name = "table"。上游的 build-logic convention 插件(includeBuild)、foojay resolver、AGP classpath、dokka / kover / spotless / vanniktech 发布插件全部随根构建替换剥离——本 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 9.6.1 降级;2.2.21 线不支持 Gradle 9.x) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.table |
export JAVA_HOME="C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version # gradlew 启动器自身仍需要一个 JAVA_HOME,gradle.properties 里的 java.home 只管 daemon
1.3 源码依赖度盘点
逐文件 grep androidx.compose / @Composable 分类,这是整个适配的"成败手":
| 模块 | commonMain 文件 | 纯 Kotlin | 处置 |
|---|---|---|---|
table-core | 112 | 11 | 11 留 commonMain,101 挪 composeMain;jvmMain 4 个 platform actual 随 expect 一起挪 |
table-format | 26 | 6 | 6 个数据模型留,20 个对话框 UI 挪 composeMain |
table-paging | 6 | 1 | 模块整体移出构建(外部 pager 元数据不兼容) |
留下的语义内核清单:
| 语义 | 上游位置 | 手术 |
|---|---|---|
FilterConstraint(14 操作符)+ isNullCheck | filter/data | 原样保留 |
TableFilterState + isActive() | filter/data | 剥 @Immutable |
NumberInputFormat + decimalSeparatorOf | filter/data | 剥注解,Composable 工厂挪 composeMain |
NumberFilterDelegate + Int/DoubleDelegate | 嵌在 TableFilterType 里 | 抽成顶层纯文件(keyboardType 留 Compose 侧) |
FilterConstraints(各过滤器默认操作符清单) | TableFilterType 默认值 | 新增纯对象,与上游一字不差 |
FilterEmission / applyEmission / resolveSourceConstraint | filter/component/main | internal → public |
resolve{Text,Number,Enum,Boolean}Filter | 与 remember*FilterState 同文件 | 逐字提取成独立公开文件,状态工厂留 composeMain |
SortOrder + nextSortOrder | TableState.setSort 内联逻辑 | 循环规则抽成纯函数 |
RowBlockOrdering(sortedWithinRowBlocks / filteredWholeRowBlocks) | 根包 | 原样保留,上游测试盯着 |
UiString(字符串键枚举) | 666 行文件混着 StringProvider | 拆分:键留 commonMain,@Composable provider 挪 composeMain |
TableFormatRule / TableCellStyleConfig / 样式对齐枚举 | table-format/data | 剥 @Immutable(颜色本就是 Int ARGB,天然可移植) |
"类型定义跟 UI 文件绑在一起"是 CMP 库手术的通病(qrose 的 Code128Type 同款):本仓有三处——NumberFilterDelegate 嵌在 import 了 ImmutableList / KeyboardType / LocalDate 的 TableFilterType 里;四个 resolve 函数与 @Composable remember 状态工厂同文件;UiString.kt 里纯键枚举与 @Composable 接口同文件。三处都得手工拆,拆错一处 JVM 编译第一下就爆。
1.4 页面边界
标题区 CMP DATA TABLE · OPENHARMONY / 状态行(op · 可见/总行 · 排序态 · 过滤数)
开关区 Group(行块分组,粘性组头)/ Format(条件格式规则)/ Reset / Clear filters
chips 区 生效过滤逐列展示,点 ✕ 清单列
过滤面板 列选择 → 约束下拉(来自 columns[].constraints)→ 按列类型出控件:
文本 TextInput / 数字 TextInput(+BETWEEN 双输入) / 枚举多选 chips / 布尔三态 / Apply
表头 七列,点表头三态排序,箭头 ▲/▼/· 与 nextSortOrder 同步
表体 List;分组时 ListItemGroup + sticky 组头;行/格样式来自 style/cellStyles
页面启动时 aboutToAppear 立即发 {"op":"snapshot"},拿全量快照渲染。
第 2 阶段:目标与依赖打通
2.1 依赖可达性验证(本适配的成败手)
语义内核要做到零外部依赖,先验证两件事在社区 Maven 上的可达性:
kotlinx-datetime(date 过滤引擎需要)
→ https://maven.eazytec-cloud.com/.../kotlinx-datetime/0.8.0/ *-ohosArm64.klib = 404
→ 处置:resolveDateFilter 下沉 jvmMain(LocalDate 是其签名类型),制品出现当天挪回
ua.wwind.paging:paging-core:2.3.4(table-paging 需要)
→ 有 JVM 制品,但 class 元数据 2.4.0,HarmonyOS Kotlin 2.2.0 只能读到 2.3.0:
Class 'ua.wwind.paging.core.LoadState' was compiled with an incompatible
version of Kotlin. The actual metadata version is 2.4.0, compiler reads up to 2.3.0.
→ 处置:table-paging 整模块移出 settings.gradle.kts(qrose 对 qrose-core 同款处理)
两个结论:其一,元数据版本比"有没有制品"更隐蔽——paging-core 连 JVM 目标都编不过,它挡住的不只是 ohos;其二,语义内核零外部依赖是本路线成立的前提,为此 kotlinx-datetime 相关的 platform/ 整包(expect/actual 的 Platform / LocaleFormat / PointerIcons / ReducedMotion / DatePattern)一并隔离进 composeMain。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区已审的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 线)。KStore 后来升到 2.4.20 线是因为它依赖的 kotlinx ohos 制品线基于 stdlib 2.4.20 构建——工具链版本跟着依赖走。本仓 commonMain 零外部依赖,没有"依赖反推工具链"的压力,2.2.21 旧线直接可用;代价是 Gradle 必须从上游的 9.6.1 降到 8.14.1(2.2.21 线不支持 Gradle 9.x),wrapper jar 一并替换。
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 {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories { /* 同上 */ }
}
rootProject.name = "table"
// table-paging 不进构建:外部分页器 2.3.4 是 Kotlin 2.4 元数据,2.2 编译器读不了,且无 ohos 制品
include(":table-core")
include(":table-format")
include(":example:nativeApp")
2.3 编译语义核心
table-core/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"))
implementation(libs.assertk) // 注意坐标:com.willowtreeapps.assertk:assertk
}
jvmMain.dependencies {
implementation(libs.kotlinx.datetime) // 仅 DateFilterResolve 用,JVM 解析
}
}
}
table-format 同构(api(project(":table-core")))。实测:
./gradlew :table-core:compileKotlinJvm :table-format:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :...OhosX64 # BUILD SUCCESSFUL
源码集手术四板斧:隔离(Compose 文件 git mv 进 composeMain,保留上游原样、保留 git 历史);剥离(@Immutable 等 HarmonyOS Kotlin 不认的注解);下沉(被 UI 文件拖走的类型抽回 commonMain);提取(纯函数从 @Composable 宿主文件里逐字搬出并公开)。
2.4 测试的处置:上游 79 个测试文件的分流
commonTest 对 ohos 测试编译同样可见,依赖 Compose 测试框架的用例必须离开。分流三路:8 个纯逻辑测试留 commonTest(行块排序、四类 resolve、枚举全选、数字输入格式);ResolveDateFilterTest 挪 jvmTest(随 date 引擎走);其余 70 个挪 composeTest(与 composeMain 同款的不编译目录,零改动保留)。再叠一根保险丝:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:桥协议建模
3.1 JSON 契约
ua.wwind.table / ua.wwind.table.format/**(语义内核,原样编译)
↓ resolve*Filter + applyEmission / nextSortOrder / RowBlockOrdering / TableFormatRule
TableEngine.kt(有状态引擎:数据集 + 排序 + 过滤表 + 分组 + 格式开关)
↓ 全量快照 JSON
Bridge.kt(@CName("TableEngine"),扁平 JSON 进出)
↓ UTF-8 JSON
N-API tableEngine(request)
↓
ArkUI Index.ets(无状态渲染快照)
请求与响应:
请求 : {"op":"sort","key":"age"}
{"op":"filterText","key":"name","text":"son","constraint":"CONTAINS"}
{"op":"filterNumber","key":"age","text":"30","secondText":"40","constraint":"BETWEEN"}
{"op":"filterEnum","key":"dept","values":["R&D","QA"],"constraint":"IN"}
{"op":"filterBoolean","key":"active","value":false}
{"op":"clearFilter" | "clearFilters" | "group" | "format" | "reset" | "snapshot", ...}
响应 : {"ok":true,"op":"sort","columns":[...],"sort":{...},"filters":[...],
"groups":[...],"rows":[...],"total":24,"visible":18}
{"ok":false,"error":"number input is not a valid, complete filter ..."}
3.2 引擎管线:每一步都踩在库语义上
fun sortOp(key: String): String {
// 同 TableState.setSort:换列回 ASCENDING,本列走 nextSortOrder 三态循环
val order = if (sort?.key == key) nextSortOrder(sort?.order) else SortOrder.ASCENDING
sort = order?.let { SortState(key, it) }
return snapshot("sort")
}
private fun pipeline(): List<DemoRow> {
var out = rows
if (filters.isNotEmpty()) {
val predicate = { row: DemoRow -> filters.all { (k, s) -> matchesColumn(row, k, s) } }
out = if (grouped)
out.filteredWholeRowBlocks(blockOf = { it.dept.name }, predicate = predicate) // 块粒度存活
else out.filter(predicate)
}
val current = sort
if (current != null) {
out = if (grouped)
out.sortedWithinRowBlocks(blockOf = { it.dept.name }, comparator = comparatorFor(...)) // 块不拆散
else out.sortedWith(comparatorFor(...))
}
return out
}
行匹配谓词与列比较器在引擎侧实现——上游本来就把这层放在消费方(sample 的 PersonFilterMatcher / PersonSorter),库负责语义(哪些操作符、输入何时合法、块怎么动),应用负责匹配(值怎么比)。过滤输入永远先过库的 resolve 引擎再落状态表:
val emission = resolveNumberFilter(text, secondText, constraint, NumberInputFormat(DoubleDelegate))
return if (applyEmissionInto(key, emission)) snapshot("filterNumber")
else fail("number input is not a valid, complete filter ...")
3.3 错误通道
整个 @CName 入口 try/catch 包裹,Kotlin 异常永远不跨 FFI;FilterEmission.Invalid(数字半填 / 倒置 / 不可解析)不发射、转成 ok:false——这正是上游"输入留在原地、UI 报错"的契约在桥上的形态,HAP 永不崩。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libtable_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohostable.so)
│ TableEngine
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;HarmonyOS CAdapter 又不会把 @CName 业务函数挂到 ArkTS 模块上(import 'libohostable.so' 只能看到 stdlib 的 zip 类型),所以 C ABI + CMake NAPI 包装是唯一路径。
4.2 Kotlin/Native 导出函数
@CName("TableEngine")
fun tableEngine(request: String): String // 11 个 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
TableEngine | tableEngine(req) | resolve*Filter × 4 + applyEmission / nextSortOrder / RowBlockOrdering / FilterConstraints / TableFormatRule 求值管线 |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"tableEngine", nullptr, NapiTableEngine, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string(两段式取串——先探长度再填 std::vector<char>,规避 API 26 头文件的非 const 缓冲区)、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohostable_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohostable_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号)。
4.4 CMake 和类型声明
add_library(ohostable SHARED IMPORTED)
set_target_properties(ohostable PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(table_napi SHARED napi_init.cpp)
target_link_libraries(table_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohostable)
OHOS_ARCH 为空或 libohostable.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const tableEngine: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 启动握手与请求分发
aboutToAppear(): void {
this.call('{"op":"snapshot"}');
}
call(request: string): void {
const raw: string = tableEngine(request);
const rec: Record<string, Object> = JSON.parse(raw) as Record<string, Object>;
if (!Boolean(rec['ok'])) { this.errorText = String(rec['error']); return; }
this.applySnapshot(rec); // @State 刷新快照 + 状态行
}
5.2 页面交互
表体按快照的 groups[] 分段渲染:List().sticky(StickyStyle.Header) + ListItemGroup 承担粘性组头,对应上游"分组表头不随滚动走"的结构性实现。点表头调 op=sort;过滤面板从 columns[].constraints 生成约束下拉(这份清单来自库的 FilterConstraints,ArkTS 里一个字都不手抄——手抄会和 Android/iOS 过滤面板对不齐);生效过滤以 chips 展示、点 ✕ 清列。
5.3 ArkTS 严格模式边界
ArkTS 不是 TypeScript:Scroll({ horizontal: true }) 这类旧写法要换成 Scroll().scrollable(ScrollDirection.Horizontal);build() 里不允许 const 局部声明(用辅助方法取值);Record<string, TStyle> 不接受 null 赋值(先判空再放);从 Record<string, Object> 取值要显式 String() / Number() / Boolean() 收窄。ok:false 时错误原文进状态行红字,排序表头深色、chips 蓝系、破坏性操作灰色,语义对齐。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohostable)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName("TableEngine") + op 分发 + 错误通道
│ ├── TableEngine.kt 有状态引擎(数据集 / 排序 / 过滤 / 行块 / 格式规则)
│ └── MiniJson.kt 扁平 JSON(含字符串数组),继承自 cmp_qrose 审核模式
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.table、versionName 3.1.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/libohostable_api.h
├── src/main/cpp/types/libtable_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :table-core:jvmTest :table-format:jvmTest # 58 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 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.table -m entry
四、完整代码对照
4.1 整体架构
ua.wwind.table/**(语义内核,原样 / 逐字提取编译)
│ FilterConstraint / resolve*Filter / FilterEmission / nextSortOrder / RowBlockOrdering
ua.wwind.table.format/**(格式规则模型)
│ TableFormatRule / TableCellStyleConfig / 样式枚举
TableEngine.kt + MiniJson
│ 数据集 / 行匹配 / 比较器 / 规则求值(消费方职责,上游 sample 同款分工)
libtable_napi.so
│ tableEngine
Index.ets
│ JSON parse + @State 快照
▼
ArkUI 真机页面(排序 / 过滤 / 粘性分组 / 条件格式)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、table-paging 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版 + assertk / kotlinx-datetime(jvmMain 专用) |
build.gradle.kts(根) | 版本 3.1.1-ohos.1、全局禁用 Ohos*Test* 任务 |
table-core/build.gradle.kts / table-format/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,commonMain 零依赖 |
table-core/.../filter/data/NumberFilterDelegate.kt | 自 TableFilterType 抽出的顶层纯接口 + Int/Double 实现 |
table-core/.../filter/data/FilterConstraints.kt | 各过滤器默认操作符清单(镜像上游默认值,多端一致锚) |
table-core/.../*FilterResolve.kt × 4 | 逐字提取并公开的输入解析引擎 |
table-core/.../data/SortOrder.kt | 追加 nextSortOrder(自 TableState.setSort 抽出的三态循环) |
table-core/src/jvmMain/.../DateFilterResolve.kt | date 解析引擎(kotlinx-datetime 无 ohos 制品,暂存 JVM) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt / TableEngine.kt / MiniJson.kt | C ABI、引擎、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 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(23 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 语义 | FilterConstraint(14 操作符) | 过滤约束的唯一词表 |
| 语义 | resolveTextFilter / resolveNumberFilter / resolveEnumFilter / resolveBooleanFilter | 输入 → 规范 TableFilterState(防抖 / Apply 双路径共用,issue #55) |
| 语义 | FilterEmission / applyEmission | Apply / Clear / Invalid 发射协议 |
| 语义 | NumberInputFormat / NumberFilterDelegate | 本地化小数位输入的解析与校验 |
| 语义 | nextSortOrder | 三态排序循环(换列回 ASCENDING 由调用方处理) |
| 语义 | sortedWithinRowBlocks / filteredWholeRowBlocks | 块内排序不拆块 / 块粒度过滤存活 |
| 语义 | FilterConstraints | 各过滤器默认操作符清单 |
| 模型 | TableFormatRule / TableCellStyleConfig | 条件格式规则与样式(Int ARGB 颜色) |
| 引擎 | TableEngine(nativeApp) | 数据集 / 行匹配 / 比较器 / 规则求值管线 |
| Native | TableEngine | C ABI JSON 导出 |
| N-API | tableEngine(req) | 向 ArkTS 暴露能力 |
| ArkTS | call(op) | 触发操作并刷新快照 |
4.4 ArkTS 与 Kotlin 的边界
const raw: string = tableEngine(JSON.stringify({ op: 'sort', key: 'age' }));
const res = JSON.parse(raw);
// res.ok === true;res.rows 已按 age 排好,res.sort = { key:'age', order:'ASCENDING' }
// ArkTS 不排序、不过滤——顺序就是语义,动了就和上游对不齐
val order = if (sort?.key == key) nextSortOrder(sort?.order) else SortOrder.ASCENDING
val out = if (grouped)
visible.sortedWithinRowBlocks(blockOf = { it.dept.name }, comparator = comparatorFor(key, order))
else visible.sortedWith(comparatorFor(key, order))
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:语义内核提取而不是真编译整库
表格库与存储库的适配面正好相反:KStore 九成是纯引擎,ohos kotlinx 制品线补齐后真编译整库是正确答案;table 九成是 Compose UI,值钱的恰是那一成纯语义。硬把 Table composable 编进 ohos 需要对齐两代 Compose;只抄 ArkTS 表格控件则丢掉多端一致性。提取语义内核 + 隔离 Compose UI,让"过滤边界、排序循环、行块几何"这些被上游 79 个测试盯着的部分原样进 so,渲染交给 ArkUI 原生组件(表格是结构化 UI,白拿 ArkUI 的无障碍、字体缩放、滚动惯性,不需要位图光栅化)。
决策 2:工具钉 2.2.21-1.0.0,Gradle 降 8.14.1
commonMain 零外部依赖,没有"依赖元数据反推工具链"的压力,社区已审的 2.2.21 旧线直接可用;代价是 Gradle 从 9.6.1 降到 8.14.1、wrapper jar 替换。与 KStore 的 2.4.20-留-9.x 结论相反,但方法论相同:工具链版本跟着依赖走,路线选择基于当天的依赖可达性。
决策 3:根构建替换(cmp_qrose 模式),table-paging 留盘
上游根构建(build-logic convention、AGP 9.4、dokka / kover / spotless、发布链)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。table-paging 依赖的外部分页器是 Kotlin 2.4 元数据且无 ohos 制品——模块整体移出 include,源码集拆分仍然做好(commonMain 只留 PagedBodyState 归约器),pager 出兼容版当天加回 include 就能编。
决策 4:resolve 函数逐字提取并公开,约束清单来自库
四个 resolve 函数的函数体是从 *FilterState.kt 逐字复制出来的(一行不改),从 internal 提为 public 供跨模块调用;FilterConstraints 镜像上游各过滤器的默认操作符,ArkTS 的约束下拉只认这份清单。语义有唯一出处,多端一致性才有锚——JVM 侧原版测试继续盯着同一份函数体。
决策 5:快照全量返回 + jvmTest 交叉验证
每个 op 都返回全量快照(rows / groups / styles / state),ArkTS 无状态渲染、ForEach 按 row.id diff。这保证 ArkTS 永远不可能"自己算错"一个顺序——它根本不算。58 项 JVM 测试(上游原测试文件)是"内核没漂"的闸门:行块排序稳定性、四类 resolve 的边界(空串清空、IS_NULL 无值应用、BETWEEN 倒置 Invalid、枚举空选清空、布尔三态)全绿,鸿蒙侧跑的是同一份逻辑的 Native 编译。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Temurin)、DevEco Studio 26、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :table-core:jvmTest :table-format:jvmTest
58 项用例全部通过、0 失败(上游原测试文件,零逻辑改动;仅 delegate import 改为抽取后的顶层名):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
RowBlockOrderingTest | 9 | 块内稳定排序、块不拆散、整块过滤存活 |
ResolveTextFilterTest | 3 | 空串清空、IS_NULL 无值应用、约束应用 |
ResolveNumberFilterTest | 10 | 单值 / 区间、半填与倒置 Invalid、不可解析 |
ResolveEnumFilterTest | 4 | 空选清空、IN/NOT_IN、无值约束 |
ResolveBooleanFilterTest | 3 | 三态:null 清空、true/false 应用 |
SetEnumValuesTest | 3 | 全选 / 反选的集合操作(issue #104) |
NumberInputFormatTest | 9 | 本地化小数位解析 / 格式化 / 接受判定 |
NumberInputErrorTest | 11 | InvalidNumber / RangeIncomplete / RangeInverted 判定 |
ResolveDateFilterTest(jvmTest) | 5 | date 输入解析(随引擎暂存 JVM) |
InfraSmokeTest(table-format) | 1 | 测试源集连通 |
6.3 HAP 验证
:table-core:compileKotlinJvm / :table-format:compileKotlinJvm: BUILD SUCCESSFUL
:table-core:jvmTest / :table-format:jvmTest: 58 tests, 0 failures
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: TableEngine / libohostable_symbols 均为全局 T 符号
BuildNativeWithCmake + BuildNativeWithNinja: finished(双 ABI 编出 libtable_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 6.8 MB,内含双 ABI 各三件套(libtable_napi.so 约 65 KB / libohostable.so strip 后 arm64 约 2.12 MB、x86_64 约 2.08 MB / libc++_shared.so 约 1.3 MB;debugShared 未 strip 的 KN so 分别约 4.03 MB / 3.72 MB,含 Kotlin/Native 运行时)。
6.4 功能用例
- 启动后状态行出现
snapshot · 24/24 rows · sort none · 0 filters,表体渲染 24 行七列; - 点 Age 表头:
sort age ASCENDING+ 表头 ▲;再点 ▼;第三点回·原顺序;换 Score 点击第一下必是 ▲(换列回升序,nextSortOrder契约); - Name / CONTAINS /
son+ Apply:只剩姓氏含 son 的行,chips 出现name CONTAINS son;空输入 Apply = 清空(blank 语义); - Age / BETWEEN / 30…40:只剩区间内行;改 45…40(倒置)Apply:状态行红字
number input is not a valid, complete filter…、行集不变——FilterEmission.Invalid不发射,HAP 不崩; - Dept / IN / R&D+QA:两部门行;NOT_IN 反转;全不选 Apply = 清空;
- Active / No:只剩
active=false的行(叠加灰色删除线规则); - Group 开:粘性组头(R&D n rows…),滚动吸顶;开分组后点 Age 排序:块不拆散,块间按各自最小成员排序、块内按年龄排(
sortedWithinRowBlocks可视化);CONTAINStor+ 分组:整块存活(filteredWholeRowBlocks); - Format 开关:score≥90 绿底加粗、age>49 橙底斜体、active=false 灰色删除线,关 Format 样式全消、数据不变;
- 乱序操作后 Reset:回到 24 行无排序无过滤无分组。
6.5 验证结论
语义内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的表格会话:三态排序、四类过滤(含 Invalid 输入的错误通道)、行块粘性分组与条件格式(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/table.git
cd table
export JAVA_HOME="/path/to/jdk-21"
./gradlew :table-core:compileKotlinJvm :table-format:compileKotlinJvm # 语义内核 JVM 编译
./gradlew :table-core:jvmTest :table-format:jvmTest # 58 项交叉验证
./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.table -m entry
hdc shell hilog | grep TableNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,第一步必须是逐文件依赖度盘点(grep
androidx.compose/@Composable),拿精确的留 / 挪清单再动刀; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 外部依赖看元数据版本而不只是"有没有制品":
paging-core 2.3.4有 JVM 制品但元数据 2.4.0,HarmonyOS Kotlin 2.2.0 只读到 2.3.0,连 JVM 目标都编不过——table-paging整模块移出构建; kotlinx-datetime在社区 Maven 上没有 ohos 制品(实测 404),resolveDateFilter的签名类型LocalDate拖着它——date 引擎下沉 jvmMain,制品出现当天挪回 commonMain,C 签名不变;- assertk 的坐标是
com.willowtreeapps.assertk:assertk(group 带后缀),写成com.willowtreeapps:assertk解析失败只报 Could not find; gradlew启动器自身需要JAVA_HOME环境变量,gradle.properties的org.gradle.java.home只管 daemon——CI / 新终端里要显式导出;- "类型定义跟 UI 文件绑在一起"是 CMP 手术通病,本仓三处:
NumberFilterDelegate嵌在 import 了 Compose 类型的TableFilterType里、四个 resolve 函数与 @Composable remember 工厂同文件、UiString666 行里纯键枚举与 @Composable provider 同文件——都得手工拆; @Immutable/@Stable等 Compose 注解留在 commonMain 会让 ohos 编译直接失败,留下的文件一律剥注解;- ArkTS 严格模式四连:
Record<string, T>不接受 null 赋值(先判空)、Scroll({horizontal:true})要改.scrollable(ScrollDirection.Horizontal)、build()内禁止const局部声明(换辅助方法)、Record 取值要显式收窄; - 手拼 JSON 的括号层级 bug 是运行期才炸的:
cellStyles被嵌进cells里,编译、链接、JSON.parse全都不报错,直到真机页面读行级字段得到 undefined,抛Cannot convert a UNDEFINED value to a JSObject(上图右)。手拼 JSON 的结构变更要同步检查读写两侧的字段层级; - 命令行跑 hvigor 报
spawn java ENOENT:打包任务从 PATH 找 java,把 DevEco 自带 JBR 加进 PATH;DEVECO_SDK_HOME也要指向 IDE 内置 sdk(all-in-one 安装); - DevEco 预览器不支持 native 模块(
libtable_napi.so加载不了,页面直接报错)——带 NAPI 的工程只能真机 / 模拟器验收;HAP 缺当前 ABI 的 so 报安装失败9568347,每个 ABI 三件套缺一不可;KN 返回字符串必须DisposeString归还运行时。
8.2 已知问题
- HarmonyOS 上没有 Compose
Table:Table/ColumnSpec/ 过滤面板 / 格式对话框 / 列拖拽固定调宽手势都在composeMain(不编译、不删除,等社区 CMP ohos 制品回接);纯几何部分(computeReorderMove/resizeScrollPullback)已在 commonMain 被 JVM 测试覆盖; - date 过滤引擎暂在 jvmMain(kotlinx-datetime 无 ohos 制品),Demo 的 joined 列只排序不过滤;
table-paging未接入(外部 pager 元数据不兼容),分页演示等 pager 出兼容版本;- 列状态类(
TableColumnsState/PinnedColumnState)依赖 Compose runtime(mutableStateOf/ScrollState),未桥接; UiString键集在 commonMain 但StringProvider/DefaultStrings是 @Composable,鸿蒙侧文案由 ArkTS 自带;- 业务数据接入需扩展协议(
op:load或 MiniJson 支持对象数组)——当前 Demo 数据集在引擎内写死以保证单 so 自包含; - HAP 安装必须使用与 bundle name
org.terminator.ohos.table匹配的签名。
8.3 未来优化方向
- 增加
op:load数据注入入口,把 Demo 引擎变成业务数据驱动; - kotlinx-datetime / paging-core 的 ohos 制品出现后,date 过滤挪回 commonMain、table-paging 加回构建;
- 社区 CMP 1.12 ohos 制品出现后,
composeMain接回commonMain,桥降级为 fallback; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)验收矩阵和 uitest 自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 table-core + table-format(112 + 26 个 commonMain 文件)
→ 逐文件依赖度盘点(纯 Kotlin 仅 11 + 6 个)
→ 源码集手术(composeMain 隔离 / 注解剥离 / 类型下沉 / resolve 逐字提取公开)
→ 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 + Gradle 8.14.1)
→ ohosArm64 / ohosX64 KLIB(commonMain 零外部依赖)
→ C ABI(@CName("TableEngine"))+ 全量快照 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机单页验收(三态排序 / 四类过滤 / 行块粘性分组 / 条件格式)
9.2 封装层次
- 语义层:上游原版
FilterConstraint、四类 resolve 引擎、FilterEmission协议、nextSortOrder、RowBlockOrdering; - 模型层:上游原版
TableFormatRule/TableCellStyleConfig/ 样式与对齐枚举; - 引擎层(nativeApp):数据集、行匹配谓词、列比较器、规则求值管线(消费方职责,上游 sample 同款分工);
- Native 层:双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:参数检查、两段式字符串转换和 Native 内存释放;
- ArkTS 层:快照解析、@State 渲染、操作分发与错误文本;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 型 CMP 库的正确切口是语义核心而不是渲染翻译:表格库九成是 Compose UI,但被上游测试盯死的语义内核(过滤边界、排序循环、行块几何)才是多端一致性的本体。逐 composable 把渲染树翻译到 ArkUI 既不可验收也不可维护;提取语义 + 快照渲染,让 ArkTS 无状态,语义漂移无处藏身;
- 依赖可达性决定工具链线:table 语义内核零外部依赖,社区已审的 2.2.21 旧线直接可用(Gradle 降 8.14.1);KStore 依赖 2.4 线 kotlinx 制品必须升 2.4.20(Gradle 留 9.x)——结论相反、方法论相同,路线选择必须基于当天的制品与元数据可达性,而不是历史结论;
- 交叉验证是"内核没漂"的唯一可信证明:58 项 JVM 测试跑的是上游原测试文件(仅改 delegate import),函数体逐字未动;真机验收里"BETWEEN 倒置报错不崩、换列回升序、分组排序块不拆"这三条最像"鸿蒙特性"的行为,恰恰都是上游契约的复现。
9.4 适配成果
当前适配已完成:
table3.1.1 语义内核的双 ABI Kotlin/Native 编译(11 + 6 个纯文件原样保留,4 个 resolve 引擎逐字提取公开,上游函数体零改动);table-core/table-format的ohosArm64+ohosX64KLIB(commonMain 零androidx.compose、零外部依赖);- 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 单页验收页面(三态排序、四类过滤、行块粘性分组、条件格式、错误通道);
- 58 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(双 ABI 六件套 so,约 6.8 MB)、签名、设备安装和真机验证;
- 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。
参考文档
更多推荐

所有评论(0)