本文记录 LazyTable 接入 OpenHarmony 的完整过程,覆盖二维表格布局库的依赖盘点、MinaBox 硬依赖的源码内置、社区 Compose OH 分支工具链选型、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、ComposeArkUIViewController / N-API 嵌入、ArkTS 宿主页面、HAP 构建和真机验收。

LazyTable 是构建在 MinaBox(其又构建在 Compose LazyLayout)之上的 Compose Multiplatform 二维懒加载表格布局库:内容组织在 column × row 平面上,dimensions 定义每列/每行尺寸,pinConfiguration 冻结左侧 N 列、顶部 N 行与表尾,LazyTableState 暴露滚动位置并支持 animateToCell / snapToCell 编程式控制。本次适配与社区早期路线(qrose 的"零依赖内核提取"、KStore 的"kotlinx 制品线真编译")有本质区别:LazyTable 的全部价值就是 Compose 布局 UI 本身,没有可提取的纯 Kotlin 内核——社区 Compose Multiplatform OH 分支(1.9.2-1.1.0-13)让完整 Compose 界面直接运行在鸿蒙上成为可能,因此库源码一行不改,二维滚动、冻结行列、动画全部是上游原件,ArkTS 页面只作为嵌入壳存在。

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

上游仓库: oleksandrbalan/lazytable

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

LazyTable 为 Compose 应用提供"二维表格"这一件事的完整答案:LazyTable(modifier, state, pinConfiguration, dimensions, contentPadding, scrollDirection) { items(...) } 一个 composable 覆盖全部用法,LazyTableItem(column, row, columnsCount, rowsCount) 声明每个条目的位置与跨度,lazyTableDimensions 工厂支持固定/按内容动态两种尺寸模式,lazyTablePinConfiguration 用三个参数描述冻结列数、冻结行数与表尾开关,rememberSaveableLazyTableState 保证旋转/重建后滚动位置不丢。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Wasm 共享。OpenHarmony 应用同样需要这套表格能力,但上游构建(Kotlin 2.1.10 / Gradle 8.11.1 / AGP 8.8.1 / Compose Multiplatform 1.7.3)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画出格子,却无法证明上游的懒加载布局引擎(LazyLayout 测量协议、可视区裁剪、手势与速度追踪、pin 的 zIndex 层级)在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证布局引擎、Compose 运行时、Skia 渲染层、Native 动态库、N-API 嵌入、ArkTS 宿主和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
依赖矩阵库对 io.github.oleksandrbalan:minabox:1.10.0 是 api 级硬依赖,而社区 Maven 上没有任何带 ohos 变体的 minabox 制品——commonMain 里声明无 ohos 变体的依赖,ohos 目标必然解析失败。
UI 框架不同库的产出物是 Compose 组合树,鸿蒙原生 UI 是 ArkUI;不能用 ArkTS 重写(等于放弃上游 API),必须让 Compose 整体跑起来。
工具链不一致上游 Kotlin 2.1.10 / CMP 1.7.3 与鸿蒙工具链(HarmonyOS Kotlin + CMP OH 分支)无法共存于一套版本目录;CMP 1.7 → 1.9 存在 API 漂移风险。
渲染管线Compose 在鸿蒙上需要渲染后端(skia 自渲染 / 统一渲染)与 ArkTS 宿主的嵌入桥。
交付链路复杂.so、CMake、N-API、compose.har、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:库与 MinaBox 的 ohos 编译(源码零改动)、根构建替换(cmp_qrose 模式)和社区 Compose 嵌入桥的落地。布局与滚动语义完全由上游原版实现负责,ArkTS 只负责承载 Compose 表面与转发生命周期。

1.2 库提供的能力

Demo 提供三个真实的使用场景,对应库的全部公共入口:

  • 基础表格(Simple):10 列 × 30 行单元格,单指拖动同时在 X / Y 两个方向滚动——LazyTable 默认 scrollDirection = BOTH;
  • 冻结表格(Pinned):11 列离线数据表,四个开关实时切换 Show header / Show footer / Pin name / Pin icon——对应 lazyTablePinConfiguration(columns, rows, footer) 与 lazyTableDimensions 的按列/按行尺寸;
  • 状态控制(State):rememberSaveableLazyTableState() 创建状态,点击任意单元格触发 state.animateToCell(column, row) 弹簧动画滚动。

配合宿主能力还可验证:返回键经 onBackPressed 传导进 Compose、深色模式经 isSystemInDarkTheme 跟随系统、旋转屏幕后滚动位置经 Saver 恢复。

1.3 实现适配

