开源鸿蒙平台 CMP 三方库「Composables UI」适配全流程
本文记录
Composables UI接入 OpenHarmony 的完整过程,覆盖组件库盘点、设计系统数值层提取、真实内核编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 五 Tab 页面、HAP 构建和设备验收。
Composables UI是 Composable Horizons 的 Compose Multiplatform 组件库:Button、Checkbox、TriStateCheckbox、Switch、RadioGroup、Slider、Progress、Tabs、TextField、AlertDialog、BottomSheet、DropdownMenu、Tooltip、Disclosure、NavigationBar、Toolbar、Scrollbars、Separations、Text、Icon,外加 Theme 令牌系统与窗口断点,以「现代、无障碍、每个组件单文件可复制」为卖点,是 Material Compose 之外的非 Material 选择。本次适配与 KStore 的"真编译整库"路线恰好相反:组件库的 25 个源文件全部是@Composable代码,依赖 Compose 1.11 / Kotlin 2.4 /composeunstyled2.8.0 二进制制品,鸿蒙工具链上无法整体编译——因此走回 cmp_qrose 的内核提取路线,但提取的不是算法,而是设计系统的数值层:26 个明暗色彩令牌、9 个形状半径、窗口断点阈值、触控-指针尺寸数学、TriState 状态机、进度条扫描公式与全部动效参数,编成ohosArm64/ohosX64KLIB;上游 Compose 源码零损耗保存在src/composeMain不参与编译,ArkUI 五 Tab Demo 按同一套数值把组件重绘出来。项目地址: AtomGit/oh-tpc/composables-ui
上游仓库: composablehorizons/ui
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Composables UI 为 Compose 应用提供一整套组件的「设计决策全集」:每个组件长什么样(令牌、形状、阴影)、多大(48/36dp 触控-指针二分法)、怎么动(强调曲线、时长、缩放系数)、怎么流转(TriState 循环、进度扫描)。KMP 项目把这些决策放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套组件语义,但上游构建(Kotlin 2.4.0 / Compose 1.11.1 / AGP 9.0.0)不会生成 OpenHarmony 变体,而社区 CMP 停在 1.9.2 / HarmonyOS Kotlin 2.2.21。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明这套设计系统在 OpenHarmony ARM64 设备上完成编译、链接和执行——两端的按钮圆角、禁用透明度、对话框缓动曲线就会各自为政。适配需要同时验证设计令牌内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 渲染边界 | 25 个源文件全部 import androidx.compose.*,@Composable 无法编进 ohos;社区 CMP 1.9.2 上也没有上游用的新 TextFieldState / Navigation 3 API。 |
| 二进制依赖 | 组件交互机制在 com.composables:composeunstyled:2.8.0 二进制制品里,没有 ohos 变体,手势物理抽不出来。 |
| 工具链不一致 | 上游 Kotlin 2.4.0 / Gradle 9.5.1 / AGP 9.0.0 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 「UI 库」的定义争议 | 渲染过不了桥,必须回答「一个纯 UI 库到底适配什么」——答案是把组件拆成「数值 + 渲染」,数值过桥,渲染重放。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:设计系统数值层的提取与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。组件语义完全由上游源码值负责,JVM 侧 19 项断言把每个数值钉死在上游源码行上,ArkTS 只负责调用 Native、解析 JSON 和按数值渲染。
1.2 库提供的能力
Demo 提供五个 Tab,对应桥接的全部入口:
- 主题(theme):明/暗双色系 26 个色彩令牌(含 ARGB 半透明预合成)、9 个形状半径、2 套阴影、涟漪/禁用透明度、排版样式,Light/Dark × Touch/Pointer 四档联动;
- 规格(spec):17 组组件尺寸——按钮/Tab/输入框/菜单项的 48/36dp 二分法(±4dp)、Switch 44×24、复选框 20dp、对勾三段归一化几何、对话框 280–560dp、抽屉把手 32×4 等;
- 行为(tristate/progress):TriState 复选框状态机(Off→On、Indeterminate→On、On→Off)与进度条
(1+0.35)t−0.35扫描偏移; - 动效(motion/transformOrigin):对话框 400/200ms 强调曲线 (0.05,0.7,0.1,1.0)/(0.3,0,0.8,0.15)、菜单 120/75ms 0.96 缩放、气泡 300/250ms、遮罩 300ms、焦点环 120ms 2dp、按压 0.98,以及弹层 side × alignment → pivot 映射;
- 断点(breakpoints):compact/medium/expanded/large/extraLarge(0/600/840/1200/1600)与 short/tall(1080)解析;
- 版本握手:
composablesVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op | 回显本次操作(theme / spec / tristate / progress / motion / breakpoints / transformOrigin)。 |
colors / shapes / shadows / alphas / typography | theme op 的五张令牌表,色彩为 #AARRGGBB 终值,ArkTS 拿到即用。 |
| 数值字段 | spec/progress/motion 各自的尺寸、时长与控制点。 |
组件渲染语义(手势物理、焦点收集)在上游二进制依赖 composeunstyled 里,不随编译进 .so(见 8.2);数值层全部来自上游源码原值,JVM 侧 19 项 jvmTest 断言交叉验证,证明 ohos .so 里执行的令牌逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 25 个 Compose 文件零损耗保存于 src/composeMain(非源码集,不编译);新增 5 个内核文件把上游数值原样提取并逐文件标注出处,不发明任何新值。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 demo/ preview/ apps/ devtools/ packages/ 留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 内核零第三方依赖(不拉 kotlinx),工具链钉 HarmonyOS Kotlin 2.2.21-1.0.0(与 qrose 同线),上游源文件零改动。 |
| UI 完整 | 五 Tab 覆盖全部桥接入口:主题联动、按钮规格、控件状态机、弹层动效、断点/规格浏览器,每个交互回显 ok <op> 状态行。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。过桥的是组件的「设计的因」(令牌/规格/时长),不是「渲染的果」;ArkTS Demo 拿同一套数值重放组件,这是当前工具链版本下纯 UI 库的可验收路径。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认"渲染不可编/数值可编"的拆分、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:内核提取 ── commonMain→composeMain 零损耗迁移、五文件数值内核(令牌/断点/规格/状态机/动效)
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 五 Tab 页面、版本握手、KN 驱动的主题/规格/动效联动
第 6 阶段:示例与验证 ── HAP、设备安装、模拟器运行、自动化截图和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
ui/ 组件库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
ui/src/commonMain/ 新增:五文件数值内核(ThemeSpecs / WindowBreakpointsSpecs /
ComponentSpecs / MotionSpecs / ComposablesCore)
ui/src/composeMain/ 上游 25 个 Compose 组件源码,零损耗保存,ohos 不编
ui/src/composeTest/ 上游 TextFieldTest,保存不编
ui/src/jvmTest/ 内核交叉验证断言(19 项)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 五 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
demo/ preview/ apps/ devtools/ … 上游工程与发布链,留盘不进构建
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :ui 与 :example:nativeApp,rootProject.name = "ohos_composables_ui"。上游的 spotless / AGP 9 / vanniktech 发布插件 / changesets npm 发布链全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 9.5.1 必须降级;2.2.21 线要求 8.x) |
| 依赖 | 内核零第三方依赖,不拉 kotlinx |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.composablesui |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 内容 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
| Compose 组件 | 23 个 components/*.kt | androidx.compose.* + composeunstyled 二进制 | 零损耗迁移 composeMain,不编译 |
| 主题 DSL | theme/Theme.kt + theme/WindowBreakpoints.kt | Compose Color/Shape/Text | 同上;数值原样提取进内核 |
| 可提取数值 | — | 纯常量与简单函数 | 新增五文件内核,编 jvm + 双 ohos |
| 上游测试 | TextFieldTest.kt | Compose + assertk | 迁 composeTest 保存,不编 |
盘点结论是本次适配的核心判断:每个 @Composable 都由「数值」和「渲染」两部分构成。数值部分——Theme.kt 里 26 个令牌的明/暗 hex、0.33f 禁用透明度、100.dp 触控圆角;Button.kt 里 48.dp ± 4 的尺寸函数;TriStateCheckbox.kt 里 Off→On、Indeterminate→On、On→Off 的循环;Progress.kt 里 (1 + 0.35f) * offset - 0.35f 的扫描公式;AlertDialog.kt 里两组 CubicBezier 控制点——全部不碰 Compose,是纯 Kotlin。渲染部分过不了桥,数值部分编进 KLIB,这就是一个纯 UI 库在鸿蒙上的「内核」。
1.4 页面边界
标题区 COMPOSABLES UI · OPENHARMONY / 版本握手行(内核 0.2.0-ohos.1 · 上游 0.2.0 · Kotlin 2.2.21-1.0.0 · 组件 24)
Tab 切换 主题 / 按钮 / 控件 / 弹层 / 断点·规格
主题 Tab Light/Dark × Touch/Pointer 四档切换,26 色令牌色板 + 形状半径 + 排版行
按钮 Tab Small/Regular/Large 三档 × 六种 ButtonStyle,spec 行回显 height/padding/radius
控件 Tab TriState 复选框(KN 状态机循环)/ Checkbox / Switch / RadioGroup / Slider / 双进度条
弹层 Tab AlertDialog(强调曲线)/ DropdownMenu(side × alignment → KN pivot)/ BottomSheet / Tooltip 长按
规格 Tab 窗口宽高滑杆 → KN 断点解析,17 组件 spec JSON 原文回显
状态行 最近一次操作结果(如 ok theme light/touch · 26 色彩令牌)
页面启动时先调 composablesVersion() 做握手,再依次拉取主题、按钮规格、动效表与复选框几何。
第 2 阶段:目标与依赖打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0,而不是 KStore 的 2.4.20 线)
KStore 升到 2.4.20-0.1.0-07 是被 coroutines 制品的 stdlib 元数据逼的。本库内核零第三方依赖——不拉 kotlinx、不拉 serialization,桥的 JSON 用手写 MiniJson——没有元数据约束,所以钉回社区最早批准的 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同线),配套两件事:Gradle Wrapper 从上游 9.5.1 降到 8.14.1(2.2.21 线不支持 Gradle 9.x,这是与 2.4.20 线经验不能互套的地方);JDK 钉 21 写进 gradle.properties。
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()
}
}
rootProject.name = "ohos_composables_ui"
include(":ui")
include(":example:nativeApp")
2.2 源码集零损耗迁移
上游 ui/src/commonMain 的 25 个 Kotlin 文件整树 git mv 到 ui/src/composeMain,TextFieldTest.kt 同步挪到 ui/src/composeTest。composeMain 不是 Kotlin 源集——ohos/jvm 编译器都看不见它,它的存在只为零损耗保存上游 UI 代码:git 把这批移动全部识别为 100% rename,上游任何改动都能干净地合并进来。
ui/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
val jvmTest by getting {
dependencies { implementation(kotlin("test")) }
}
}
}
实测:
./gradlew :ui:compileKotlinJvm :ui:compileKotlinOhosArm64 :ui:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.3 内核提取的纪律
五文件内核(约 500 行)不是翻译,是誊抄,三条纪律:
- 每个常量标注上游出处:
ThemeSpecs.kt注明theme/Theme.kt,ComponentSpecs.kt列出全部上游文件名,将来上游升级按文件对照重提; - 不用任何 JVM/JS/Compose 注解:上游的
@JvmInlinevalue class 换成普通 enum(内核非热路径),@Composable/@Stable一律不进 commonMain——HarmonyOS Kotlin 认不全这些注解,留着编译期直接失败; - 半透明色预合成 ARGB:
Color.White.copy(alpha = 0.05f)这类写法用argbWithAlpha()在内核里算成#0DFFFFFF,ArkTS 拿到即用,两端不需要各自算 alpha,也避免两端舍入不一致。
另有一个 Float 精度细节:进度条偏移 (1+w)t−w 在 Float 下中点不精确,内核改写为等价的 t + w(t−1),让 t=0/t=1 精确命中 −w/1.0,JVM 断言才能用精确相等钉死。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.composables.ui/**(数值内核,jvm + 双 ohos 编译)
↓ themeSpec / resolveBreakpoint / nextTriState / overlayTransformOrigin …
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API composablesVersion() / composablesDispatch()
↓
ArkUI Index.ets(五 Tab)
请求与响应:
请求 : {"op":"theme","scheme":"light","mode":"touch"}
{"op":"spec","component":"button","size":"regular","mode":"touch"}
{"op":"tristate","state":"off"} …(共 8 个 op)
响应 : {"ok":true,"op":"theme","scheme":"light","mode":"touch",
"colors":{"backgroundColor":"#FFF4F4F4",…26 项},
"shapes":{"buttonShape":100,…9 项},
"shadows":{"raisedShadow":{"radius":16,"color":"#FF000000","offsetX":0,"offsetY":4,"alpha":0.14}},
"alphas":{"disabledAlpha":0.33,…},"typography":{"body":{"fontSize":17,"lineHeight":24,"weight":"Normal"},…},
"colorTransitionMillis":180}
{"ok":false,"error":"unknown op: xxx"}
一个对称设计:theme 的嵌套表(colors/shadows/…)作为原生 JSON 对象嵌入响应,ArkTS 一次 JSON.parse 拿到全部令牌,不做双重转义。
3.2 op 分发
@CName("ComposablesDispatch")
fun composablesDispatch(request: String): String = try {
val json = MiniJson.parse(request)
when (val op = json.str("op")) {
"version" -> composablesVersion()
"theme" -> themeOp(json)
"breakpoints" -> breakpointsOp(json)
"spec" -> specOp(json)
"tristate" -> tristateOp(json)
"progress" -> progressOp(json)
"motion" -> motionOp()
"transformOrigin" -> transformOriginOp(json)
else -> errorJson("unknown op: $op")
}
} catch (t: Throwable) {
errorJson(t.message ?: t.toString())
}
spec 的 component 覆盖 17 种组件,每种返回自己的字段集(button 的 height/horizontalPadding、switch 的 trackWidth/thumbSize、checkbox 的对勾三段归一化坐标数组……);未知 component 走错误通道,HAP 不崩。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;非法 JSON、缺字段、不支持的 op 全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcomposables_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscomposables.so)
│ ComposablesDispatch / ComposablesVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制组件语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("ComposablesDispatch")
fun composablesDispatch(request: String): String // 8 个 op 一口进
@CName("ComposablesVersion")
fun composablesVersion(): String // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ComposablesDispatch | composablesDispatch(req) | theme / breakpoints / spec ×17 / tristate / progress / motion / transformOrigin |
ComposablesVersion | composablesVersion() | fork 版本握手(页面版本行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"composablesDispatch", nullptr, NapiComposablesDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"composablesVersion", nullptr, NapiComposablesVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscomposables_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscomposables_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoscomposables SHARED IMPORTED)
set_target_properties(ohoscomposables PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(composables_napi SHARED napi_init.cpp)
target_link_libraries(composables_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoscomposables)
OHOS_ARCH 为空或 libohoscomposables.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const composablesDispatch: (request: string) => string;
export const composablesVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 版本握手和令牌拉取
aboutToAppear(): void {
const raw = composablesVersion();
const res = JSON.parse(raw) as VersionResult;
this.versionLine = `内核 ${res.version} · 上游 ${res.upstream} · Kotlin ${res.kotlin} · 组件 ${res.components}`;
this.loadTheme();
this.loadButtonSpec();
this.loadMotion();
this.loadCheckSpec();
this.resolveBreakpoints();
this.indTimer = setInterval(() => this.tickIndeterminate(), 33);
}
不定态进度条每帧(33ms)调一次 op=progress 取 offsetFraction——扫描动画本身就是 Native 驱动的,这是「数值过桥」最直观的运行时证明。
5.2 页面交互
五个 Tab 全部由 KN 数据驱动:主题 Tab 切 Light/Dark 后整页背景、面板、按钮、色板联动,Touch/Pointer 切换改变按钮圆角(100↔10)与排版(17↔15sp);按钮 Tab 的六种 ButtonStyle 用令牌配色、KN 高度/内距/圆角布局;控件 Tab 的 TriState 每次点击都过 op=tristate 状态机,对勾按 KN 归一化坐标以 Path 绘制;弹层 Tab 的对话框用 KN 控制点构建 curves.cubicBezierCurve,菜单面板按 op=transformOrigin 返回的 pivot 缩放淡入;规格 Tab 拖宽高滑杆实时解析断点标签,17 个组件的 spec JSON 原文回显。
5.3 对比度和错误边界
每个交互后状态行回显 ok <op> …,操作可追溯;ok:false 时错误原文进状态行。Native 调用本身抛错(理论不该发生)会中断页面构建,桥侧 try/catch 已兜底。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscomposables)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/Bridge.kt @CName × 2 + MiniJson + Json writer(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 product 引用 signingConfig "default"
├── AppScope/app.json5 bundleName org.terminator.ohos.composablesui、versionName 0.2.0-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohoscomposables_api.h
├── src/main/cpp/types/libcomposables_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 五 Tab 验收页面
6.2 构建、签名和安装
./gradlew :ui:jvmTest # 19 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
命令行打包必须配两个环境变量:DEVECO_SDK_HOME 指向 DevEco SDK、JAVA_HOME 指向 DevEco 自带 JBR(系统 JDK 8 会让 PackageHap 阶段直接 spawn java ENOENT 或报不相干的错)。签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书不进入仓库。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-unsigned.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.composablesui -m entry
四、完整代码对照
4.1 整体架构
com.composables.ui/**(数值内核,提取自上游源码原值)
│ ThemeSpecs(26 令牌/形状/阴影/排版)/ WindowBreakpointsSpecs
│ ComponentSpecs(尺寸数学 + TriState + 进度公式)/ MotionSpecs(时长/曲线/pivot)
Bridge.kt + MiniJson
│ ComposablesDispatch / ComposablesVersion
libcomposables_napi.so
│ composablesDispatch / composablesVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 设备页面(五 Tab,按同一套数值重放组件)
上游 composeMain/**(25 个 @Composable 文件)零损耗保存在树里,不参与任何目标编译,等社区 compose.ui ohos 制品与上游版本对齐后接回。
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_composables_ui、上游模块移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单条钉版 |
build.gradle.kts(根) | 版本 0.2.0-ohos.1、全局禁用 Ohos*Test* 任务 |
ui/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
ui/src/commonMain/.../ThemeSpecs.kt 等 5 文件 | 数值内核(令牌/断点/规格/动效/版本) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、8 个 op 分发、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 五 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(25 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 内核 | themeSpec(scheme, mode) | 26 令牌 + 形状 + 阴影 + 透明度 + 排版,四档组合 |
| 内核 | resolveWidthBreakpoint / resolveHeightBreakpoint | 0/600/840/1200/1600 与 0/1080 断点解析 |
| 内核 | buttonHeightDp / buttonHorizontalPaddingDp | 48/36dp 二分法 ±4dp |
| 内核 | nextTriState / isTriChecked | Off→On、Indeterminate→On、On→Off |
| 内核 | ProgressSpec.indeterminateOffsetFraction | (1+w)t−w,35% 宽度段扫描 |
| 内核 | overlayTransformOrigin(side, alignment) | 弹层 pivot 分数映射 |
| Native | ComposablesDispatch / ComposablesVersion | C ABI JSON 导出 |
| N-API | composablesDispatch(req) / composablesVersion() | 向 ArkTS 暴露能力 |
| ArkTS | loadTheme() / cycleTriState() 等 | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const theme = JSON.parse(composablesDispatch(JSON.stringify({
op: 'theme', scheme: 'dark', mode: 'touch'
}))) as ThemeResult;
// theme.colors.backgroundColor === '#FF0A0A0A'
// theme.shapes.buttonShape === 100
const tri = JSON.parse(composablesDispatch(JSON.stringify({
op: 'tristate', state: 'indeterminate'
}))) as TriStateResult;
// tri.next === 'on'(上游 TriStateCheckbox 语义)
val spec = themeSpec(parseScheme(json.str("scheme")), parseMode(json.str("mode")))
val current = when (json.str("state").lowercase()) {
"on" -> TriState.On
"indeterminate" -> TriState.Indeterminate
else -> TriState.Off
}
val next = nextTriState(current)
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:提取数值内核而不是等 Compose 工具链或重写组件
社区 CMP 1.9.2 与上游 Compose 1.11 之间隔着 TextFieldState / Navigation 3 / 新 theming DSL 等 API 断层,等待对齐没有时间表;在 ArkTS 里重写 24 个组件则是 fork 设计而非适配。数值提取让「上游改令牌 → fork 重编译 → 鸿蒙侧跟着变」这条链路成立——JVM 断言红即提醒漂移,这是「适配」与「重写」的分界线。
决策 2:工具链钉回 2.2.21-1.0.0,Gradle 降 8.14.1
与 KStore 相反:内核零第三方依赖,没有 kotlinx 制品的 stdlib 元数据约束,不需要 2.4.20 线。钉回 qrose 同线的 2.2.21-1.0.0 最大化复用社区已验证经验;代价是 Gradle Wrapper 必须从 9.5.1 降到 8.14.1——两条工具链线的经验不能互相套用,这条在两个库里各自成立。
决策 3:根构建替换(cmp_qrose 模式),上游工程留盘
上游根构建(spotless / AGP 9 / changesets npm 发布链 / demo / apps / devtools)对鸿蒙没有保留价值,整个替换为极简 ohos 构建;demo/ preview/ apps/ devtools/ packages/ 留盘不进 settings.gradle.kts,它们依赖被剥离的发布链,拿来 sync 必死。
决策 4:载荷是设计决策,不是渲染结果
UI 库的桥不返回图片或组件树:返回图片会让鸿蒙侧失去无障碍、按压态与焦点响应,那才是组件库的命。桥返回「设计的因」——令牌/规格/时长/状态机,渲染由 ArkTS 按数值重放;手势物理(滑杆吸附、抽屉拖拽)在上游二进制依赖里,如实列为已知限制而不伪造。
决策 5:jvmTest 断言钉值 + 上游测试零改动保存
与 KStore「跑上游原测试」不同,组件库上游唯一的测试(TextFieldTest)测的是 Compose 行为,无法在内核上跑。交叉验证改为新写 19 项断言把每个提取值钉死在上游源码字面量上(hex、dp、毫秒、控制点、状态机循环),上游测试原样保存于 composeTest。断言即升级向导:上游改版后重跑,红了的就是漂移点。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Temurin)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与 DevEco 模拟器(x86_64,API 兼容 5.1.0(18))。
6.2 JVM 交叉验证
./gradlew :ui:jvmTest
19 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
ThemeSpecsTest | 6 | 26 令牌集合完整、明/暗 13 组 hex 原值、scrim/ring 的 ARGB alpha 字节(round(0.38×255)=97)、Touch/Pointer 形状与排版分档、两套阴影与禁用 alpha |
WindowBreakpointsSpecsTest | 3 | 阈值表逐项、599/600、839/840、1079/1080 边界行为 |
ComponentSpecsTest | 6 | 按钮 44/48/52 与 32/36/40、内距 16/20/24 与 12/16/20、各 minHeight、TriState 循环、进度条端点/中点/时长、18 组组件尺寸 |
MotionSpecsTest | 4 | 对话框 400/200/150/100ms + 0.92 + 两组控制点、菜单 120/30/75ms、气泡 300/250ms、焦点环/按压/主题过渡、transform-origin 组合抽查 |
6.3 HAP 验证
:ui:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
:ui:jvmTest: 19 tests, 0 failures
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ComposablesDispatch / ComposablesVersion 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libcomposables_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
hilog: version handshake {"ok":true,"version":"0.2.0-ohos.1","upstream":"0.2.0","kotlin":"2.2.21-1.0.0","components":24}
entry-default-unsigned.hap 约 7.1 MB,内含双 ABI 各三件套(libcomposables_napi.so / libohoscomposables.so / libc++_shared.so);KN so arm64 约 3.9MB、x86_64 约 3.6MB(strip 后约 2.0MB / 1.9MB,含 Kotlin/Native 运行时)。
6.4 功能用例
- 启动后状态行出现版本握手
内核 0.2.0-ohos.1 · 上游 0.2.0 · Kotlin 2.2.21-1.0.0 · 组件 24(来自真实 Native 调用,hilog 有对应记录); - 主题 Tab:Light/Dark 切换整页联动,
backgroundColor明#FFF4F4F4/ 暗#FF0A0A0A,scrim 带 ARGB alpha;Touch/Pointer 切换按钮圆角 100↔10、body 字号 17↔15; - 按钮 Tab:Small/Regular/Large 三档高度 44/48/52(触控)变化,spec 行回显
height=48dp · padding=20dp · radius=100dp;六种 ButtonStyle 配色来自令牌表; - 控件 Tab:TriState 点击循环,状态行回显
ok tristate off -> on checked=true;对勾三段线按归一化坐标绘制;不定态进度条 35% 宽度段 1100ms 循环扫描(每帧调op=progress); - 弹层 Tab:对话框以 0.92→1 缩放 + 强调减速曲线 400ms 进入;菜单 side/alignment 切换后
pivot=行回显 KN transformOrigin,面板按该 pivot 缩放淡入;抽屉遮罩 300ms; - 断点/规格 Tab:宽高滑杆拖动,360×780 →
compact/short,跨 600/840/1200/1600 逐档变化;17 个组件 spec JSON 原文回显; - 每步操作状态行都有
ok <op>留痕。
6.5 验证结论
设计令牌内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙模拟器上成功安装并稳定运行(修复桥接 JSON 缺陷后连续多次启动无闪退),页面以真实 Native 数据渲染了主题令牌、按钮规格、TriState 状态机、进度扫描、弹层动效与窗口断点(见下节效果图)。
七、运行效果
7.1 运行截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/composables-ui.git
cd composables-ui
export JAVA_HOME="/path/to/jdk-21"
./gradlew :ui:compileKotlinOhosArm64 :ui:compileKotlinOhosX64 # klib
./gradlew :ui:jvmTest # 19 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
export DEVECO_SDK_HOME="<DevEco SDK 路径>"
export JAVA_HOME="<DevEco 自带 JBR 路径>"
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-unsigned.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.composablesui -m entry
hdc shell hilog | grep Composables
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让数值内核真正编译成
ohosArm64/ohosX64动态库,并让页面每个数值都来自 Native; settings.gradle.kts里 eazytec 双仓必须排在最前,否则配置期直接Unresolved reference: ohosArm64;- 上游的
@JvmInline/@Immutable/@Composable注解在 HarmonyOS Kotlin 上编译期失败,内核一律用普通 enum/class; - Kotlin 类型推断陷阱:alphas 表混入 Long 颜色值会被推断成
Map<String, Number & Comparable<*>>,颜色进 colors 表、alphas 只放 Float; - Float 精度:
(1+w)t−w中点不精确,等价改写t + w(t−1)让端点精确,断言才能用精确相等; - 闪退根因是桥侧 JSON,不是 N-API:theme 响应里颜色值未加引号(
"backgroundColor":#FFF4F4F4)、嵌套对象丢失{}("colors":"backgroundColor":…),ArkTSJSON.parse抛 SyntaxError 直接闪退。faultlogger 的 jscrash 报告定位到loadTheme,再在设备上对响应做十六进制转储(charCodeAt逐字节打 hilog)拿到铁证——UI 库适配里,桥的 JSON writer 必须被完整解析验证,不能只看编译通过; - 严格 ArkTS 与标准 TS 的 API 差异一次性踩齐:
padding({horizontal, vertical})不合法(要展开 left/right/top/bottom)、Curve.EASE_OUT应为Curve.EaseOut、ScaleOptions用centerX/centerY而非pivotX/pivotY、LongPressGesture的repeat是 boolean、ScrollAttribute无maxHeight、无FontWeight.SemiBold、cubicBezierCurve返回ICurve与Curve枚举不能互赋; @State border与组件内置.border()属性撞名,严格模式报「not assignable to the same property in base type ‘CustomComponent’」——状态命名先查内置属性表;- hvigor 命令行打包需要
DEVECO_SDK_HOME指向 DevEco SDK、java用 DevEco 自带 JBR,否则 PackageHap 阶段spawn java ENOENT; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - Windows 上
hdc的设备路径会被 Git Bash 转义成C:/Program Files/Git/data/...,命令前加MSYS_NO_PATHCONV=1;snapshot_display只接受.jpeg后缀;uitest dumpLayout的控件树是{attributes:{text,bounds},children:[]}递归结构,按 text 找 bounds 中心坐标点击,不写死分辨率。
8.2 已知问题
- 鸿蒙上没有 Compose 渲染:
Button/Tabs/AlertDialog等@Composable仍在composeMain,等社区compose.uiohos 制品与上游版本对齐后接回,数值层届时降级为参考实现; - 手势物理没过桥:滑杆步进吸附、抽屉拖拽安放、滚动条滑块几何在上游二进制依赖
composeunstyled里,不在本仓源码;Demo 滑杆用 ArkUI 原生 Slider,规格(track 6dp / thumb 18dp)来自本仓; - 动效是参数级复刻不是曲线级:强调曲线控制点过桥后由 ArkTS
curves.cubicBezierCurve重建,观感接近但不保证与 Compose 逐帧一致; - 令牌表是 0.2.0 快照:上游改色板/规格后需重提内核并重跑 19 项断言——这是刻意设计,断言红即提醒漂移;
- android / js / wasmJs / apple 目标与上游 demo/apps/docs 一并留盘不进构建,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.composablesui匹配的签名。
8.3 未来优化方向
- 真机(arm64)签名验收与截图补充,形成模拟器 + 真机双矩阵;
- 把 uitest 自动化点击固化成仓库内脚本(五个 Tab 全量遍历 + 截图归档),替代手工回归;
- 评估
composeunstyled独立成仓适配,补齐滑杆吸附与抽屉安放的手势物理; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 若社区 CMP 工具链与上游版本对齐,把
composeMain接回commonMain,在ComposeArkUIViewController里直接跑组件。
九、总结
9.1 核心难点回顾
上游 composables-ui(25 个 @Composable 文件)
→ 判定"渲染不可编 / 数值可编"的拆分边界
→ commonMain→composeMain 零损耗迁移(git 识别 100% rename)
→ 五文件数值内核(令牌/断点/规格/状态机/动效)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1)
→ 根构建替换(cmp_qrose 模式)
→ ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
→ C ABI(@CName × 2)+ 8 op JSON 载荷
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析(严格模式 API 差异适配)
→ ArkUI 五 Tab 验收(主题/按钮/控件/弹层/断点规格)
→ 修复桥 JSON 两处缺陷,模拟器稳定运行 + 自动化截图
9.2 封装层次
- 内核层:提取自上游源码原值的 26 色彩令牌、9 形状、2 阴影、窗口断点、尺寸数学、TriState 状态机、进度公式与动效参数;
- Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、JSON 解析、按数值重放组件、状态行留痕;
- DevEco 层:完成 CMake、HAP、签名、安装和模拟器运行。
9.3 三条经验
- 纯 UI 库的适配面是「设计的因」而不是「渲染的果」:组件过桥传令牌/规格/时长/状态机,不传图片——返回图片会让鸿蒙侧失去无障碍、按压态与焦点响应,数值过桥才能让「上游改令牌、鸿蒙跟着变」的链路成立;
- 工具链线跟着依赖形态走:KStore 因为拉 kotlinx 制品被 stdlib 元数据逼到 2.4.20 线(Gradle 留 9.x),本库内核零依赖就钉回 2.2.21 线(Gradle 降 8.14.1)——先看依赖再定钉版,不套用上一仓的结论;
- 桥的 JSON 必须被「完整解析」验证:编译通过、符号导出都不代表响应合法;本次闪退的两处缺陷(值未引号、嵌套丢大括号)只有设备上
JSON.parse才暴露。jscrash 报告 + 响应十六进制转储是定位这类问题的最短路径,比反复猜测 N-API 编码问题快一个数量级。
9.4 适配成果
当前适配已完成:
- Composables UI
0.2.0设计系统数值层的双 ABI Kotlin/Native 编译(五文件内核,上游 25 个 Compose 源文件零损耗保存于composeMain,git 全量识别为 rename); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,双 ABI); - DevEco Stage ArkUI 五 Tab 验收页面(主题四档联动、按钮规格、TriState 状态机、弹层动效、断点/规格浏览器、版本握手、状态行留痕);
- 19 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so,约 7.1MB)、设备安装和模拟器验证(含闪退定位修复与自动化截图);
- 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)