开源鸿蒙平台 CMP 三方库「DesignSystem」适配全流程
本文记录
DesignSystem接入 OpenHarmony 的完整过程,覆盖 Compose 主题库盘点、设计令牌边界划定、鸿蒙 Kotlin 工具链钉版、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
DesignSystem是基于 Compose Multiplatform 的设计系统库:41 个颜色 token 组成明暗两套调色板(text / background / icons / controls / accent / accentInverted / primary 七组),Poppins 字体的 12 档字号与 8 个字
重,6 档间距、2 档阴影、3 档圆角,43 个 dp 与 24 个 sp 尺寸,外加一份 material3ColorScheme槽位映射,最后由DesignSystemTheme把它们装配到MaterialTheme上。本次适配与 KStore 的"真编译整库"路线相反、与社区早期 qrose / FlexibleBottomSheet 的"零依赖内核提取"同路:上游全部 API 都站在androidx.compose与 material3 上,社区 HarmonyOS Kotlin(2.2.21-1.0.0)没有配套的 Compose 发行版,Compose 侧一行都编不进去——所以设计令牌(纯数据,不是 UI)被抽成零依赖的纯 Kotlin 内核编成 OpenHarmony KLIB / 动态库,上游 Compose 主题装配原样隔离进composeMain等工具链对齐后接回,ArkUI 页面作为验收宿主把同一套主题重建出来。
项目地址: AtomGit/oh-tpc/DesignSystem.git
上游仓库: yusuf0405/DesignSystem
上游基线:
yusuf0405/DesignSystem1.0.1(本 fork1.0.1-ohos.1)。
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
DesignSystem 为 Compose Multiplatform 应用提供"主题从哪来"的完整答案:DesignSystemTheme { } 一行包住应用,内部把 41 色 token 按 isDarkTheme 切换明暗两套调色板,把 Poppins 的 12 档字号装进 DesignSystemTypography,把间距、阴影、圆角装进 Shapes 与 DesignSystemDimens,再经 debugColors() 映射成 material3 ColorScheme 交给 MaterialTheme;业务侧通过 DesignSystemTheme.colors / typography / shapes / dimens 四个入口取值,token 名即 DesignSystemColors 属性名,跨端对照零翻译。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套统一主题,但上游构建(Kotlin 2.1.0 / JB Compose 1.7.3 / AGP 8.6.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以画出色块,却无法证明同一份 token 数据在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证设计令牌数据、主题映射语义、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.1.0 + JB Compose 1.7.3 + AGP 8.6.1 与鸿蒙工具链无法共存于一个 Gradle 构建;androidx.compose / material3 在社区 HarmonyOS Kotlin 上没有制品,Compose 侧零行可编。 |
| 库形态特殊 | 这个库是"纯主题库":没有算法、没有组件,全部价值是 token 数据 + Compose 装配。数据可编,装配不可编,必须切开。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| Compose 语义跨不了桥 | @Composable 取值、CompositionLocal、mutableStateOf 是 Kotlin 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:token 内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。token 数值完全由上游 Colors*.kt / Dimensions.kt / DesignSystemDimens.kt 原样搬运(逐字,不发明、不"优化"),ArkTS 只负责调用 Native、解析扁平 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一次完整的主题取数会话,对应桥接的全部入口:
- 全量取数(theme):一次返回当前模式的 41 个 token(
name=#AARRGGBB)、7 组分组名与每组计数(groupCounts)、12 档字号、8 个字重、6 档间距、2 档阴影、3 档圆角、43 dp、24 sp 与 29 槽 material3 映射; - 单点取色(color):按 token 名 + 明暗取单值,未知 token 返回
ok:false而不是崩溃; - 分组取数(typography / spacing / shapes / material):字号与字重、间距与阴影、圆角、M3 槽位映射分别可独立获取;
- 版本握手(ping):返回 fork 版本号
1.0.1-ohos.1,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
dark / version | 回显当前明暗模式与 fork 版本。 |
tokens 等 | 全部列表字段编码为 ; 分隔的 name=value 字符串(颜色 name=#AARRGGBB),响应只含扁平标量。 |
主题装配语义(CompositionLocal 注入、mutableStateOf 响应式切换、MaterialTheme 装配)全部在上游 composeMain 里原样保留——不是被删掉,是被隔离;JVM 侧保留 21 项 TokenIntegrityTest 交叉验证用例,证明 ohos .so 里执行的 token 数据没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | token 内核数值逐字取自上游 Colors*.kt / Dimensions.kt / DesignSystemDimens.kt / Shape.kt,每个值带出处注释;上游 Compose 源码零改动整体隔离进 composeMain。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 composeApp(依赖远端库 + Compose 插件 + AGP)留盘不进 settings.gradle.kts。 |
| 桥接稳定 | 单个 C ABI 和 UTF-8 扁平 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 数据纪律 | token 数值、字号、间距、圆角与上游逐字一致;测试钉住项数、格式与抽样值。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:色板、字体、间距与形状、M3 映射。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的
DesignSystemTheme是 Compose 装配,鸿蒙侧当前没有可用的 Compose 运行时;ArkTS Demo 是把桥暴露的 token 数据用 ArkUI 重新装配出来,装配语义与上游一一对应。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 token 边界与 Compose 依赖面、确定"数据可编、装配隔离"的交付物
第 2 阶段:工具链钉版 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0、Gradle 8.14.1、JDK 21
第 3 阶段:源码集拆分 ── 上游 Compose 源码整体隔离 composeMain、纯 Kotlin token 内核落 commonMain、21 项完整性测试
第 4 阶段:桥协议建模 ── 7 个 op、扁平标量契约、groupCounts 分组计数、错误通道
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、CMake 三件套、DisposeString 纪律
第 6 阶段:宿主与验证 ── ArkUI 四 Tab、jvmTest 交叉验证、HAP、签名、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
designsystem/src/commonMain/ 新增:纯 Kotlin token 内核(零依赖,进 ohos 构建)
designsystem/src/commonTest/ 新增:TokenIntegrityTest 21 项(JVM 交叉验证)
designsystem/src/composeMain/ 上游 Compose 源码整体隔离(commonMain/androidMain/iosMain/jsMain/jvmMain/wasmJsMain
六个源集 + 8 个 Poppins ttf,不注册、不编译,等工具链对齐接回)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
composeApp/ iosApp/ 上游示例与 iOS 壳,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :designsystem 与 :example:nativeApp,rootProject.name = "ohos_designsystem"。上游的 vanniktech 发布插件、AGP 插件、JB Compose 插件、Sonatype 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(2.2.21 旧线的配套版本) |
| 依赖 | 内核零第三方依赖;commonTest 仅 kotlin("test") |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.designsystem |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块/源集 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
| 上游 colors / dimens / shapes / typography / theme | 19 | 全部依赖 androidx.compose.ui/foundation/material3 + org.jetbrains.compose.resources | 整体隔离 composeMain(ohos 不编) |
| 新内核 tokens | 4 | 纯 Kotlin,零 import | 进 ohos 构建(jvm + 双 ohos) |
| 上游 composeResources | 8 ttf | Compose resources 字体 | 随 composeMain 隔离;ttf 另拷入 HAP rawfile 给 ArkUI 注册 |
token 数值的盘点结果是内核的验收基线:41 个颜色 token(明暗两套、7 组;含 onTextPrimary、onBackgroundPrimary 这类明暗交叉取值,以及 backgroundStroke = #80BCC3D0、backgroundOverlay = #661D2023 这类带 alpha 的半透明 token)、12 档字号(title 28/26/24/22/21/20 sp + body 18/17/16/14/13/12 sp)、8 个字重(上游 createFontsType() 参数序:black / bold / regular / light / medium / semiBold / thin / extraBold)、6 档间距 + 2 档阴影、3 档圆角(small=4 / medium=8 / large=16,分别来自 SmallSpacing / MediumSpacing / LargeSpacing)、43 个 dp 与 24 个 sp 尺寸(上游 DesignSystemDimens 声明序原样保留——dp 实际 43 项、sp 实际 24 项,按盘点结果收录,不做"补齐成整十"的假数据)。
material3 映射单独说明:上游 debugColors() 只映射两个槽位——primary = palette.onBackgroundPrimary、secondary = palette.backgroundSecondary,其余槽位落 lightColorScheme() / darkColorScheme() 出厂基线。内核把 29 个槽位按出厂参数序整体移植成纯数据,primary / secondary 两槽按上游逻辑覆盖——映射规则与上游逐槽一致,不自行"补全设计"。
1.4 页面边界
标题区 DesignSystem · OpenHarmony / 版本行(设计令牌内核 1.0.1-ohos.1 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 色板 / 字体 / 间距与形状 / M3 映射
色板 Tab 明暗 Toggle、7 分组 41 token(色块 + token 名 + #AARRGGBB)、状态行
字体 Tab 8 字重胶囊、title/body 各 6 档按桥返回字号实时渲染、12 项字号总表
间距与形状 Tab 6 档间距彩条(值×6 vp)、2 档 elevation 阴影卡、3 档圆角盒
M3 映射 Tab 29 槽位色表 + 迷你卡片预览(primary 头部 / backgroundPrimary 卡身 / textPrimary 正文)
状态行 最近一次操作结果(如"加载成功 theme dark=false tokens=41")
页面 aboutToAppear 先注册 Poppins 8 个 ttf(rawfile/fonts/),再调 theme dark=false 渲染默认明色主题。
第 2 阶段:工具链钉版
2.1 为什么是 2.2.21-1.0.0 + Gradle 8.14.1
这个库没有第三方 Kotlin 依赖,工具链只由一件事决定:ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件里。社区已审的钉法是 2.2.21-1.0.0(qrose / FlexibleBottomSheet 同款),配套 Gradle 8.14.1、JDK 21。注意两条工具链线的经验不能互相套用:KStore 那类依赖 kotlinx ohos 制品线的库必须升 2.4.20-0.1.0-07 + Gradle 9.x;本库零依赖,落在旧线的钉法上即可。上游的 Kotlin 2.1.0 + AGP 8.6.1 + JB Compose 1.7.3 在鸿蒙上直接 sync 会在配置期就死,根构建必须整个替换。
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_designsystem"
include(":designsystem")
include(":example:nativeApp")
两个 eazytec 仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64。同时 gradle.properties 钉 kotlin.native.ignoreDisabledTargets=true、kotlin.native.distribution.downloadFromMaven=true(KN 发行版走 Maven 下载)。
2.2 泄漏凭据清理
上游 gradle.properties 把 Maven Central 账密与签名私钥口令明文提交进了仓库。fork 重写该文件时全部清除,只留工具链项——不要再从上游原样拷贝这个文件。
2.3 库模块收敛
designsystem/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
android library、JB Compose、vanniktech 发布、POM 配置全部剥离。实测:
./gradlew :designsystem:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :designsystem:compileKotlinOhosArm64 # BUILD SUCCESSFUL
./gradlew :designsystem:jvmTest # 21/21 全绿
第 3 阶段:源码集拆分
3.1 composeMain 隔离
上游 19 个 Compose 源文件 + 5 个平台 actual + 8 个 Poppins ttf,用 git mv 整体移入 designsystem/src/composeMain/(保持六个源集的目录结构)。composeMain 不是注册的源集,不参与任何编译——隔离不是删除,等社区 CMP 工具链对齐后整个目录移回 commonMain 即可恢复上游全量 API。Windows 下 git mv 整目录可能被 java 进程句柄锁住报 Permission denied,按包逐个 git mv 即可,rename 记录等价。
3.2 纯 Kotlin token 内核
新内核 4 个文件落在 org.joseph.designsystem.tokens(与上游包名连续,API 对照零翻译):
data class DesignTokenColor(val name: String, val argb: Long) // 0xAARRGGBB
class DesignSystemPalette(val dark: Boolean, val tokens: List<DesignTokenColor>) {
fun size(): Int
fun names(): List<String>
fun argb(name: String): Long?
fun hex(name: String): String // "#AARRGGBB" 8 位大写
}
object DesignSystemTokens {
const val VERSION = "1.0.1-ohos.1"
fun palette(dark: Boolean): DesignSystemPalette
val categories: Map<String, List<String>> // 7 组,token 名 = 上游 DesignSystemColors 属性名
val titleSizes / bodySizes: Map<String, Double>
val fontWeights: List<String> // 上游 createFontsType 参数序
val spacings / elevations / shapeRadii: Map<String, Double>
val dimensDp / dimensSp: Map<String, Double>
fun materialScheme(dark: Boolean): Map<String, String> // 29 槽,LinkedHashMap 保序
fun snapshot(dark: Boolean): DesignSystemSnapshot
}
每个数值带出处注释(如 DesignTokenColor("textPrimary", 0xFF1D2023L), // TextPrimary),审查时可以直接和上游 Colors*.kt 逐行对。半透明 token(0x80BCC3D0 等)原样保留——测试断言完整 32 位范围而不是"不透明",因为上游本来就有带 alpha 的 token。
3.3 21 项完整性测试
TokenIntegrityTest(纯 kotlin.test,不依赖 compose)钉住:明暗两套各 41 项且 token 名集合一致、argb 范围与 hex 格式回环、字号/间距/阴影/圆角/dp/sp 的项数与抽验值(titleLarge=26.0、bodyMedium=14.0、dp800=800.0、sp48=48.0)、materialScheme 明暗槽位集合一致且 ≥20 槽、抽样 hex 与上游源码逐字一致(期望值从 composeMain 源码抄出并注释来源)。JVM 与 ohos 跑同一份内核代码,这 21 项就是"内核没漂"的闸门。
第 4 阶段:桥协议建模
4.1 JSON 契约
org.joseph.designsystem.tokens/**(纯 Kotlin 内核,零依赖)
↓ DesignSystemTokens / Palette / materialScheme
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API designSystemBridge()
↓
ArkUI Index.ets(四 Tab)
请求与响应:
请求 : {"op":"theme","dark":false}
{"op":"color","token":"textPrimary","dark":true}
{"op":"typography"} / {"op":"spacing"} / {"op":"shapes"} / {"op":"material","dark":true}
{"op":"ping"}
响应 : {"ok":true, ...扁平标量字段...}
{"ok":false,"error":"unknown token: xxx"}
响应只允许扁平标量(string/number/boolean)——鸿蒙 ArkTS 侧 JSON.parse 出来的嵌套对象在类型化上下文里不可靠,这是 cmp_qrose 审核模式定死的约束。所以全部列表字段压成一条 ; 分隔的 name=value 字符串,ArkTS 一个 parsePairs()(找 = 切名值)就能还原;categories、weights 这类纯列表用 ; 直接连项。
4.2 七个 op 与 groupCounts
| op | 请求字段 | 响应字段 |
|---|---|---|
ping | — | lib / version / symbol |
theme | dark | dark、version、categories(7)、groupCounts(7)、tokens(41)、titleSizes(6)、bodySizes(6)、weights(8)、spacings(6)、elevations(2)、shapeRadii(3)、dimensDp(43)、dimensSp(24)、material(29) |
color | token、dark | token、dark、hex |
typography | — | titleSizes / bodySizes / weights |
spacing | — | spacings / elevations |
shapes | — | shapeRadii |
material | dark | dark、material |
groupCounts 是踩坑后补的字段:tokens 按 categories 顺序铺开,ArkTS 分组如果按字符串前缀匹配,accentActiveInverted 会因为开头是 accent 被错分进 accent 组(它不是 accentInverted 开头)。桥多发一份 text=11;background=11;icons=3;controls=7;accent=4;accentInverted=4;primary=1,ArkTS 按序切片分组,不做任何前缀判断。
4.3 错误通道
导出函数整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("DesignSystemBridge")
fun designSystemBridge(request: String): String = try {
dispatch(MiniJson.parse(request))
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、未知 op、未知 token、任何内部异常,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行,页面不崩。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libdesignsystem_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosdesignsystem.so)
│ DesignSystemBridge
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制 token 语义,所以选择 C ABI + JSON。
5.2 Kotlin/Native 导出函数
@CName("DesignSystemBridge")
fun designSystemBridge(request: String): String // 七个 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
DesignSystemBridge | designSystemBridge(req) | DesignSystemTokens 全部取数入口 |
HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS 模块——libohosdesignsystem.so 里 llvm-nm 能看到 DesignSystemBridge 是全局 T 符号,但 ArkTS import 'libohosdesignsystem.so' 拿不到它,这就是必须再包一层 CMake NAPI 的原因。
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"designSystemBridge", nullptr, NapiDesignSystemBridge, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。三个名字必须一致:CMake nm_modname = "designsystem_napi"、ArkTS import 'libdesignsystem_napi.so'、index.d.ts 导出 designSystemBridge——有一个写错,表现都是"能安装、一调桥就崩"。KN 返回的字符串必须经 libohosdesignsystem_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosdesignsystem_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
5.4 CMake 和类型声明
add_library(ohosdesignsystem SHARED IMPORTED)
set_target_properties(ohosdesignsystem PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(designsystem_napi SHARED napi_init.cpp)
target_link_libraries(designsystem_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosdesignsystem)
OHOS_ARCH 为空或 libohosdesignsystem.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。KN 侧 copyKnSo* Gradle 任务把 libohosdesignsystem.so 与对应 triple 的 libc++_shared.so(定位自 ~/.konan/dependencies)自动拷进 entry/libs/<abi>/,link 任务 finalizedBy 回填,不手工借 so。
export const designSystemBridge: (request: string) => string;
第 6 阶段:ArkUI 宿主与验证
6.1 注入字体和触发取数
aboutToAppear(): void {
this.registerFonts(); // Poppins 8 个 ttf:$rawfile('fonts/poppins_xxx.ttf')
this.loadTheme(false); // 默认明色主题
}
private loadTheme(dark: boolean): void {
const res = this.request({ 'op': 'theme', 'dark': dark }, 'theme');
this.tokens = this.parsePairs(this.asString(res['tokens']));
this.groupCounts = this.parsePairs(this.asString(res['groupCounts']));
// ...
}
上游 Poppins 只有 8 个 ttf、没有 SemiBold 字重文件——poppins_semiBold 家族注册到 poppins_bold.ttf 近似渲染(字重名保持上游 8 个不变,文档如实记录)。SDK 对 registerFont 给了 deprecation 警告,功能正常,属非阻断项。
6.2 页面交互
色板 Tab 自上而下:明暗 Toggle(切换重新拉 theme)、7 个分组标题 + 色块行(28dp 色块 + token 名等宽字体 + #AARRGGBB 原文),半透明 token(如 backgroundStroke = #80BCC3D0)在色块上直接可见,底部状态行显示 加载成功 theme dark=false tokens=41。字体 Tab 用 8 个字重胶囊切换 fontFamily,title/body 各 6 档的 fontSize 直接取桥返回值——页面上渲染的每个字号都是 Native 传来的数据,不是硬编码。间距与形状 Tab 用彩条(值×6 vp)、阴影卡、圆角盒还原 6+2+3 档刻度。M3 映射 Tab 渲染 29 槽位色表,顶部用 primary / backgroundPrimary / textPrimary 拼一个迷你卡片头直观看到装配效果。
6.3 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosdesignsystem)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/Bridge.kt @CName × 1 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.designsystem、versionName 1.0.1-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohosdesignsystem_api.h
├── src/main/cpp/types/libdesignsystem_napi/{index.d.ts, oh-package.json5}
├── src/main/resources/rawfile/fonts/ Poppins 8 个 ttf
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.4 构建、签名和安装
./gradlew :designsystem:jvmTest # 21 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。
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.designsystem -m entry
四、完整代码对照
4.1 整体架构
designsystem/src/composeMain/**(上游 Compose 源码,原样隔离,ohos 不编)
│ DesignSystemTheme / Colors / Typography / Dimens / Shapes + 8 个 Poppins ttf
designsystem/src/commonMain/kotlin/org/joseph/designsystem/tokens/**(纯 Kotlin 内核,进 ohos)
│ DesignSystemTokens(41 色 / 12 档字号 / 43 dp / 24 sp / 29 个 M3 槽)
Bridge.kt + MiniJson
│ DesignSystemBridge(扁平 JSON)
libdesignsystem_napi.so
│ designSystemBridge
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(色板 / 字体 / 间距与形状 / M3 映射)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_designsystem、composeApp 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 + kotlin-multiplatform 插件别名(其余全删) |
build.gradle.kts(根) | 版本 1.0.1-ohos.1、全局禁用 Ohos*Test* 任务 |
designsystem/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,Compose/AGP/发布插件全剥离 |
designsystem/src/composeMain/ | 上游 Compose 源码整体隔离(31 个 git mv,零改动) |
tokens/DesignSystemTokens.kt 等内核 4 文件 | token 纯数据(逐字取自上游,带出处注释) |
commonTest/TokenIntegrityTest.kt | 21 项完整性交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | C ABI、MiniJson、groupCounts、错误通道 |
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 | 鸿蒙适配主文档 |
4.3 关键 API 对照
| 层次 | 上游 Compose API | 鸿蒙对应 |
|---|---|---|
| 主题 | DesignSystemTheme(isDarkTheme) | theme op(dark 参数) |
| 颜色 | DesignSystemTheme.colors.textPrimary | tokens 列表 / color op(token 名 + 明暗) |
| 字体 | DesignSystemTheme.typography.titleLarge.bold | typography op(12 档字号 × 8 字重,ArkUI fontFamily 装配) |
| 形状 | DesignSystemTheme.shapes(Shapes) | shapes op(3 档圆角 → borderRadius) |
| 尺寸 | DesignSystemTheme.dimens.dp16 | theme 响应的 dimensDp / dimensSp(43 + 24 项) |
| M3 | MaterialTheme.colorScheme(debugColors) | material op(29 槽,primary/secondary 按上游映射) |
| 分组 | DesignSystemColors 属性名 | categories + groupCounts(7 组,计数切片) |
| Native | — | DesignSystemBridge / designSystemBridge(req) |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(designSystemBridge(JSON.stringify({ op: 'theme', dark: true })));
// res.tokens === "textHeadline=#FFFFFFFF;textPrimary=#FFFAFAFA;..."
// res.material === "primary=#FFFFFFFF;onPrimary=#FF181A20;..."
@CName("DesignSystemBridge")
fun designSystemBridge(request: String): String = try {
val json = MiniJson.parse(request)
dispatch(json) // palette(dark) / materialScheme(dark) / titleSizes ...
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:抽零依赖内核而不是真编译整库
KStore 那类库在 kotlinx ohos 制品线补齐后走"真编译整库";本库正相反——上游 API 100% 站在 androidx.compose 与 material3 上,社区 HarmonyOS Kotlin 没有配套 Compose 发行版,Compose 侧一行都编不进去。库的价值切成两半:token 数据(纯数据,可编)与主题装配(Compose,不可编)。抽内核让数据侧零依赖直达 .so,装配侧整体隔离 composeMain 等工具链,两边都不造假。
决策 2:数值逐字搬运,每值带出处
设计系统库的验收底线是"鸿蒙上拿到的就是上游那份 token"。内核里每个 DesignTokenColor 都注释上游字面量出处(// TextPrimary),测试再钉一轮抽样比对。token 里有 alpha 半透明值、有明暗交叉取值(light 的 onTextPrimary 取的是 Dark 色板里的 TextPrimaryDark),这些"看起来像 bug"的细节恰恰是上游语义,照抄不改。
决策 3:根构建替换(cmp_qrose 模式),composeApp 留盘
上游根构建(vanniktech 发布 / AGP / JB Compose 插件)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。上游 composeApp 依赖的是远端 io.github.yusuf0405:designsystem:1.0.1 而非本仓模块,对本 fork 的验证毫无作用,留盘不进 settings.gradle.kts,README 里明确"不要拿来 sync"。
决策 4:单 C ABI + 全扁平标量 + groupCounts
一个 DesignSystemBridge 覆盖七个 op,桥面不随功能膨胀。响应全扁平标量(ArkTS 嵌套对象不可靠),列表压 ; 分隔的 name=value;分组不用字符串前缀匹配,桥多发一份 groupCounts 计数、ArkTS 按序切片——accentInverted 与 accent 的前缀歧义从协议层消除。
决策 5:jvmTest 交叉验证 + 测试钉源头
内核逻辑在 commonMain,JVM 与 ohos 跑同一份代码;21 项 TokenIntegrityTest 钉项数、格式、抽样值与上游逐字比对,是"内核没漂"的闸门。测试期望值直接从 composeMain 上游源码抄出并注释来源,审查链路闭合。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Eclipse Temurin,org.gradle.java.home 钉版)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :designsystem:jvmTest
21 项用例全部通过(TokenIntegrityTest):
| 验证块 | 钉住什么 |
|---|---|
| 调色板结构 | 明暗两套各 41 项、token 名集合一致、与 categories 展开序一致 |
| 数值格式 | argb 完整 32 位范围(含 alpha token)、hex() 8 位大写回环 |
| 刻度表 | title/body 各 6 项、字重 8 项、间距 6 项、阴影 2 项、圆角 3 项、dp 43 项、sp 24 项 |
| 抽样比对 | textPrimary=#FF1D2023、dp800=800.0、sp48=48.0 等与上游源码逐字一致(期望值注释来源) |
| M3 映射 | 明暗各 29 槽、槽位集合一致、primary/secondary 两槽覆盖逻辑与 debugColors() 一致 |
6.3 HAP 验证
:designsystem:compileKotlinJvm: BUILD SUCCESSFUL
:designsystem:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 回填 entry/libs)
llvm-nm -D: DesignSystemBridge / libohosdesignsystem_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libdesignsystem_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully
libohosdesignsystem.so arm64 约 2.3MB、x86_64 约 2.0MB(内含 Kotlin/Native 运行时;内核零第三方依赖,体积比带 kotlinx 的库小一个档位),随包各带一份匹配 triple 的 libc++_shared.so 与 CMake 编出的 libdesignsystem_napi.so。
6.4 功能用例
- 启动后自动注册 Poppins 8 个 ttf 并调
theme dark=false,状态行出现加载成功 theme dark=false tokens=41(来自真实 Native 调用); - 色板 Tab 按 text(11) / background(11) / icons(3) / controls(7) / accent(4) / accentInverted(4) / primary(1) 七组渲染,
accentActiveInverted等 4 个 token 稳定落在accentInverted组、不混入accent; - 切暗色 Toggle,41 个色块全部换值,
primary明暗同为#FF1E80F8语义、文本/背景组整体反转; - 字体 Tab 切换 8 个字重胶囊,title/body 各 6 档字号实时按桥返回值重渲染;
- 间距与形状 Tab 的彩条、阴影卡、圆角盒与 4/6/8/12/16/24 dp、2/4 dp、4/8/16 dp 刻度一一对应;
- M3 映射 Tab 显示 29 槽位,迷你卡片头用
primary/backgroundPrimary/textPrimary装配,与色板 Tab 的对应 token 一致; - 篡改请求为未知 token(
colorop),状态行显示失败 color: unknown token: xxx,页面不崩; - 真机截图见第七节。
6.5 验证结论
设计令牌数据、主题映射语义、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的主题取数会话:版本行、41 token 明暗调色板、Poppins 字号表、间距与圆角刻度、M3 槽位映射(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/DesignSystem.git
cd DesignSystem
export JAVA_HOME="/path/to/jdk-21"
./gradlew :designsystem:compileKotlinOhosArm64 :designsystem:compileKotlinOhosX64 # klib
./gradlew :designsystem:jvmTest # 21 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
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.designsystem -m entry
hdc shell hilog | grep DesignSystemNapi
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让 token 内核真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游根构建的 AGP + JB Compose 插件与鸿蒙工具链不共存,根构建必须整个替换,不能"加目标"了事;
- ArkTS 的滚动容器对齐枚举是
Alignment不是Align——.align(Align.Top)编译期直接Cannot find name 'Align',4 处报错同一根因; - 色板分组不能按字符串前缀匹配:
accentActiveInverted开头是accent不是accentInverted,必然错分——协议层补groupCounts计数、ArkTS 按序切片; - ArkTS
JSON.parse嵌套对象在类型化上下文不可靠,桥响应必须全扁平标量、列表压;分隔的name=value; - HarmonyOS Kotlin 的 CAdapter 不把
@CName业务符号挂到 ArkTS,必须 CMake NAPI 包装;nm_modname/ import 名 /index.d.ts三个名字一个都不能错; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 验证 so 内容时注意:Kotlin/Native 把字符串常量存成 UTF-16LE,
strings直接搜 ASCII 关键字会假阴性(groupCounts曾因此被误判没编进去),按 UTF-16 字节序列搜; - 上游
gradle.properties明文提交了 Maven Central 账密与签名口令,fork 重写时全部清除; - Windows 下
git mv整目录可能被进程句柄锁住,按包逐个git mv即可,rename 记录等价; build-profile.json5的signingConfigs保持空数组,勾选 DevEco 自动签名后由 IDE 回填,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧当前没有 Compose 版
DesignSystemTheme:上游装配语义完整隔离在composeMain,ArkTS 侧以桥数据重建主题,等社区 CMP 工具链对齐后整个目录移回commonMain; - Token 内核只有数据、没有控件组件(上游本就是纯主题库),组件级封装留给业务侧或后续库;
materialScheme29 槽中 27 槽是 material3 出厂基线默认值,只有primary/secondary来自上游自定义映射——上游debugColors()本来就只映射这两槽;- 上游 Poppins 无 SemiBold 字重 ttf,Demo 的
poppins_semiBold家族以 Bold 近似渲染(8 个字重名保持不变); - android / ios / js / wasmJs / jvm-desktop 目标剥离(源文件整体留在
composeMain树里),发布、POM 一并剥离,本 fork 不发 Maven Central; - ArkTS 页面的
font.registerFont在新 SDK 上有 deprecation 警告(功能正常,后续跟进新字体注册 API); - HAP 安装必须使用与 bundle name
org.terminator.ohos.designsystem匹配的签名。
8.3 未来优化方向
- 社区 CMP /
compose.uiohos 制品就绪后,把composeMain接回commonMain,让真正的DesignSystemTheme在 Compose 运行时里跑起来,ArkUI Demo 降级为对照实现; - 补齐暗色主题、字体、M3 映射的真机截图矩阵(当前收录色板明色一张);
- 评估
.har/ npm 形态分发 token 数据,让 ArkTS 工程直接依赖而不是走 so 三件套; - 增加
snapshot全量 diff、token 检索(按前缀/分组过滤)等桥入口; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建。
九、总结
9.1 核心难点回顾
上游 DesignSystem(19 个 Compose 源文件,100% androidx.compose/material3 依赖)
→ 判定路线:Compose 侧零行可编 → 数据/装配切分
→ 工具链钉版 Kotlin 2.2.21-1.0.0(eazytec 置前,Gradle 8.14.1)
→ 根构建替换(cmp_qrose 模式),composeApp 留盘
→ 31 个 git mv 整体隔离 composeMain,commonMain 落纯 Kotlin token 内核
→ ohosArm64 / ohosX64 KLIB + 21 项完整性测试
→ C ABI(@CName × 1)+ 扁平 JSON + groupCounts
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + Poppins rawfile 注册
→ ArkUI 四 Tab 真机验收(色板 / 字体 / 间距与形状 / M3 映射)
9.2 封装层次
- 数据层:上游 token 数值逐字搬运的纯 Kotlin 内核(41 色 ×2、12 档字号、8 字重、6+2+3 刻度、43 dp、24 sp、29 槽 M3 映射);
- 隔离层:上游 Compose 装配整体隔离
composeMain,源码零改动、可整体回归; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出扁平 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、字体注册、分组切片、操作分发与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 路线选择先看依赖面:库的 API 站在哪个生态上,比库"是干什么的"更决定适配路线——KStore 站在 kotlinx(有 ohos 制品线)走真编译,DesignSystem 站在 Compose(无 ohos 制品)走内核提取;同一份社区里两种路线都成立,判据是当天的制品可达性;
- 主题库的"内核"是数据不是逻辑:没有算法可抽时,验收底线从"行为一致"变成"数值逐字一致"——每个 token 带出处注释 + 测试抽样比对,把审查链路闭合到上游源码行;
- 协议层消灭 UI 层的字符串技巧:ArkTS 侧一切按字符串前缀/手工切分的技巧都会在某个 token 名上翻车(
accentInverted就是现成反例),桥多发一个计数字段,比 UI 层十行防御代码可靠。
9.4 适配成果
当前适配已完成:
- DesignSystem
1.0.1设计令牌的双 ABI Kotlin/Native 编译(41 色 ×2 / 12 档字号 / 8 字重 / 43 dp / 24 sp / 29 槽 M3 映射,数值逐字取自上游); - 上游 Compose 源码 31 个文件整体隔离
composeMain(零改动,可随工具链回归); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证双 ABI 全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面(色板明暗切换、Poppins 字体、间距与形状、M3 映射);
- 21 项 JVM 完整性交叉验证全绿 + HAP 构建(双 ABI 三件套)、签名、设备安装和真机验证;
- 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)