维度要求
代码复用lazytable 与 minabox 的全部 commonMain 文件原样编译,不提取、不重写、零新增 Kotlin 源文件。
平台目标根构建直接声明 ohosArm64 + ohosX64,库模块出 KLIB,example/composeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 demo / androiddemo / desktopdemo / wasmdemo / build-logic 留盘不进构建。
嵌入稳定使用社区 compose.har 桥(@cpf-kmp-cmp/compose)+ N-API 胶水 + ComposeArkUIViewController,Kotlin 与 ArkTS 之间只传递 N-API 控制器句柄,不传递对象地址。
依赖纪律MinaBox 以源码内置方式随仓编译(版本与上游逐字相同 1.10.0),Compose 从 1.7.3 换到 OH 分支 1.9.2 线仅出现 1 个 deprecation(见 8.1)。
UI 完整单 App 覆盖全部公共 API:三示例 + 开关面板 + 动画 + 状态恢复。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkTS 页面不是验收对象,只是嵌入壳。KStore 的验收面在引擎(重启后数据还在),LazyTable 的验收面就是 Compose 界面本身——表格能不能滚、pin 住不住、动画顺不顺滑,一屏见分晓。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 minabox 无 ohos 制品、确定源码内置策略
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.1.0-05 / CMP 1.9.2-1.1.0-13 钉版、jvm 变体缺失处置
第 3 阶段:Demo UI 移植  ── 三屏幕移植、数据离线化、剥离 ktor / coil 网络依赖
第 4 阶段:ohos 嵌入      ── sharedLib(baseName=kn) + export + resource cinterop + ComposeArkUIViewController
第 5 阶段:ArkTS 宿主     ── compose.har 桥、CMake N-API 胶水、双 ABI 拷贝任务
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
lazytable/                            上游表格库(进 ohos 构建,ohosArm64 + ohosX64,零源码改动)
minabox/                              上游 MinaBox 1.10.0 源码内置(7 个文件原样,零源码改动)
example/composeApp/                   Compose Demo(Gradle 侧,产物 libkn.so + libkn_api.h)
example/harmonyApp/                   DevEco Stage、CMake N-API、compose.har 和 ArkTS 壳页面
example/runscript/runOhosApp-Win.bat  Windows 一键构建 / 安装 / 启动脚本
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
demo/ androiddemo/ desktopdemo/ wasmdemo/ iosdemo/ build-logic/ config/
                                      上游遗留,留盘不进 settings

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :minabox、:lazytable 与 :example:composeApp。上游的 detekt / spotless / vanniktech 发布插件、AGP classpath、CocoaPods 配置全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.1.0-05(eazytec Maven,"毕昇"编译器)
Compose MultiplatformOH 分支 1.9.2-1.1.0-13(runtime / ui / foundation / material3 均有 ohos 变体)
Skiko0.9.22.2-1.1.0-13(skia 自渲染后端,ohosSkikoVersion 指定)
JDK21(Temurin,org.gradle.java.home 硬编码)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 目标因 fork 无 jvm 变体移除,见 2.3)
Gradle8.14.1(上游 8.11.1 升级;2.2.21 线要求 8.x)
DevEco product5.0.5(17) 兼容,hvigor modelVersion 6.0.0
DevEco Studio6.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.lazytable
版本1.10.0-ohos.1(上游版本-ohos.N,cmp_qrose 命名法)
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
lazytable8compose.runtime + compose.foundation + minabox(api),零平台源码整体编译(双 ohos)
minabox(上游制品)7compose.runtime + compose.foundation,触碰 LazyLayout / LazyLayoutItemProvider / MutableIntervalList 内部 API源码内置后整体编译
demo(上游共享 UI)多ktor + coil3 + serialization + CocoaPods留盘不进构建,Demo 由 example/composeApp 重写(离线化)

expect 声明全库 0 处——lazytable 连 androidMain 都只有一个空 manifest 占位。这比 KStore(1 处 StoreDispatcher expect)更纯粹:平行适配的理论改动面是零,唯一的硬骨头在依赖矩阵(minabox)与嵌入链路(Compose on ohos)。

1.4 页面边界
示例列表        LazyTable Demo 标题 + 三个入口按钮
Simple 屏幕     TopBar("LazyTable Simple") + 10×30 单元格表格
Pinned 屏幕     TopBar + 四开关面板(header/footer/pin name/pin icon)+ 11 列离线数据表
State 屏幕      TopBar + 10×30 表格(点击任意格 animateToCell)

三个屏幕对库 API 的调用方式与上游 Demo 逐字一致,只有数据源从网络换成本地。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有什么、缺什么:

org.jetbrains.compose:compose-gradle-plugin
  1.9.2 系 OH 线:1.9.2-0.1.0 … 1.9.2-1.2.0-03(库制品 ui/foundation/runtime/material3 同版本)
org.jetbrains.compose.export:export:<同版本>
  导出 libkn_api.h 所需的 export 制品
sh.calvin.reorderable:reorderable:3.1.0-1.0.0-02
  .module 含 ohosArm64ApiElements-published / ohosX64ApiElements-published   ← 平行适配先例
io.github.oleksandrbalan:minabox
  仅上游原版 1.0.0 … 1.10.0,无任何 ohos 变体                                 ← 硬阻塞
io.github.oleksandrbalan:lazytable
  制品不存在

