开源鸿蒙平台 CMP 三方库「LazyTable」适配全流程
本文记录
LazyTable接入 OpenHarmony 的完整过程,覆盖二维表格布局库的依赖盘点、MinaBox 硬依赖的源码内置、社区 Compose OH 分支工具链选型、ohosArm64/ohosX64目标、Kotlin/Native 动态库、ComposeArkUIViewController/ N-API 嵌入、ArkTS 宿主页面、HAP 构建和真机验收。
LazyTable是构建在 MinaBox(其又构建在 ComposeLazyLayout)之上的 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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.1.0-05(eazytec Maven,"毕昇"编译器) |
| Compose Multiplatform | OH 分支 1.9.2-1.1.0-13(runtime / ui / foundation / material3 均有 ohos 变体) |
| Skiko | 0.9.22.2-1.1.0-13(skia 自渲染后端,ohosSkikoVersion 指定) |
| JDK | 21(Temurin,org.gradle.java.home 硬编码) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 目标因 fork 无 jvm 变体移除,见 2.3) |
| Gradle | 8.14.1(上游 8.11.1 升级;2.2.21 线要求 8.x) |
| DevEco product | 5.0.5(17) 兼容,hvigor modelVersion 6.0.0 |
| DevEco Studio | 6.0.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.lazytable |
| 版本 | 1.10.0-ohos.1(上游版本-ohos.N,cmp_qrose 命名法) |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
lazytable | 8 | compose.runtime + compose.foundation + minabox(api),零平台源码 | 整体编译(双 ohos) |
minabox(上游制品) | 7 | compose.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 三屏幕对照
| 本仓屏幕 | 上游对应 | 差异 |
|---|---|---|
SimpleTableScreen | LazyTableSimpleScreen | 无(10×30 单元格) |
PinnedTableScreen | LazyTablePinnedScreen | 上游 ktor 拉 Pokémon + Coil 图片;本仓 24 行确定性离线数据,图片列改图标字符 |
StateTableScreen | LazyTableStateScreen | 无(点击 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.toml | Kotlin 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13 + export 钉版 |
gradle.properties | JDK 21、1.10.0-ohos.1、KN 开关、skia 后端、ohosSkikoVersion |
build.gradle.kts(根) | 版本号、全局禁用 Ohos*Test* 任务 |
lazytable/build.gradle.kts | ohosArm64 + 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.cpp | N-API 注册 + androidx_compose_ui_arkui_init |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 libkn.so、链接 skikobridge、双 ABI 选目录 |
harmonyApp/.../ets/pages/Index.ets | Compose 嵌入 + 生命周期转发 |
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 机制 |
| Native | MainArkUIViewController | C 导出,Compose 控制器工厂 |
| N-API | libentry.so 的 MainArkUIViewController() | 向 ArkTS 暴露控制器 |
| ArkTS | Compose({ 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.so | entry/libs/arm64-v8a/(78.9 MB) | debug 含 Compose + Skiko,属正常水位 |
libkn.so | entry/libs/x86_64/(76.0 MB) | 模拟器用 |
libkn_api.h | entry/src/main/cpp/include/{arm64-v8a,x86_64}/ | 21.7 KB,声明全部导出符号 |
HAP 侧:CompileArkTS、BuildNativeWithCmake(编 libentry.so)、PackageHap 依次通过,签名后 bm install 成功、aa start 进入前台。
6.4 功能用例
- 启动进入示例列表,三个入口按钮渲染正常(Compose 冷启动无白屏闪退);
- Simple:10×30 单元格,单指拖动 X / Y 双向滚动,格线与
(列,行)文本渲染正确; - Pinned:开启 Show header 后首行冻结,Pin name / Pin icon 后首列 / 图标列滚动时保持可见且层级盖在滚动内容之上;Show footer 开启后底部表尾固定;
- State:点击任意单元格,弹簧动画滚动至该格(
animateToCell); - 返回键从子屏幕回到列表(
onBackPressed传导); - 系统切深色模式,Demo 主题跟随(
isSystemInDarkTheme); - 旋转屏幕后滚动位置保持(
rememberSaveableLazyTableState的 Saver 生效); - 杀进程重启直接进上次状态(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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:lazytable/:minabox真正编译成ohosArm64/ohosX64KLIB、让 Demo 链出双 ABI so; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- OH 分支的 Compose 制品没有 jvm 变体:保留
jvm()做交叉验证会报No matching variant of org.jetbrains.compose.runtime:runtime——UI 库适配要提前放弃桌面交叉验证预设,改用链接 + 符号 + 真机链路; commonMain里api级依赖无 ohos 变体时(minabox),ohos 目标必然解析失败且无法绕过——源码内置是让仓库自包含的务实解;- 默认层级模板不含 ohos 分组:
ohosMain要by creating手动建,再让ohosArm64Main/ohosX64Main依赖它,否则源码集找不到; - 上游
.gitignore的/build是根锚定,盖不住minabox/build/、example/composeApp/build/——首次git add就把 klib 中间产物混进了暂存区(真实踩坑),补全局build/规则后重新暂存; - 上游 Demo 的 ktor + coil 依赖会拖出整条网络栈的 ohos 变体问题,离线化数据是最短路径;
- compose 1.7 → 1.9 把
measure(index, constraints)标为 deprecated:为守住"源码零改动"红线不跟进,等上游自己升级写法后再同步; - DevEco 打开的是
example/harmonyApp而不是仓库根目录——根目录是 Gradle 工程,DevEco 只认 hvigor 工程; abiFilters必须双 ABI:缺x86_64时模拟器安装报9568347;- ArkTS 不能直接 import
libkn.so,必须走libentry.so(N-API 胶水 +androidx_compose_ui_arkui_init)——与"绝不直接 import KN so"的社区规则一致; - 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 三条经验
- UI 库的适配路线由"库的价值是否在 UI"决定:编码器类库可以提取内核(qrose 路线),存储类库可以真编译引擎 + ArkTS 验收(KStore 路线),而布局库的价值就是 Compose 组合树本身——只有让整套 Compose 跑起来,"适配"二字才成立;路线选择先看库的形态,再看制品线;
- 依赖链上的制品可达性决定工程形态:
api级依赖没有 ohos 变体时,源码内置(vendor)比等待发布或双仓联动都更能让仓库独立复现;内置时守住"源码零改动"红线,将来换回制品就是一行依赖的事; - 没有桌面交叉验证时,四层链路是替代证据: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。
参考文档
更多推荐


所有评论(0)