本文记录 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 / ohosX64 klib)已经齐备——因此上游 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 / iconnotify op 的四元组。
instantiated / overlayColor / overlayAlphapress 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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle9.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 product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.vetraui
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
core29(约 1.17 万行,含 @Preview)compose runtime / foundation / ui / material3 / animation + icons,零平台源码集整体编译(jvm + 双 ohos)
icons-shim25(新增)纯 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 同款):

  1. 从 Maven Central 下载 material-icons-core:1.7.3-sources.jar 与 material-icons-extended:1.7.3-sources.jar;
  2. 逐字取出 Icons.kt(Icons 对象 + materialIcon / materialPath 构建器)与 24 个向量文件,路径数据一字不改、Apache-2.0 版权头保留;
  3. 放进独立模块 icons-shim/src/commonMain/kotlin/androidx/compose/material/icons/...,模块只挂 ohosArm64() / ohosX64();
  4. 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 方法覆盖内核
VetraDispatchvetraDispatch(req)全部 4 个 op 的令牌读取
VetraVersionvetraVersion()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.tomlKotlin 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.kt2 个 C ABI、令牌序列化(toArgb)、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面(GalleryCard / VetraButtonFace 自组件)
README.OpenHarmony_CN.md鸿蒙适配主文档(21 节,含桥协议契约与真实与复刻边界)

4.3 关键 API 对照

层次API作用
引擎VetraLightColorScheme / VetraDarkColorScheme37 槽位明 / 暗预设(桥运行时直读)
引擎DefaultVetraTypography14 级字型(size / lineHeight / tracking / weight)
引擎DefaultVetraShadows + Light/DarkModeShadowConfig8 级海拔 + 明暗阴影配置
引擎VetraPressIndication按压覆盖层指示类(press op 真实实例化)
引擎VetraTheme / VetraButton 等 24 个组件文件CompositionLocal 主题与全部 Composable(随编译进 .so,不跨桥)
NativeVetraDispatch / VetraVersionC ABI JSON 导出
N-APIvetraDispatch(req) / vetraVersion()向 ArkTS 暴露能力
ArkTSc(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 功能用例

  1. 启动后标题区出现 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;
  2. 主题 Tab 的 37 色板显示真值:dark 模式 brand #3B82F6、accent #8B5CF6——与上游 VetraDarkColorScheme 源码逐字一致;
  3. 右上角 Light/Dark Toggle 翻转,全页(含其他 Tab)令牌即时换套,零额外桥调用;
  4. 14 级字型标本按桥值渲染(labelLg 14sp Bold、displayXl 48sp / 行高 56 / 字距 -0.5);
  5. 组件 Tab 按钮五变体令牌取色,按住出现 0.1 alpha 覆盖层、松开消退(press op 规格);
  6. TextField 聚焦描边切换为 borderFocus 令牌;
  7. 反馈 Tab 四类 Notify 横幅的 bg / fg / accent 与 notify op 返回一致;
  8. 链路 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 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让 :core 真正编译成 ohosArm64 / ohosX64 动态库——设计系统库尤其如此,令牌真值是唯一能自证的交付物;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. material3 的版本线独立于 compose 主体:jvm 线找不到 material3:1.9.2(不存在),必须钉 1.9.0——可用上游发布件的 .module 依赖表对账确认;
  4. 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;
  5. material-icons 无任何 ohos 制品:shim 必须做成独立依赖模块(源码集声明对 commonMain 同目标编译不可见),向量路径逐字取自 1.7.3 源码而不是手绘;
  6. konanc 的 JVM 参数属性名是大写 A 的 kotlin.native.jvmArgs(小写 jvmargs 静默失效);堆大小按机器实测调——开着 DevEco / 模拟器时 3g 反而比 5g 稳(大堆挤占 LLVM 原生 arena,Internal Error 0x20474343 / malloc failed);
  7. debug 链接在内存紧张机器上固定崩在 KN 的 DWARF 生成路径(DIInsertDeclaration),Demo 改用 linkReleaseShared* 交付(无 -g 直接绕开,还小 10 MB);
  8. Color.value 在 64 位 ULong 里把 ARGB 编码在高 32 位(低 32 位是色彩空间标记),直接 toString(16) 得到 16 位十六进制(#FF3B82F600000000),ArkTS 按非法 ResourceColor 渲染成透明——全页"没有颜色"的根因;必须 color.toArgb().toUInt().toString(16);
  9. ArkTS 的 @Builder 方法不接受函数参数('content()' does not meet UI component syntax),也不能对 builder 调用结果链 .onClick()(返回 void)——内容槽改 @Component + @BuilderParam(尾随闭包),交互改回调属性;
  10. ArkTS 尾随闭包的收尾是单独的 },写成 }) 会报一串 ';' expected(全文件括号配平检查可定位);自组件 prop 不能撞 ArkUI 内置属性名(borderColor 是 CommonAttribute 方法名,改名 strokeColor);.scale() 必须传 {x, y} 对象不能传数字;
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  12. 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.ui ohos 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 三条经验

  1. 路线选择跟着"库的形态"走,而不是跟着惯例走:有内核的库抽内核(qrose / snowfall),100% UI 的库整库编译(本仓 / ripple / eungabi)——判据是"抽走 Compose 后还剩不剩交付物"。设计系统抽走 Compose 只剩数据类,整库编译是唯一不折损的选择,而它成立的前提是先验证 material3 等 ohos klib 齐备;
  2. 令牌桥的诚实边界比"看起来能跑"重要:OHOS 没有 composition host 时,能自证的只有运行时真值——37 色 × 明暗从真库对象读出、VetraPressIndication 真实例化,转录项(shape / 映射 / 曲线)显式标注。Color.value 高 32 位编码这类序列化坑只有在真机渲染时才暴露,颜色类桥接必须过 toArgb();
  3. ** ArkTS 侧的"语法税"要用组件化模式化解**:@Builder 不收函数参数、builder 调用不可链式、尾随闭包收尾是 }、prop 不能撞内置属性名——把内容槽(@BuilderParam + 尾随闭包)和交互(回调属性 + 自组件内聚状态)拆成两个模式后,四 Tab 页面 1400 行零特例。

9.4 适配成果

当前适配已完成:

  • Vetra UI 1.0.0-alpha03(MIT)真实设计系统的双 ABI Kotlin/Native 编译(上游 commonMain 29 文件零改动,含全部 @Preview);
  • HarmonyOS Compose 制品线在设计系统库上的完整落地(runtime / foundation / ui / material3 / animation / tooling-preview 1.12.0-0.1.0-07 + Kotlin 2.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,dark brand #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。

参考文档

Logo

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

更多推荐