开源鸿蒙平台 CMP 三方库「Vetra UI」适配全流程
本文记录
Vetra UI接入 OpenHarmony 的完整过程,覆盖设计系统库盘点、鸿蒙 Compose 制品线选型、整库真实编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
Vetra UI是基于 Compose Multiplatform 的现代设计系统 / 组件库:37 槽位语义化色彩(明 / 暗双预设)、14 级字型、8 级圆角、8 级阴影与明暗 ShadowConfig 构成主题引擎,在此之上提供按钮(5 变体)、图标按钮、分段控件、输入框、开关 / 复选 / 单选 / 滑杆、卡片(5 变体)、Chip、Badge、分割线、顶部栏 / 导航栏 / Tab、对话框 / 菜单、通知(Notify + Host)、Loading 家族、骨架屏与下拉刷新。本次适配与社区早期路线(qrose / Shapes / snowfall 的"零依赖内核提取")有本质区别:这是一个 100% Compose 语义的设计系统,没有可隔离的纯 Kotlin 内核,而社区 Maven 上的 HarmonyOS Compose 制品线(1.12.0-0.1.0-07,material3 / animation / ui / ui-tooling-preview 均带ohosArm64/ohosX64klib)已经齐备——因此上游core/commonMain全部 29 个文件(含全部@Preview函数)零改动原样编译成 OpenHarmony KLIB,37 色板 × 明暗、14 级字型、8 级阴影在设计令牌桥上以运行时真值读到真机,ArkUI 四 Tab 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/vetra-ui.git
上游仓库: flyfishxu/vetra-ui
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
Vetra UI 为 Kotlin Multiplatform 应用提供"设计令牌 + 组件面"的完整设计系统:VetraTheme(darkMode, colors, typography, shapes, shadows) 经 CompositionLocal 下发主题;VetraColorScheme 用语义命名组织色彩(brand / accent 两组含禁用态、canvas 三级、文本四级、边框三级、success / warning / danger / info 语义色含 on 色、overlay / shadow 工具色),明暗双预设直接可用;VetraTypography 14 级(display / heading / body / label,主二比率字号阶);VetraShadows 8 级海拔配明暗 ShadowConfig(ambientAlpha / spotAlpha / elevationMultiplier);自研 VetraPressIndication 按压反馈(明黑罩 / 暗白罩,alpha 0.1,spring 动画)替代 Material ripple。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要一套现成的语义化设计系统,但上游构建(Kotlin 2.2.21 / CMP 1.9.2 / AGP 8.13)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以画出按钮,却无法证明这个设计系统的主题引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证主题引擎、Compose 运行时栈、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 依赖矩阵 | 引擎依赖 compose runtime / foundation / ui / material3 / animation 与 material-icons,前五者要有 ohos klib,icons 需要另行处置。 |
| 无内核可抽 | 全部 29 个文件都是 Compose 语义(@Composable 组件 + CompositionLocal 主题),第一档"抽内核"路线会把库搬空。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 主题语义跨不了桥 | VetraTheme 靠 CompositionLocal 组合传播,OHOS 没有 composition host,Composable 无法上屏,只能搬"值"不能搬"组件"。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:整库的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 设计令牌桥接层。令牌值完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和用真值重绘界面。
1.2 库提供的能力
Demo 提供一个真实的设计系统会话,对应桥接的全部入口:
- 版本握手(version):
vetraVersion()返回 upstream / fork / Kotlin / Compose / repo 五项,来自真实 Native 调用而非静态文字; - 主题令牌(theme):
mode=light|dark一次取回该模式全套令牌——37 个颜色槽位(#AARRGGBB)、14 级字型(size / lineHeight / tracking / weight)、8 级圆角阶梯、7 档阴影海拔与 ShadowConfig 三参数、6 档生效海拔(elevation × multiplier); - 通知映射(notify):
type=info|success|warning|danger返回 bg / fg / accent / icon 四元组,颜色值来自真 scheme; - 按压规格(press):返回按压覆盖层颜色 / alpha / 动画规格,并在 Native 侧真实实例化
VetraPressIndication类(instantiated=true)。
响应包含:
| 字段 | 作用 |
|---|---|
ok / op | 操作是否成功与回显,失败时 error 带原文。 |
c.<token> | 37 个颜色槽位(如 c.brand = "#FF3B82F6",dark 模式真值)。 |
ty.<style>.<field> | 14 级字型的字号 / 行高 / 字距 / 字重(int + 名称)。 |
shape.<level> / shadow.<level> / shadowCfg.* | 圆角阶梯 / 海拔阶梯 / ShadowConfig。 |
bg / fg / accent / icon | notify op 的四元组。 |
instantiated / overlayColor / overlayAlpha | press op 的实例证明与覆盖层规格。 |
引擎侧完整能力(全部组件、主题、按压指示、@Preview)都在上游 commonMain 里原样随编译进入 .so,JVM 侧保留同一份源码对上游制品线(compose 1.9.2 / material3 1.9.0 / icons 1.7.3)的编译交叉验证,证明 ohos .so 里执行的令牌引擎没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | core 的全部 commonMain 文件(29 个,含 @Preview)原样编译,不提取、不重写、零改动;全仓不新增任何库侧源文件,缺口用独立 icons-shim 依赖模块补。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);composeApp / iosApp / 文档站留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | ohos 线换仓库不换语义(社区 Compose 1.12.0-0.1.0-07),jvm 线保持上游版本主体;上游声明但零引用的 lifecycle 依赖直接剔除。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:主题令牌画廊、组件画廊、反馈(Notify / Loading / Skeleton / Dialog)、链路与日志。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 目前没有社区 Compose 线的 composition host,Composable 无法进入 ArkUI 树;ArkTS Demo 是"令牌消费者"——每一个渲染色值都来自桥返回的真库值,视觉按令牌重绘。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose ohos 制品线、确定"整库编译"路线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、icons-shim 补位
第 3 阶段:桥协议建模 ── 设计令牌 JSON 契约(37 色 × 明暗 / 14 字型 / 阴影)、真实与转录边界、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 版本握手、明暗切换、四 Tab 令牌驱动页面、桥日志
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
core/ 上游设计系统(进 ohos 构建,jvm + ohosArm64 + ohosX64,commonMain 零改动)
icons-shim/ 新增:24 个 material 图标向量(1.7.3 源码逐字,依赖模块)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.md / README.upstream.md 社区首页(重写)与上游原文保存
composeApp/ iosApp/ docs/ mkdocs.yml 上游示例 App、iOS 壳与文档站,留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :core、:icons-shim 与 :example:nativeApp,rootProject.name = "ohos_vetra_ui"。上游的 AGP / compose 插件 / 热重载 / vanniktech 发布配置全部随根构建替换剥离(各自保留 .upstream 备份)——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 9.6.1(上游 wrapper 9.2.0 升级;与同路线 JetCo / eungabi 一致) |
| Compose(ohos 线) | 1.12.0-0.1.0-07:runtime / foundation / ui / material3 / animation / components-ui-tooling-preview |
| Compose(jvm 线) | compose 1.9.2 / material3 1.9.0 / icons 1.7.3(上游版本主体) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.vetraui |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
core | 29(约 1.17 万行,含 @Preview) | compose runtime / foundation / ui / material3 / animation + icons,零平台源码集 | 整体编译(jvm + 双 ohos) |
icons-shim | 25(新增) | 纯 androidx.compose.material.icons 声明复刻 | 依赖模块供 ohos 编译 |
composeApp | 多平台示例 | AGP / 热重载 / js + wasm 工具链 | 留盘不进构建 |
expect 声明全库 零处——core 是 100% commonMain 的纯 UI 库,没有任何平台接缝,这正是"整库编译"路线成立的先决条件。依赖盘点的一个关键发现:上游 catalog 声明了 lifecycle-viewmodel-compose / lifecycle-runtime-compose,但全源码 零 import——直接从构建剔除,不为无用依赖背制品线风险。唯一的硬缺口是 material-icons:社区线没有任何 ohos 变体(icons-core / extended 都没有),而 commonMain 连同 @Preview 共引用 24 个向量(13 filled + 8 outlined + 3 automirrored)——处置见第 2.3 节。
1.4 页面边界
标题区 VETRA UI · OPENHARMONY / 明暗 Toggle / fork·Kotlin·Compose 版本行 / bridge ok · N ops 状态行
Tab 切换 主题 / 组件 / 反馈 / 链路
主题 Tab 37 色板(名称 + hex 真值)、14 级字型标本、8 级圆角阶梯、6 档阴影阶梯 + ShadowConfig 参数
组件 Tab 按钮 5 变体(按压覆盖层)、分段控件、输入框(focus 描边)、开关/复选/单选/滑杆、
卡片/Chip/Badge/分割线、Tab 行 / 导航栏
反馈 Tab Notify 四类型(bg/fg/accent 真值)、Loading 家族、骨架屏、Dialog 预览
链路 Tab 版本握手、六层调用链、press 规格、真实与复刻边界、桥日志(上限 40 条)
页面启动时先调 vetraVersion() 做握手,再经 vetraDispatch 依次取 light / dark 两套主题、press 规格与四类 notify 令牌,全部就绪后才进入四 Tab。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 Compose 的 ohos 制品。用 maven-metadata.xml + .module 变体逐个验证:
org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07 ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07 ✓
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07 ✓
org.jetbrains.compose.material3:material3:1.12.0-0.1.0-07 ✓(设计系统的核心依赖)
org.jetbrains.compose.animation:animation:1.12.0-0.1.0-07 ✓
org.jetbrains.compose.components:components-ui-tooling-preview:1.12.0-0.1.0-07 ✓(@Preview 注解)
org.jetbrains.compose.material:material-icons-core / -extended ✗ 无任何 ohos 变体
三个关键结论:其一,设计系统依赖的 Compose 六件套 ohos klib 齐备,"整库编译"路线成立(对比 qrose 时代上游是 Compose 1.12 快照接不上社区 1.9.2,只能抽内核——本次上游恰是 Kotlin 2.2.21 + CMP 1.9.2 的保守写法,没有用到社区线没有的 API);其二,ohos 线不发布 *-desktop 子制品,jvm 交叉验证目标必须另立上游线(“换线不换版”);其三,Preview 注解的正确坐标是 org.jetbrains.compose.components:components-ui-tooling-preview——手写 org.jetbrains.compose.ui:ui-tooling-preview 会在 jvm 上解析到只含 androidx.compose.desktop 包名的 desktop 变体、在 ohos klib 里也只有 androidx.compose.ui.tooling.preview 包,唯独没有上游源码 import 的 org.jetbrains.compose.ui.tooling.preview.Preview。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
社区 Compose 线 1.12.0-0.1.0-07 基于 Kotlin 2.4.20-0.1.0-07 构建(klib 元数据 2.4),旧钉法 2.2.21-1.0.0 读不了 2.4 元数据。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07、Gradle 升 9.6.1(与同路线的 JetCo / eungabi / compose-ripple-indication 一致),上游源码零改动兼容。
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_vetra_ui"
include(":core")
include(":icons-shim")
include(":example:nativeApp")
2.3 icons-shim:24 个图标的来龙去脉
material-icons 在社区线上没有 ohos 制品,而本适配选择"含 @Preview 整文件编译",24 个向量全部要能解析。处置分四步(JetCo 同款):
- 从 Maven Central 下载
material-icons-core:1.7.3-sources.jar与material-icons-extended:1.7.3-sources.jar; - 逐字取出
Icons.kt(Icons对象 +materialIcon/materialPath构建器)与 24 个向量文件,路径数据一字不改、Apache-2.0 版权头保留; - 放进独立模块
icons-shim/src/commonMain/kotlin/androidx/compose/material/icons/...,模块只挂ohosArm64()/ohosX64(); core的ohosMain以api(project(":icons-shim"))引入。
为什么是依赖模块而不是源码集:源码集里的声明对同一目标的 commonMain 编译不可见(JetCo 注释里踩过),KMP 目标编译的 classpath 是"common 依赖 ∪ 该目标中间源集依赖",依赖模块天然满足。
2.4 编译真实库
core/build.gradle.kts 收敛为纯 KMP 双线模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.compose.compiler)
}
kotlin {
applyDefaultHierarchyTemplate()
jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } // 交叉验证线
ohosArm64() // the adaptation
ohosX64()
sourceSets {
ohosMain.dependencies { // 社区 Compose 线
api(libs.compose.runtime)
api(libs.compose.foundation)
api(libs.compose.ui)
api(libs.compose.material3)
api(libs.compose.animation)
api(libs.compose.ui.tooling.preview)
api(project(":icons-shim"))
}
jvmMain.dependencies { // 上游线(Maven Central)
api(libs.compose.runtime.jvm) // 1.9.2
api(libs.compose.material3.jvm) // 1.9.0(material3 版本线独立!)
api(libs.icons.extended.jvm) // 1.7.3
/* ... */
}
}
}
实测:
./gradlew :core:compileKotlinJvm :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64
# BUILD SUCCESSFUL —— 29 个文件(含全部 @Preview)零 error,仅 Preview 注解 deprecation 警告
两个版本线的坑要记录:material3 的版本线独立于 compose 主体(1.9.x 稳定线止于 1.9.0,上游 CMP 1.9.2 插件映射的就是 material3 1.9.0——可用上游 core-jvm 发布件的 .module 对账验证);测试任务统一 JVM 跑 + 根构建全局禁用 Ohos*Test* 任务(KN ohos 测试运行器不可用)。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.flyfishxu.vetraui.core/**(上游设计系统,原样编译)
↓ VetraLight/DarkColorScheme / DefaultVetraTypography / DefaultVetraShadows / ShadowConfig
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API vetraVersion() / vetraDispatch()
↓
ArkUI Index.ets(四 Tab)
请求与响应:
请求 : {"op":"theme","mode":"dark"}
{"op":"notify","type":"warning","mode":"light"}
{"op":"press","mode":"dark"}
响应 : {"ok":"true","op":"theme","c.brand":"#FF3B82F6","ty.headingMd.size":"18",
"shadow.md":"4","shadowCfg.multiplier":"1.2","shadowEff.xl":"19.2", ...}
{"ok":"false","op":"...","error":"unsupported op"}
设计令牌一次取整套(37 色 + 14 字型 + 阶梯 + 配置约 90 个键值),页面缓存 light / dark 两份,明暗切换零桥调用、即时换肤——这对应上游 VetraTheme(darkMode = ...) 的语义。
3.2 载荷为什么是"运行时真值"
桥内取值这样写:
private fun schemeFor(mode: String): VetraColorScheme? = when (mode) {
"light" -> VetraLightColorScheme // 上游单例,运行时对象
"dark" -> VetraDarkColorScheme
else -> null
}
for ((name, color) in scheme.tokens()) { // 37 槽位逐个读
fields.add("c.$name" to hex(color))
}
with(DefaultVetraTypography) { /* 14 级 TextStyle 逐字段读 */ }
with(DefaultVetraShadows) { /* 7 档 Dp 读 */ }
关键在真实性边界:37 色 × 明暗、14 级字型、阴影阶梯与 ShadowConfig 是运行时从真库对象读的(改上游色值,Demo 即变);shape 阶梯半径(Shape 对象不可内省)、notify 类型→令牌映射规则(内联在 @Composable 体内)、press 覆盖层规格是转录自上游源码并文档化的;组件像素由 ArkTS 按令牌重绘。这条"真实与复刻"边界在 README 与 Demo 链路 Tab 里双向可见。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("VetraDispatch")
fun vetraDispatch(request: String): String = try {
when (MiniJson.parse(request).str("op")) { ... }
} catch (t: Throwable) {
err("dispatch", t.message ?: t.toString()) // {"ok":"false","error":...}
}
未知 op / mode / type、非法 JSON 全部走同一个 ok:false 通道,ArkTS 侧统一渲染进链路 Tab 的桥日志。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libvetra_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosvetra.so)
│ VetraDispatch / VetraVersion
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
@CName("VetraVersion")
fun vetraVersion(): String // 版本握手
@CName("VetraDispatch")
fun vetraDispatch(request: String): String // version / theme / notify / press 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
VetraDispatch | vetraDispatch(req) | 全部 4 个 op 的令牌读取 |
VetraVersion | vetraVersion() | fork 版本握手(真机版本行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"vetraDispatch", nullptr, NapiVetraDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"vetraVersion", nullptr, NapiVetraVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosvetra_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosvetra_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号)。字符串读取用两段式 napi_get_value_string_utf8(先量长度再开 std::vector<char>),规避 API 26 非 const 缓冲区的坑。
4.4 CMake 和类型声明
add_library(ohosvetra SHARED IMPORTED)
set_target_properties(ohosvetra PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(vetra_napi SHARED napi_init.cpp)
target_link_libraries(vetra_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosvetra)
OHOS_ARCH 为空或 libohosvetra.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const vetraVersion: () => string;
export const vetraDispatch: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 令牌加载和明暗切换
aboutToAppear(): void {
this.versionInfo = JSON.parse(vetraVersion()); // 版本握手
this.themeLight = this.dispatch('theme', { 'mode': 'light' }); // 37 色 + 字型 + 阴影
this.themeDark = this.dispatch('theme', { 'mode': 'dark' });
this.pressSpec = this.dispatch('press', { 'mode': 'light' });
/* notify ×4 ... */
this.ready = true;
}
private c(name: string): string { // 每一个渲染色值都从这里出
return (this.dark ? this.themeDark : this.themeLight)['c.' + name] || '#FF000000';
}
明暗 Toggle 只翻 dark 布尔,全页色值经 c() 即时切换——对应上游 VetraTheme(darkMode)。
5.2 页面交互
四 Tab 各自的验收面:主题 Tab 用 31% 宽色板格展示 37 槽位(名称 + hex 真值),14 级字型标本按桥值渲染字号 / 字重 / 行高 / 字距,圆角与阴影阶梯逐档对比;组件 Tab 的按钮五变体全部令牌取色,按住出现 press op 的 0.1 alpha 覆盖层(animateTo 渐入渐出),TextField 聚焦描边切 borderFocus;反馈 Tab 的四类 Notify 横幅用 notify op 的 bg / fg / accent 真值;链路 Tab 滚动显示每次 dispatch 的 op → ok/ERROR (耗时ms) 日志与"真实与复刻"边界清单。
5.3 对比度和错误边界
深色画布(c.canvas)配近白主文本(c.textPrimary),语义色(success 绿 / warning 琥珀 / danger 红)直接来自上游配好的对比组合;ok:false 时错误原文进桥日志,statusLine 显示累计 ops 数。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosvetra)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/vetra/nativeapp/
│ └── Bridge.kt @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.vetraui、versionName 1.0.0-alpha03-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkReleaseShared* 经 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/libohosvetra_api.h
├── src/main/cpp/types/libvetra_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :core:compileKotlinJvm :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64
./gradlew :example:nativeApp:linkReleaseSharedOhosArm64 :example:nativeApp:linkReleaseSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库。
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.vetraui -m entry
四、完整代码对照
4.1 整体架构
com.flyfishxu.vetraui.core/**(上游设计系统原样编译)
│ VetraColorScheme(37 槽) / VetraTypography(14 级) / VetraShadows + ShadowConfig
│ VetraPressIndication / 24 个组件文件(含 @Preview)
androidx.compose.material.icons(icons-shim 复刻,仅 ohos 编译可见)
│
Bridge.kt + MiniJson
│ VetraDispatch / VetraVersion(theme / notify / press / version)
libvetra_napi.so
│ vetraDispatch / vetraVersion
Index.ets
│ JSON parse + @State 令牌缓存
▼
ArkUI 真机页面(四 Tab,全令牌驱动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_vetra_ui、composeApp/iosApp 移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + 社区 Compose 线(1.12.0-0.1.0-07)+ 上游 jvm 线双线钉版 |
build.gradle.kts(根) | 版本 1.0.0-alpha03-ohos.1、全局禁用 Ohos*Test* 任务 |
core/build.gradle.kts | 纯 KMP 双线:jvm() + ohosArm64 + ohosX64,依赖按目标线声明 |
icons-shim/**(25 文件) | Icons.kt + 24 向量,1.7.3 源码逐字、Apache-2.0 头保留 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填(Release) |
nativeApp/.../Bridge.kt | 2 个 C ABI、令牌序列化(toArgb)、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 验收页面(GalleryCard / VetraButtonFace 自组件) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(21 节,含桥协议契约与真实与复刻边界) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | VetraLightColorScheme / VetraDarkColorScheme | 37 槽位明 / 暗预设(桥运行时直读) |
| 引擎 | DefaultVetraTypography | 14 级字型(size / lineHeight / tracking / weight) |
| 引擎 | DefaultVetraShadows + Light/DarkModeShadowConfig | 8 级海拔 + 明暗阴影配置 |
| 引擎 | VetraPressIndication | 按压覆盖层指示类(press op 真实实例化) |
| 引擎 | VetraTheme / VetraButton 等 24 个组件文件 | CompositionLocal 主题与全部 Composable(随编译进 .so,不跨桥) |
| Native | VetraDispatch / VetraVersion | C ABI JSON 导出 |
| N-API | vetraDispatch(req) / vetraVersion() | 向 ArkTS 暴露能力 |
| ArkTS | c(name) / tyNum(style, field) | 令牌访问器,全页唯一取色通道 |
4.4 ArkTS 与 Kotlin 的边界
const theme = JSON.parse(vetraDispatch(JSON.stringify({ op: 'theme', mode: 'dark' })));
// theme.ok === 'true',theme['c.brand'] === '#FF3B82F6'(上游 VetraDarkColorScheme.brand 真值)
// theme['ty.headingMd.size'] === '18',theme['shadowCfg.multiplier'] === '1.2'
private fun hex(color: Color): String =
"#" + color.toArgb().toUInt().toString(16).uppercase().padStart(8, '0')
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或 CompositionLocal。
五、关键决策说明
决策 1:真编译整库而不是提取零依赖内核
设计系统的全部价值就在 Compose 语义里(组件 + CompositionLocal 主题 + Modifier.Node 按压反馈),抽走 Composable 剩下的只有几十个数据类——等于把库搬空。社区 Compose 线的 material3 / animation / ui / ui-tooling-preview ohos klib 齐备后,“整文件原样编译”(compose-ripple-indication / eungabi / JetCo 已过审的同款路线)是唯一不折损交付物的选择;上游恰好是 Kotlin 2.2.21 + CMP 1.9.2 的保守写法,整库编译零 API 障碍。
决策 2:jvm() 双线交叉验证(换线不换版)
同一份 commonMain 对上游线(compose 1.9.2 / material3 1.9.0 / icons 1.7.3)编译通过,证明源码没有对社区线的隐藏假设。依赖按目标线声明在 ohosMain / jvmMain 而不是 commonMain——KMP 目标编译的 classpath 是两者的并集,commonMain 保持线无关。
决策 3:icons 走独立依赖模块而不是源码集
源码集声明对同目标 commonMain 编译不可见;依赖模块经 api(project(":icons-shim")) 对 ohos 编译可见,jvm 线继续解析 Maven Central 真 icons-extended 1.7.3——两条线永不同框。向量路径逐字取自 1.7.3 官方源码(版权头保留),不是手绘复刻。
决策 4:令牌桥 + 真实与复刻边界显式化
OHOS 没有 composition host,能跨 C ABI 的只有"值"。桥的货物是设计令牌全集:颜色 / 字型 / 阴影是运行时真值,shape 半径 / notify 映射 / press 规格是文档化转录,像素是 ArkTS 重绘——三层边界写进 README、代码注释和 Demo 链路 Tab,避免"看起来在跑 Compose"的误导。
决策 5:Release so 交付 + konanc 内存钉版
16 GB 开发机上开着 DevEco / 模拟器时,debug 链接(-g)固定死在 KN 的 DWARF 生成路径(LLVM DIInsertDeclaration,Internal Error 0x20474343);Release 无调试信息直接绕开且小 10 MB。konanc 钉 kotlin.native.jvmArgs=-Xmx3g -XX:TieredStopAtLevel=1——更大的堆反而挤占 LLVM 原生内存(注意属性名是大写 A 的 jvmArgs)。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :core:compileKotlinJvm
# BUILD SUCCESSFUL —— 29 个文件对上游线(compose 1.9.2 / material3 1.9.0 / icons 1.7.3)编译通过
上游无自带测试文件(commonTest 空),交叉验证落在编译层:同一份 commonMain(含全部 @Preview)同时通过上游线与社区线两套制品的编译,即为"源码线无关、内核没漂"的证据。KN ohos 测试运行器不可用,根构建全局禁用 Ohos*Test* 任务。
6.3 HAP 验证
:core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkReleaseSharedOhosArm64 / linkReleaseSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: VetraVersion / VetraDispatch / libohosvetra_symbols 均为全局 T 符号(双 ABI 各 3/3)
BuildNativeWithCmake: finished(双 ABI 编出 libvetra_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
libohosvetra.so Release arm64 约 18.7 MB、x86_64 约 18.3 MB(含 Kotlin/Native 运行时与整条社区 Compose 线;debug 变体约 28.9 MB / 27.5 MB,调试需要时可自行链接)。
6.4 功能用例
- 启动后标题区出现
fork 1.0.0-alpha03-ohos.1/Kotlin 2.4.20-0.1.0-07/Compose 1.12.0-0.1.0-07(来自vetraVersion()真握手,非静态文字),状态行bridge ok · 8 ops; - 主题 Tab 的 37 色板显示真值:dark 模式
brand #3B82F6、accent #8B5CF6——与上游VetraDarkColorScheme源码逐字一致; - 右上角 Light/Dark Toggle 翻转,全页(含其他 Tab)令牌即时换套,零额外桥调用;
- 14 级字型标本按桥值渲染(
labelLg14sp Bold、displayXl48sp / 行高 56 / 字距 -0.5); - 组件 Tab 按钮五变体令牌取色,按住出现 0.1 alpha 覆盖层、松开消退(press op 规格);
- TextField 聚焦描边切换为
borderFocus令牌; - 反馈 Tab 四类 Notify 横幅的 bg / fg / accent 与
notifyop 返回一致; - 链路 Tab 桥日志每条
ok,press 区instantiated=true(真库类在设备上实例化)。
6.5 验证结论
设计系统引擎、社区 Compose 运行时栈、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四 Tab 页面以运行时真值展示完整设计令牌会话:版本握手、明暗双主题切换、组件画廊、反馈族与桥日志(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/vetra-ui.git
cd vetra-ui
export JAVA_HOME="/path/to/jdk-21"
./gradlew :core:compileKotlinJvm :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64 # 双线编译
./gradlew :example:nativeApp:linkReleaseSharedOhosArm64 :example:nativeApp:linkReleaseSharedOhosX64
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.vetraui -m entry
hdc shell hilog | grep Vetra
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让
:core真正编译成ohosArm64/ohosX64动态库——设计系统库尤其如此,令牌真值是唯一能自证的交付物; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- material3 的版本线独立于 compose 主体:jvm 线找不到
material3:1.9.2(不存在),必须钉1.9.0——可用上游发布件的.module依赖表对账确认; - Preview 注解坐标是
org.jetbrains.compose.components:components-ui-tooling-preview,手写org.jetbrains.compose.ui:ui-tooling-preview会解析到别的包名(desktop 变体里是androidx.compose.desktop...,ohos klib 里是androidx.compose.ui.tooling.preview),唯独没有上游 import 的org.jetbrains.compose.ui.tooling.preview.Preview; - material-icons 无任何 ohos 制品:shim 必须做成独立依赖模块(源码集声明对 commonMain 同目标编译不可见),向量路径逐字取自 1.7.3 源码而不是手绘;
- konanc 的 JVM 参数属性名是大写 A 的
kotlin.native.jvmArgs(小写jvmargs静默失效);堆大小按机器实测调——开着 DevEco / 模拟器时 3g 反而比 5g 稳(大堆挤占 LLVM 原生 arena,Internal Error 0x20474343/ malloc failed); - debug 链接在内存紧张机器上固定崩在 KN 的 DWARF 生成路径(
DIInsertDeclaration),Demo 改用linkReleaseShared*交付(无-g直接绕开,还小 10 MB); Color.value在 64 位 ULong 里把 ARGB 编码在高 32 位(低 32 位是色彩空间标记),直接toString(16)得到 16 位十六进制(#FF3B82F600000000),ArkTS 按非法 ResourceColor 渲染成透明——全页"没有颜色"的根因;必须color.toArgb().toUInt().toString(16);- ArkTS 的 @Builder 方法不接受函数参数(
'content()' does not meet UI component syntax),也不能对 builder 调用结果链.onClick()(返回 void)——内容槽改@Component + @BuilderParam(尾随闭包),交互改回调属性; - ArkTS 尾随闭包的收尾是单独的
},写成})会报一串';' expected(全文件括号配平检查可定位);自组件 prop 不能撞 ArkUI 内置属性名(borderColor是 CommonAttribute 方法名,改名strokeColor);.scale()必须传{x, y}对象不能传数字; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();entry/libs/与*.so必须进 gitignore(构建产物由copyKnSo*回填)。
8.2 已知问题
- OHOS 没有社区 Compose 线的 composition host:Composable 编译链接加载三层都验证通过,但不上屏,组件视觉为 ArkTS 按令牌重绘;
- shape 阶梯半径、notify 类型→令牌映射规则、press 覆盖层规格是文档化转录(
Shape不可内省、映射内联在@Composable体内),上游改动需人工同步; - Demo 的 Loading / Skeleton / Notify 动效曲线是 ArkTS 近似,不是上游 animation spec 的逐帧复刻;图标用 unicode 字形近似(向量本体已编译进 .so,ArkTS 侧不消费 ImageVector);
PullToRefresh/ 手势类组件未在 Demo 单独验收(withFrameNanos语义无头环境不可驱动);- 上游
composeApp/iosApp/ 文档站目标剥离(源文件留在树里),本 fork 不发 Maven Central; - HAP 安装必须使用与 bundle name
org.terminator.ohos.vetraui匹配的签名。
8.3 未来优化方向
- 等社区
compose.uiohos composition host / HAR 形态落地后,把 Demo 从"ArkTS 重绘"切到 Compose 宿主——库侧 commonMain 已全量编译,届时零改动; - 桥补组件行为类 op(如按压 / 通知的逐帧动画值,对齐 day-and-night-switch 的帧数据模式);
- 跟踪社区线
1.12.0-0.1.0-08..11与工具链更新,评估升线重验; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 core(29 文件 commonMain,含 @Preview)
→ 验证 Compose ohos 制品线(material3 / animation / ui / tooling-preview)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
→ 根构建替换(cmp_qrose 模式,双线依赖按目标声明)
→ icons-shim 依赖模块(24 向量,1.7.3 逐字)
→ ohosArm64 / ohosX64 KLIB,零源码改动
→ C ABI(@CName × 2)+ 设计令牌载荷(toArgb 序列化)
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + 明暗令牌缓存
→ ArkUI 真机四 Tab 验收(版本握手 / 色板 / 组件画廊 / 桥日志)
9.2 封装层次
- 引擎层:上游原版
VetraColorScheme(37 槽明暗)、VetraTypography、VetraShadows+ShadowConfig、VetraPressIndication与全部组件; - Shim 层:24 个 material 图标向量的依赖模块复刻(仅 ohos 编译可见);
- Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON 令牌;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理明暗令牌缓存、四 Tab 页面状态、桥日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 路线选择跟着"库的形态"走,而不是跟着惯例走:有内核的库抽内核(qrose / snowfall),100% UI 的库整库编译(本仓 / ripple / eungabi)——判据是"抽走 Compose 后还剩不剩交付物"。设计系统抽走 Compose 只剩数据类,整库编译是唯一不折损的选择,而它成立的前提是先验证 material3 等 ohos klib 齐备;
- 令牌桥的诚实边界比"看起来能跑"重要:OHOS 没有 composition host 时,能自证的只有运行时真值——37 色 × 明暗从真库对象读出、
VetraPressIndication真实例化,转录项(shape / 映射 / 曲线)显式标注。Color.value高 32 位编码这类序列化坑只有在真机渲染时才暴露,颜色类桥接必须过toArgb(); - ** ArkTS 侧的"语法税"要用组件化模式化解**:@Builder 不收函数参数、builder 调用不可链式、尾随闭包收尾是
}、prop 不能撞内置属性名——把内容槽(@BuilderParam + 尾随闭包)和交互(回调属性 + 自组件内聚状态)拆成两个模式后,四 Tab 页面 1400 行零特例。
9.4 适配成果
当前适配已完成:
- Vetra UI
1.0.0-alpha03(MIT)真实设计系统的双 ABI Kotlin/Native 编译(上游commonMain29 文件零改动,含全部@Preview); - HarmonyOS Compose 制品线在设计系统库上的完整落地(runtime / foundation / ui / material3 / animation / tooling-preview
1.12.0-0.1.0-07+ Kotlin2.4.20-0.1.0-07); - icons-shim 依赖模块(24 向量逐字复刻,1.7.3 源码)与双线依赖声明(ohos 社区线 / jvm 上游线);
- 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证双 ABI 各 3/3 全局 T 符号),Release so arm64 18.7 MB / x86_64 18.3 MB; - DevEco Stage ArkUI 四 Tab 验收页面(版本握手、37 色板明暗切换、14 级字型、组件画廊、反馈族、桥日志);
- HAP 构建、签名、设备安装和真机验证(
bridge ok · 8 ops,darkbrand #3B82F6与上游逐字一致); - AtomGit 项目文档、OpenHarmony README(中英双份,21 节主文档)和本地效果图,适配以单提交
鸿蒙适配推送至社区仓。
适配过程 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)