本文记录 table(Data Table for Compose Multiplatform)接入 OpenHarmony 的完整过程,覆盖响应式表格库盘点、源码集手术(composeMain 隔离)、语义内核提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

table 是 Compose Multiplatform 的 Material 3 数据表格库:tableColumns DSL 声明列(表头、单元格、可排序、过滤器),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 整块存活,粘性组头由 ArkUI ListItemGroup 承担;
  • 条件格式(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 实现。Table composable 及其手势 / 布局体系留在 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin jdk-21.0.12.101-hotspot)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 wrapper 9.6.1 降级;2.2.21 线不支持 Gradle 9.x)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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-core1121111 留 commonMain,101 挪 composeMain;jvmMain 4 个 platform actual 随 expect 一起挪
table-format2666 个数据模型留,20 个对话框 UI 挪 composeMain
table-paging61模块整体移出构建(外部 pager 元数据不兼容)

留下的语义内核清单:

语义上游位置手术
FilterConstraint(14 操作符)+ isNullCheckfilter/data原样保留
TableFilterState + isActive()filter/data剥 @Immutable
NumberInputFormat + decimalSeparatorOffilter/data剥注解,Composable 工厂挪 composeMain
NumberFilterDelegate + Int/DoubleDelegate嵌在 TableFilterType 里抽成顶层纯文件(keyboardType 留 Compose 侧)
FilterConstraints(各过滤器默认操作符清单)TableFilterType 默认值新增纯对象,与上游一字不差
FilterEmission / applyEmission / resolveSourceConstraintfilter/component/maininternal → public
resolve{Text,Number,Enum,Boolean}Filter与 remember*FilterState 同文件逐字提取成独立公开文件,状态工厂留 composeMain
SortOrder + nextSortOrderTableState.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 方法覆盖内核
TableEnginetableEngine(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.tomlKotlin 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.ktdate 解析引擎(kotlinx-datetime 无 ohos 制品,暂存 JVM)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt / TableEngine.kt / MiniJson.ktC ABI、引擎、JSON
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作用
语义FilterConstraint(14 操作符)过滤约束的唯一词表
语义resolveTextFilter / resolveNumberFilter / resolveEnumFilter / resolveBooleanFilter输入 → 规范 TableFilterState(防抖 / Apply 双路径共用,issue #55)
语义FilterEmission / applyEmissionApply / Clear / Invalid 发射协议
语义NumberInputFormat / NumberFilterDelegate本地化小数位输入的解析与校验
语义nextSortOrder三态排序循环(换列回 ASCENDING 由调用方处理)
语义sortedWithinRowBlocks / filteredWholeRowBlocks块内排序不拆块 / 块粒度过滤存活
语义FilterConstraints各过滤器默认操作符清单
模型TableFormatRule / TableCellStyleConfig条件格式规则与样式(Int ARGB 颜色)
引擎TableEngine(nativeApp)数据集 / 行匹配 / 比较器 / 规则求值管线
NativeTableEngineC ABI JSON 导出
N-APItableEngine(req)向 ArkTS 暴露能力
ArkTScall(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 改为抽取后的顶层名):

测试类用例数钉住什么
RowBlockOrderingTest9块内稳定排序、块不拆散、整块过滤存活
ResolveTextFilterTest3空串清空、IS_NULL 无值应用、约束应用
ResolveNumberFilterTest10单值 / 区间、半填与倒置 Invalid、不可解析
ResolveEnumFilterTest4空选清空、IN/NOT_IN、无值约束
ResolveBooleanFilterTest3三态:null 清空、true/false 应用
SetEnumValuesTest3全选 / 反选的集合操作(issue #104)
NumberInputFormatTest9本地化小数位解析 / 格式化 / 接受判定
NumberInputErrorTest11InvalidNumber / RangeIncomplete / RangeInverted 判定
ResolveDateFilterTest(jvmTest)5date 输入解析(随引擎暂存 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 功能用例

  1. 启动后状态行出现 snapshot · 24/24 rows · sort none · 0 filters,表体渲染 24 行七列;
  2. 点 Age 表头:sort age ASCENDING + 表头 ▲;再点 ▼;第三点回 · 原顺序;换 Score 点击第一下必是 ▲(换列回升序,nextSortOrder 契约);
  3. Name / CONTAINS / son + Apply:只剩姓氏含 son 的行,chips 出现 name CONTAINS son;空输入 Apply = 清空(blank 语义);
  4. Age / BETWEEN / 30…40:只剩区间内行;改 45…40(倒置)Apply:状态行红字 number input is not a valid, complete filter…、行集不变——FilterEmission.Invalid 不发射,HAP 不崩;
  5. Dept / IN / R&D+QA:两部门行;NOT_IN 反转;全不选 Apply = 清空;
  6. Active / No:只剩 active=false 的行(叠加灰色删除线规则);
  7. Group 开:粘性组头(R&D n rows…),滚动吸顶;开分组后点 Age 排序:块不拆散,块间按各自最小成员排序、块内按年龄排(sortedWithinRowBlocks 可视化);CONTAINS tor + 分组:整块存活(filteredWholeRowBlocks);
  8. Format 开关:score≥90 绿底加粗、age>49 橙底斜体、active=false 灰色删除线,关 Format 样式全消、数据不变;
  9. 乱序操作后 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,第一步必须是逐文件依赖度盘点(grep androidx.compose / @Composable),拿精确的留 / 挪清单再动刀;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 外部依赖看元数据版本而不只是"有没有制品":paging-core 2.3.4 有 JVM 制品但元数据 2.4.0,HarmonyOS Kotlin 2.2.0 只读到 2.3.0,连 JVM 目标都编不过——table-paging 整模块移出构建;
  4. kotlinx-datetime 在社区 Maven 上没有 ohos 制品(实测 404),resolveDateFilter 的签名类型 LocalDate 拖着它——date 引擎下沉 jvmMain,制品出现当天挪回 commonMain,C 签名不变;
  5. assertk 的坐标是 com.willowtreeapps.assertk:assertk(group 带后缀),写成 com.willowtreeapps:assertk 解析失败只报 Could not find;
  6. gradlew 启动器自身需要 JAVA_HOME 环境变量,gradle.properties 的 org.gradle.java.home 只管 daemon——CI / 新终端里要显式导出;
  7. "类型定义跟 UI 文件绑在一起"是 CMP 手术通病,本仓三处:NumberFilterDelegate 嵌在 import 了 Compose 类型的 TableFilterType 里、四个 resolve 函数与 @Composable remember 工厂同文件、UiString 666 行里纯键枚举与 @Composable provider 同文件——都得手工拆;
  8. @Immutable / @Stable 等 Compose 注解留在 commonMain 会让 ohos 编译直接失败,留下的文件一律剥注解;
  9. ArkTS 严格模式四连:Record<string, T> 不接受 null 赋值(先判空)、Scroll({horizontal:true}) 要改 .scrollable(ScrollDirection.Horizontal)、build() 内禁止 const 局部声明(换辅助方法)、Record 取值要显式收窄;
  10. 手拼 JSON 的括号层级 bug 是运行期才炸的:cellStyles 被嵌进 cells 里,编译、链接、JSON.parse 全都不报错,直到真机页面读行级字段得到 undefined,抛 Cannot convert a UNDEFINED value to a JSObject(上图右)。手拼 JSON 的结构变更要同步检查读写两侧的字段层级;
  11. 命令行跑 hvigor 报 spawn java ENOENT:打包任务从 PATH 找 java,把 DevEco 自带 JBR 加进 PATH;DEVECO_SDK_HOME 也要指向 IDE 内置 sdk(all-in-one 安装);
  12. 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 三条经验

  1. UI 型 CMP 库的正确切口是语义核心而不是渲染翻译:表格库九成是 Compose UI,但被上游测试盯死的语义内核(过滤边界、排序循环、行块几何)才是多端一致性的本体。逐 composable 把渲染树翻译到 ArkUI 既不可验收也不可维护;提取语义 + 快照渲染,让 ArkTS 无状态,语义漂移无处藏身;
  2. 依赖可达性决定工具链线:table 语义内核零外部依赖,社区已审的 2.2.21 旧线直接可用(Gradle 降 8.14.1);KStore 依赖 2.4 线 kotlinx 制品必须升 2.4.20(Gradle 留 9.x)——结论相反、方法论相同,路线选择必须基于当天的制品与元数据可达性,而不是历史结论;
  3. 交叉验证是"内核没漂"的唯一可信证明:58 项 JVM 测试跑的是上游原测试文件(仅改 delegate import),函数体逐字未动;真机验收里"BETWEEN 倒置报错不崩、换列回升序、分组排序块不拆"这三条最像"鸿蒙特性"的行为,恰恰都是上游契约的复现。

9.4 适配成果

当前适配已完成:

  • table 3.1.1 语义内核的双 ABI Kotlin/Native 编译(11 + 6 个纯文件原样保留,4 个 resolve 引擎逐字提取公开,上游函数体零改动);
  • table-core / table-format 的 ohosArm64 + ohosX64 KLIB(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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