三个关键结论:其一,Compose OH 分支制品线完整,"整个 Compose 界面跑在鸿蒙上"路线成立——reorderable(同为 compose.foundation 系库)已用同款方式过审;其二,minabox 是本适配唯一的硬阻塞,且它是 api 依赖会传导给所有消费者,绕不开;其三,上游用的 Compose 1.7.3 没有 OH 线,必须换到 1.9.2 线——好消息是经逐文件核对,lazytable 与 minabox 源码在 1.9.2 下仅 1 个 deprecation,无一处签名不兼容。

2.2 工具链钉版(为什么是 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13)

社区 nexus 上并存两条 OH 工具链线:1.9.2 线(Kotlin 2.2.21 系,配 Gradle 8.x)与 1.12.0 线(Kotlin 2.4.20 系,配 Gradle 9.x,KStore 适配用的就是这条的新代)。本仓选 1.9.2 线的 1.1.0 代,依据是社区官方三方库 Demo 集合(kmp-thirdlibdemo 的 reorderable-demo)的实测组合:Kotlin 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13 + skiko 0.9.22.2-1.1.0-13——Demo 壳工程(compose.har 桥、N-API 胶水、hvigor 配置)全部按这一代验证过,直接复用最稳。Gradle 从上游 8.11.1 升到 8.14.1(2.2.21 线要求),wrapper 一行改完。

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

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-releases/")
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
// dependencyResolutionManagement 同样置前,略

rootProject.name = "LazyTable"

include(":minabox")
include(":lazytable")
include(":example:composeApp")

gradle.properties 钉住工具链与渲染后端:

org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=1.10.0-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.cacheKind=none
kotlin.native.distribution.downloadFromMaven=true
kotlin.mpp.enableCInteropCommonization=true
rendererBackend=skia
ohosSkikoVersion=0.9.22.2-1.1.0-13
2.3 jvm 交叉验证目标的移除(一次真实的失败)

起初按 cmp_qrose / KStore 的经验给库模块保留 jvm() 目标做桌面交叉验证,编译期直接报:

Could not resolve org.jetbrains.compose.runtime:runtime:1.9.2-1.1.0-13.
  No matching variant ... was found. The consumer was configured to find a library
  for use during compile-time, preferably optimized for standard JVMs, as well as
  attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm' but:
  ... variant 'uikitArm64ApiElements-published' declares ... 'native' ...

原因明确:OH 分支的 Compose 库制品只发 android / ohos / uikit 变体,没有 jvm/desktop 变体。这是 UI 库适配与存储库适配的关键差异——KStore 依赖的 kotlinx 制品线 jvm + ohos 双线齐全,desktopTest 可用同一版本号;Compose OH 分支做不到。处置:全部模块只保留 ohosArm64() + ohosX64(),交叉验证责任改由"klib 编译 → 完整链接 → 符号检查 → 真机验收"四层链路承担(见第六节)。

2.4 MinaBox 源码内置(本适配最关键的决策)
方案说明结论
等社区发布 ohos minabox无时间表,阻塞审核放弃
先单独适配 minabox 仓、发 nexus,本仓依赖其坐标需要发布权限,两仓联动暂缓(见 8.3)
MinaBox 1.10.0 源码内置为本仓模块同作者、同 Apache 2.0;7 个文件全是 commonMain✅ 仓库自包含

内置方式:上游 1.10.0 tag 的 src/commonMain/kotlin/eu/wewox/minabox/ 原样拷入 minabox/,构建脚本头注释写明 vendor 来源与换回条件;lazytable 的依赖从 api(libs.minabox) 改为 api(project(":minabox"))。实测:

./gradlew :minabox:compileKotlinOhosArm64 :minabox:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :lazytable:compileKotlinOhosArm64 :lazytable:compileKotlinOhosX64 # BUILD SUCCESSFUL

全部 15 个上游源文件一个没动。唯一输出是一行 deprecation(见 8.1 第 8 条)。

第 3 阶段:Demo UI 移植

3.1 三屏幕对照
本仓屏幕上游对应差异
SimpleTableScreenLazyTableSimpleScreen无(10×30 单元格)
PinnedTableScreenLazyTablePinnedScreen上游 ktor 拉 Pokémon + Coil 图片;本仓 24 行确定性离线数据,图片列改图标字符
StateTableScreenLazyTableStateScreen无(点击 animateToCell)
3.2 数据离线化
val demoRows: List<DemoRow> = (1..24).map { index ->
    DemoRow(
        name = "Item $index",
        icon = ICONS[index % ICONS.size],
        number = numberString(index),
        heightCm = 30 + index * 7,
        weightKg = decimalString(20 + index * 13),
        stats = DemoRow.Stats(/* 确定性生成 */),
    )
}

两个细节:numberString 手写补零(不用 String.format,commonMain 下平台行为不一致);decimalString 用整数运算拼一位小数,避免浮点格式化差异。离线化只影响展示数据,对 LazyTable API 的调用方式与上游逐字一致。

3.3 主题与导航

DemoTheme 用 isSystemInDarkTheme() 切换 material3 明暗色板(真机可验证深色跟随);导航用最朴素的状态机(mutableStateOf<Example?>),不引入任何导航库——Demo 的依赖面越小,ohos 侧的解释面越小。

第 4 阶段:ohos 嵌入(技术难点)

4.1 嵌入分层
commonMain 的 App()(三示例 + 主题)
        ↓ ComposeArkUIViewController(env) { App() }
@CName("MainArkUIViewController")(libkn.so 导出,libkn_api.h 声明)
        ↓ napi_env / napi_value
napi_init.cpp(libentry.so:注册方法 + androidx_compose_ui_arkui_init)
        ↓ ArkUIViewController 句柄
Index.ets 的 Compose({ controller, libraryName: 'entry' })(compose.har 提供)
        ↓ XComponent + Skiko(skia) 自渲染
HarmonyOS 屏幕

与 KStore 的"JSON 进 JSON 出"桥不同,这里跨边界的只有一个控制器句柄与生命周期回调——UI 树整体活在 Kotlin 侧,ArkTS 不复制任何界面语义。

4.2 Kotlin 侧入口
// example/composeApp/src/ohosMain/kotlin/eu/wewox/lazytable/MainArkUIViewController.kt
@OptIn(ExperimentalNativeApi::class, ExperimentalForeignApi::class)
@CName("MainArkUIViewController")
fun MainArkUIViewController(env: napi_env): napi_value {
    initMainHandler(env)
    return ComposeArkUIViewController(env) {
        App()
    }
}

ComposeArkUIViewController 是 OH 分支在 androidx.compose.ui.window 下新增的 API,把 Compose 内容包装成 ArkUI 可嵌入控制器;initMainHandler 把 Kotlin 主线程消息循环挂到宿主。

4.3 composeApp 构建配置
listOf(ohosArm64(), ohosX64()).forEach { ohosTarget ->
    ohosTarget.binaries.sharedLib {
        baseName = "kn"
        export(libs.compose.multiplatform.export)   // 导出 libkn_api.h
        linkerOpts("-lz")
    }
    ohosTarget.compilations.getByName("main").apply {
        cinterops.create("resource") {              // OH_ResourceManager rawfile 绑定
            defFile(file("src/ohosMain/cinterop/resource.def"))
            includeDirs(file("src/ohosMain/cinterop/include"))
        }
    }
}

sourceSets {
    val ohosMain by creating { dependsOn(commonMain.get()) }   // 手动接线
    val ohosArm64Main by getting { dependsOn(ohosMain) }
    val ohosX64Main by getting { dependsOn(ohosMain) }
}

两个易错点:默认层级模板不包含 ohos 分组,ohosMain 必须 by creating 手动建并让两个目标源码集 dependsOn 它;export() 必须配合 ohosMain 里的 api(libs.compose.multiplatform.export) 依赖,否则头文件不完整。

拷贝任务按 debug / release 注册:

tasks.register<Copy>("publishDebugBinariesToHarmonyApp") {
    dependsOn("linkDebugSharedOhosArm64", "linkDebugSharedOhosX64")
    into(rootProject.file("example/harmonyApp"))
    from("build/bin/ohosArm64/debugShared/libkn_api.h") { into("entry/src/main/cpp/include/arm64-v8a/") }
    from("build/bin/ohosArm64/debugShared/libkn.so")   { into("entry/libs/arm64-v8a/") }
    // x86_64 同构,略
}
4.4 渲染后端

rendererBackend=skia 选择自渲染:XComponent 提供 Surface,Skiko 直接驱动 Skia 绘制,VSync 与帧调度在 Compose 侧闭环——对表格这类高频滚动场景最稳,也是社区 Demo 的默认选择。另一可选 fusion-renderer(统一渲染管线,与 ArkTS 原生组件深度混排)尚在建设中,本仓未启用(见 8.2)。

第 5 阶段:ArkTS 宿主

5.1 Index.ets 嵌入 Compose
import { ArkUIViewController, Compose } from '@cpf-kmp-cmp/compose';
import nativeApi from 'libentry.so';

@Entry @Component struct Index {
  private controller: ArkUIViewController | undefined = undefined;
  @State errorMessage: string = 'Native module not ready';

  aboutToAppear() {
    this.controller = nativeApi.MainArkUIViewController();   // Kotlin 侧入口
  }

  build() {
    Column() {
      if (this.controller) {
        Compose({
          controller: this.controller,
          libraryName: 'entry',
          onBackPressed: () => this.controller!.onBackPress()
        })
      } else {
        Text(this.errorMessage)
      }
    }.width('100%').height('100%')
  }

  onPageShow() { this.controller?.onPageShow(); }
  onPageHide() { this.controller?.onPageHide(); }
  onBackPressed(): boolean { return this.controller ? this.controller.onBackPress() : false; }
}

@cpf-kmp-cmp/compose 来自社区发布的 compose.har(经 oh-package.json5 的 file:./libs/compose.har 本地引用),提供 Compose 组件与控制器类型;生命周期四件套(Show/Hide/Back)逐个转发给 Kotlin 侧。

5.2 N-API 胶水与 CMake
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    androidx_compose_ui_arkui_init(env, exports);   // Compose 运行时初始化(libkn.so 导出)
    napi_property_descriptor desc[] = {
        {"MainArkUIViewController", nullptr, MainArkUIViewController, nullptr, nullptr,
         nullptr, napi_default, nullptr},
        // 其余渲染辅助导出,略
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

CMake 侧把 libkn.so 作为 IMPORTED 库引入,先链它再链 compose::skikobridge(find_package(compose),来自 compose.har),最后补 libace_napi.z.so / libhilog_ndk.z.so / librawfile.z.so / EGL / GLESv3。按 CMAKE_OHOS_ARCH_ABI 选对应目录的 so 与头文件——libkn.so 与 libkn_api.h 必须来自同一次 K/N link。

5.3 工程配置要点
  • AppScope/app.json5:bundleName org.terminator.ohos.lazytable、versionName 1.10.0-ohos.1;
  • entry/build-profile.json5:abiFilters ["arm64-v8a", "x86_64"] 双 ABI 必须齐(模拟器是 x86_64);
  • 根 build-profile.json5:compatibleSdkVersion 5.0.5(17)、nativeCompiler BiSheng、signingConfigs 留空数组(自动签名由 DevEco 回填,证书绝不入库);
  • hvigor modelVersion 6.0.0,对应 DevEco Studio 6.0.0+。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── composeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=kn) + export + cinterop + 拷贝任务
│   └── src/
│       ├── commonMain/kotlin/eu/wewox/lazytable/   App / DemoTheme / 三屏幕 / 离线数据 / TopBar
│       └── ohosMain/                         MainArkUIViewController.kt + cinterop(resource.def + 头文件)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(DevEco 自动签名回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.lazytable
    ├── entry/
    │   ├── libs/compose.har                  社区 Compose ArkTS 桥(@cpf-kmp-cmp/compose)
    │   ├── libs/{arm64-v8a,x86_64}/          拷贝任务回填的 libkn.so(gitignore)
    │   ├── src/main/cpp/                     CMakeLists.txt + napi_init.cpp + include/<abi>/libkn_api.h
    │   └── src/main/ets/pages/Index.ets      嵌入 Compose 的宿主页面
    └── hvigor/hvigor-config.json5            modelVersion 6.0.0
6.2 构建、签名和安装
./gradlew :example:composeApp:publishDebugBinariesToHarmonyApp   # 编 so 并拷入 harmonyApp

cd example/harmonyApp
ohpm install --all
node "<DevEco>/tools/hvigor/bin/hvigorw.js" --mode module -p module=entry@default \
    -p product=default -p buildMode=debug assembleHap
# 产物:entry/build/default/outputs/default/entry-default-signed.hap

签名在 DevEco Studio(打开 example/harmonyApp,不是仓库根)的 File → Project Structure → Signing Configs 勾选自动生成,仓库内 signingConfigs 保持空数组。安装启动可走 hdc,或直接用随仓的 Windows 一键脚本 example/runscript/runOhosApp-Win.bat ohosArm64 <设备ID>(内含 DevEco 版本检查、ohpm、hvigor、安装、启动全流程)。

四、完整代码对照

4.1 整体架构

eu.wewox.lazytable/**(上游表格库,原样编译)
    │ LazyTable / LazyTableScope / LazyTableItem / dimensions / pinConfiguration / state
eu.wewox.minabox/**(上游 MinaBox 1.10.0,源码内置原样编译)
    │ MinaBox / LazyLayout 测量 / 可视区裁剪 / 手势与速度追踪
androidx.compose.*(OH 分支 1.9.2-1.1.0-13)+ Skiko(skia 自渲染)
    │
libkn.so(@CName MainArkUIViewController + Compose 运行时)
    │ libkn_api.h
libentry.so(N-API 胶水:arkui_init + 方法注册)
    │ ArkUIViewController 句柄
Index.ets → Compose({ controller, libraryName }) → 屏幕

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、include 三模块
gradle/libs.versions.tomlKotlin 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13 + export 钉版
gradle.propertiesJDK 21、1.10.0-ohos.1、KN 开关、skia 后端、ohosSkikoVersion
build.gradle.kts(根)版本号、全局禁用 Ohos*Test* 任务
lazytable/build.gradle.ktsohosArm64 + ohosX64,api(project(":minabox"))
minabox/build.gradle.kts同上目标;文件头注明 vendor 来源与换回条件
example/composeApp/build.gradle.kts双 ABI sharedLib、export、cinterop、拷贝任务
composeApp/.../MainArkUIViewController.kt@CName 入口 + ComposeArkUIViewController
harmonyApp/.../cpp/napi_init.cppN-API 注册 + androidx_compose_ui_arkui_init
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 libkn.so、链接 skikobridge、双 ABI 选目录
harmonyApp/.../ets/pages/Index.etsCompose 嵌入 + 生命周期转发
README.OpenHarmony_CN.md鸿蒙适配主文档(25 节)

4.3 关键 API 对照

层次API作用
库LazyTable(modifier, state, pinConfiguration, dimensions, …)核心 composable,内部委托 MinaBox
库items(count / list, layoutInfo, …)注册条目,layoutInfo 返回 LazyTableItem(column, row, 列跨, 行跨)
库lazyTableDimensions(columnSize, rowSize)Exact / Dynamic 两种尺寸模式
库lazyTablePinConfiguration(columns, rows, footer)冻结左 N 列 / 顶 N 行 / 表尾
库rememberSaveableLazyTableState()状态 + Saver(旋转恢复)
库LazyTableState.animateToCell / snapToCell编程式滚动(动画 / 瞬移)
引擎MinaBox / MinaBoxItem二维平面布局、lockHorizontally/lockVertically 即 pin 机制
NativeMainArkUIViewControllerC 导出,Compose 控制器工厂
N-APIlibentry.so 的 MainArkUIViewController()向 ArkTS 暴露控制器
ArkTSCompose({ controller, libraryName, onBackPressed })嵌入 Compose 表面

4.4 Kotlin 与 ArkTS 的边界

// Kotlin 侧——上游用法原样,一行不改
val columns = 10
LazyTable(
    dimensions = lazyTableDimensions(48.dp, 32.dp)
) {
    items(
        count = columns * rows,
        layoutInfo = { LazyTableItem(column = it % columns, row = it / columns) }
    ) { index ->
        Text(text = "#$index")
    }
}
// ArkTS 侧——只做壳:拿控制器、嵌表面、转发生命周期
this.controller = nativeApi.MainArkUIViewController();
Compose({ controller: this.controller, libraryName: 'entry',
          onBackPressed: () => this.controller!.onBackPress() })

两者之间只传输 N-API 控制器句柄,不传输对象地址、不做 JSON 序列化——UI 库的桥比存储库轻得多,重活全在 so 里。

五、关键决策说明

决策 1:Compose 整体移植(平行适配),不做内核提取也不做 ArkUI 重写

qrose 能提取纯 Kotlin 编码器,是因为条码库的核心价值在编码不在 Painter;LazyTable 的核心价值就是布局 UI 本身,"内核"与 UI 不可分。ArkUI 重写则等于放弃上游 API,不能再叫适配。社区 CMP OH 分支让第三条路成立:源码零改动、API 原样、行为由上游负责——这是 UI 库适配的正确答案,reorderable / haze 已先行验证。

决策 2:工具链选 reorderable-demo 实测组合(1.9.2 线 1.1.0 代)

社区 nexus 两条线并存:1.9.2 线(Kotlin 2.2.21 系,Gradle 8.x)与 1.12.0 线(Kotlin 2.4.20 系,Gradle 9.x)。本仓选前者,因为 Demo 壳工程三件套(compose.har、N-API 胶水、hvigor 配置)在 1.1.0-13 代有官方 Demo 集合的完整验证记录,复用风险最小;Gradle 相应从上游 8.11.1 升到 8.14.1。

决策 3:MinaBox 源码内置,仓库自包含

api 级硬依赖 + 无 ohos 制品 + 无发布权限,三重约束下 vendor 是唯一能让仓库独立复现的解。同作者、同 Apache 2.0、7 个纯 commonMain 文件,内置成本几乎为零;将来社区发布 ohos minabox 后一行换回 Maven 依赖。

决策 4:Demo 数据离线化,剥离 ktor / coil

上游 Pinned 屏拉 Pokémon API + Coil 图片,这两个库的 ohos 变体可用性需要单独验证,会把适配面从"布局库"扩大到"网络 + 图片库"。离线化让 Demo 的依赖只剩 compose + 库本身,验收解释面最小。

决策 5:jvm 目标移除,验证链改为 klib → 链接 → 符号 → 真机

fork 的 Compose 制品没有 jvm 变体(真实报错见 2.3),KStore 式的 desktopTest 交叉验证在此不可用。替代链路:双 ABI klib 编译证明源码兼容、linkDebugShared 完整链接证明依赖闭包闭合、llvm-nm 证明入口符号导出、真机验收证明行为一致——四层下来,"没漂"的证据强度不弱于桌面单测。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 11(x64)、JDK 21(Temurin jdk-21.0.12.101-hotspot)、DevEco Studio 6.0.0+、OpenHarmony 命令行工具(hvigor / ohpm / hdc)与 HarmonyOS 真机(arm64)。

6.2 编译与链接层验证

./gradlew :minabox:compileKotlinOhosArm64 :minabox:compileKotlinOhosX64       # BUILD SUCCESSFUL
./gradlew :lazytable:compileKotlinOhosArm64 :lazytable:compileKotlinOhosX64   # BUILD SUCCESSFUL
./gradlew :example:composeApp:publishDebugBinariesToHarmonyApp                # BUILD SUCCESSFUL in 5m40s

15 个上游源文件零改动通过编译;唯一输出是 MinaBox.kt:84 的 measure(index, constraints) deprecation 警告(compose 1.7 → 1.9 标记,仍可用,处置见 8.1)。

6.3 符号层与产物验证

llvm-nm --defined-only libkn.so(arm64)
  0000000001fb359c T MainArkUIViewController
  0000000001fb694c T androidx_compose_ui_arkui_init

拷贝任务产物落位:

产物位置说明
libkn.soentry/libs/arm64-v8a/(78.9 MB)debug 含 Compose + Skiko,属正常水位
libkn.soentry/libs/x86_64/(76.0 MB)模拟器用
libkn_api.hentry/src/main/cpp/include/{arm64-v8a,x86_64}/21.7 KB,声明全部导出符号

HAP 侧:CompileArkTS、BuildNativeWithCmake(编 libentry.so)、PackageHap 依次通过,签名后 bm install 成功、aa start 进入前台。

6.4 功能用例

  1. 启动进入示例列表,三个入口按钮渲染正常(Compose 冷启动无白屏闪退);
  2. Simple:10×30 单元格,单指拖动 X / Y 双向滚动,格线与 (列,行) 文本渲染正确;
  3. Pinned:开启 Show header 后首行冻结,Pin name / Pin icon 后首列 / 图标列滚动时保持可见且层级盖在滚动内容之上;Show footer 开启后底部表尾固定;
  4. State:点击任意单元格,弹簧动画滚动至该格(animateToCell);
  5. 返回键从子屏幕回到列表(onBackPressed 传导);
  6. 系统切深色模式,Demo 主题跟随(isSystemInDarkTheme);
  7. 旋转屏幕后滚动位置保持(rememberSaveableLazyTableState 的 Saver 生效);
  8. 杀进程重启直接进上次状态(saveable 恢复)。

6.5 验证结论

布局引擎(MinaBox / LazyLayout)、Compose 运行时(OH 分支 1.9.2-1.1.0-13)、Skiko 渲染层、Native 动态库、N-API 嵌入、ArkTS 宿主和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装进入前台,三个示例屏幕的二维滚动、冻结行列与动画均为上游原件在执行(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/lazytable.git
cd lazytable
export JAVA_HOME="/path/to/jdk-21"
./gradlew :minabox:compileKotlinOhosArm64 :lazytable:compileKotlinOhosArm64     # klib
./gradlew :example:composeApp:publishDebugBinariesToHarmonyApp                  # 双 ABI libkn.so
cd example/harmonyApp && ohpm install --all
node "<DevEco>/tools/hvigor/bin/hvigorw.js" --mode module -p module=entry@default \
    -p product=default -p buildMode=debug assembleHap                            # HAP
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.lazytable
hdc shell hilog | grep Compose

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :lazytable / :minabox 真正编译成 ohosArm64 / ohosX64 KLIB、让 Demo 链出双 ABI so;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. OH 分支的 Compose 制品没有 jvm 变体:保留 jvm() 做交叉验证会报 No matching variant of org.jetbrains.compose.runtime:runtime——UI 库适配要提前放弃桌面交叉验证预设,改用链接 + 符号 + 真机链路;
  4. commonMain 里 api 级依赖无 ohos 变体时(minabox),ohos 目标必然解析失败且无法绕过——源码内置是让仓库自包含的务实解;
  5. 默认层级模板不含 ohos 分组:ohosMain 要 by creating 手动建,再让 ohosArm64Main / ohosX64Main 依赖它,否则源码集找不到;
  6. 上游 .gitignore 的 /build 是根锚定,盖不住 minabox/build/、example/composeApp/build/——首次 git add 就把 klib 中间产物混进了暂存区(真实踩坑),补全局 build/ 规则后重新暂存;
  7. 上游 Demo 的 ktor + coil 依赖会拖出整条网络栈的 ohos 变体问题,离线化数据是最短路径;
  8. compose 1.7 → 1.9 把 measure(index, constraints) 标为 deprecated:为守住"源码零改动"红线不跟进,等上游自己升级写法后再同步;
  9. DevEco 打开的是 example/harmonyApp 而不是仓库根目录——根目录是 Gradle 工程,DevEco 只认 hvigor 工程;
  10. abiFilters 必须双 ABI:缺 x86_64 时模拟器安装报 9568347;
  11. ArkTS 不能直接 import libkn.so,必须走 libentry.so(N-API 胶水 + androidx_compose_ui_arkui_init)——与"绝不直接 import KN so"的社区规则一致;
  12. debug 版 libkn.so 接近 79 MB 属正常(Compose + Skiko 全家桶),release 会显著瘦身;签名材料由 DevEco 回填 build-profile.json5,证书绝不进入 AtomGit。

8.2 已知问题

  • minabox 以源码内置形态存在,社区发布 ohos 制品后应换回 Maven 依赖;
  • 上游 android / iOS / Desktop / Wasm 目标剥离(源文件留在树里),本 fork 不发 Maven Central;
  • jvm 交叉验证目标移除(fork 无 jvm 变体),“没漂”的证据链由链接 + 符号 + 真机承担;
  • Demo 数据离线化,不含网络与图片加载演示;
  • 渲染走 skia 自渲染,fusion-renderer(与 ArkTS 深度混排)社区建设中,本仓未启用;
  • Ohos*Test* 任务全局禁用(ohos 测试执行器需要 OH 系统库,本仓不含设备端单测);
  • HAP 安装必须使用与 bundle name org.terminator.ohos.lazytable 匹配的签名。

8.3 未来优化方向

  • 把 LazyTable klib 发布到社区 nexus(保持上游坐标 io.github.oleksandrbalan:lazytable,版本按社区惯例带工具链代次后缀);
  • 推动 minabox 独立适配发布,本仓换回 Maven 依赖并删除内置模块;
  • 跟进 fusion-renderer 统一渲染管线,评估表格滚动性能与 ArkTS 混排能力;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备形态的验收矩阵、自动化截图脚本与 release so 瘦身数据。

九、总结

9.1 核心难点回顾

上游 lazytable + minabox(全部 commonMain,零 expect/actual)
    → 验证 Compose OH 制品线(1.9.2-1.1.0-13 完整、minabox 缺失)
    → 工具链对齐 Kotlin 2.2.21-1.1.0-05(Gradle 升 8.14.1)
    → 根构建替换(cmp_qrose 模式,jvm 目标移除)
    → minabox 源码内置(api 硬依赖的自包含解)
    → Demo 离线化移植(剥离 ktor / coil)
    → sharedLib(kn) + export + cinterop + ComposeArkUIViewController
    → compose.har 桥 + N-API 胶水 + ArkTS 宿主
    → 双 ABI klib / so / 符号验证 → HAP 签名安装 → 真机三屏幕验收

9.2 封装层次

  • 库层:上游原版 LazyTable、LazyTableScope、dimensions / pinConfiguration / state 全套 API;
  • 引擎层:上游原版 MinaBox——LazyLayout 测量协议、可视区裁剪、手势与速度追踪;
  • Compose 层:OH 分支 runtime / ui / foundation / material3 与 Skiko skia 自渲染;
  • Native 层:双 ABI libkn.so,@CName 导出 Compose 控制器工厂;
  • N-API 层:libentry.so 完成方法注册、运行时初始化与句柄传递;
  • ArkTS 层:Compose 组件嵌入表面、生命周期四件套转发;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. UI 库的适配路线由"库的价值是否在 UI"决定:编码器类库可以提取内核(qrose 路线),存储类库可以真编译引擎 + ArkTS 验收(KStore 路线),而布局库的价值就是 Compose 组合树本身——只有让整套 Compose 跑起来,"适配"二字才成立;路线选择先看库的形态,再看制品线;
  2. 依赖链上的制品可达性决定工程形态:api 级依赖没有 ohos 变体时,源码内置(vendor)比等待发布或双仓联动都更能让仓库独立复现;内置时守住"源码零改动"红线,将来换回制品就是一行依赖的事;
  3. 没有桌面交叉验证时,四层链路是替代证据:klib 编译(源码兼容)→ 完整链接(依赖闭包闭合)→ 符号导出(入口可达)→ 真机行为(语义一致)——UI 库的最终验收面就在屏幕上,滚得动、pin 得住、动画顺滑,一屏见分晓。

9.4 适配成果

当前适配已完成:

  • LazyTable 1.10.0 全部 commonMain 源码的双 ABI Kotlin/Native 编译(上游 8 个文件零改动,零新增源码);
  • MinaBox 1.10.0 源码内置模块的同工具链编译(上游 7 个文件零改动);
  • Compose Multiplatform OH 分支(1.9.2-1.1.0-13 + Skiko skia 自渲染)在表格布局库上的完整落地;
  • 双 ABI libkn.so(arm64 78.9 MB / x86_64 76.0 MB,debug)+ libkn_api.h + llvm-nm 符号验证(MainArkUIViewController / androidx_compose_ui_arkui_init 均为全局 T 符号);
  • DevEco Stage ArkTS 宿主(compose.har 桥 + N-API 胶水 + 生命周期转发)与三示例 Demo(离线数据);
  • HAP 构建、签名、设备安装和真机验收(二维滚动 / 冻结行列 / 动画 / 返回键 / 深色模式);
  • AtomGit 项目文档、OpenHarmony README(中英双份)、25 节适配主文档和本地效果图。

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

更多推荐