本文记录 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 尺寸,外加一份 material3 ColorScheme 槽位映射,最后由 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/DesignSystem 1.0.1(本 fork 1.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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(2.2.21 旧线的配套版本)
依赖内核零第三方依赖;commonTest 仅 kotlin("test")
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.designsystem
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块/源集文件依赖形态处置
上游 colors / dimens / shapes / typography / theme19全部依赖 androidx.compose.ui/foundation/material3 + org.jetbrains.compose.resources整体隔离 composeMain(ohos 不编)
新内核 tokens4纯 Kotlin,零 import进 ohos 构建(jvm + 双 ohos)
上游 composeResources8 ttfCompose 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
themedarkdark、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)
colortoken、darktoken、dark、hex
typography—titleSizes / bodySizes / weights
spacing—spacings / elevations
shapes—shapeRadii
materialdarkdark、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 方法覆盖内核
DesignSystemBridgedesignSystemBridge(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.tomlKotlin 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.kt21 项完整性交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.ktC ABI、MiniJson、groupCounts、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次上游 Compose API鸿蒙对应
主题DesignSystemTheme(isDarkTheme)theme op(dark 参数)
颜色DesignSystemTheme.colors.textPrimarytokens 列表 / color op(token 名 + 明暗)
字体DesignSystemTheme.typography.titleLarge.boldtypography op(12 档字号 × 8 字重,ArkUI fontFamily 装配)
形状DesignSystemTheme.shapes(Shapes)shapes op(3 档圆角 → borderRadius)
尺寸DesignSystemTheme.dimens.dp16theme 响应的 dimensDp / dimensSp(43 + 24 项)
M3MaterialTheme.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 功能用例

  1. 启动后自动注册 Poppins 8 个 ttf 并调 theme dark=false,状态行出现 加载成功 theme dark=false tokens=41(来自真实 Native 调用);
  2. 色板 Tab 按 text(11) / background(11) / icons(3) / controls(7) / accent(4) / accentInverted(4) / primary(1) 七组渲染,accentActiveInverted 等 4 个 token 稳定落在 accentInverted 组、不混入 accent;
  3. 切暗色 Toggle,41 个色块全部换值,primary 明暗同为 #FF1E80F8 语义、文本/背景组整体反转;
  4. 字体 Tab 切换 8 个字重胶囊,title/body 各 6 档字号实时按桥返回值重渲染;
  5. 间距与形状 Tab 的彩条、阴影卡、圆角盒与 4/6/8/12/16/24 dp、2/4 dp、4/8/16 dp 刻度一一对应;
  6. M3 映射 Tab 显示 29 槽位,迷你卡片头用 primary/backgroundPrimary/textPrimary 装配,与色板 Tab 的对应 token 一致;
  7. 篡改请求为未知 token(color op),状态行显示 失败 color: unknown token: xxx,页面不崩;
  8. 真机截图见第七节。

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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 token 内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游根构建的 AGP + JB Compose 插件与鸿蒙工具链不共存,根构建必须整个替换,不能"加目标"了事;
  4. ArkTS 的滚动容器对齐枚举是 Alignment 不是 Align——.align(Align.Top) 编译期直接 Cannot find name 'Align',4 处报错同一根因;
  5. 色板分组不能按字符串前缀匹配:accentActiveInverted 开头是 accent 不是 accentInverted,必然错分——协议层补 groupCounts 计数、ArkTS 按序切片;
  6. ArkTS JSON.parse 嵌套对象在类型化上下文不可靠,桥响应必须全扁平标量、列表压 ; 分隔的 name=value;
  7. HarmonyOS Kotlin 的 CAdapter 不把 @CName 业务符号挂到 ArkTS,必须 CMake NAPI 包装;nm_modname / import 名 / index.d.ts 三个名字一个都不能错;
  8. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. 验证 so 内容时注意:Kotlin/Native 把字符串常量存成 UTF-16LE,strings 直接搜 ASCII 关键字会假阴性(groupCounts 曾因此被误判没编进去),按 UTF-16 字节序列搜;
  11. 上游 gradle.properties 明文提交了 Maven Central 账密与签名口令,fork 重写时全部清除;
  12. Windows 下 git mv 整目录可能被进程句柄锁住,按包逐个 git mv 即可,rename 记录等价;
  13. build-profile.json5 的 signingConfigs 保持空数组,勾选 DevEco 自动签名后由 IDE 回填,证书绝不进入 AtomGit。

8.2 已知问题

  • 鸿蒙侧当前没有 Compose 版 DesignSystemTheme:上游装配语义完整隔离在 composeMain,ArkTS 侧以桥数据重建主题,等社区 CMP 工具链对齐后整个目录移回 commonMain;
  • Token 内核只有数据、没有控件组件(上游本就是纯主题库),组件级封装留给业务侧或后续库;
  • materialScheme 29 槽中 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.ui ohos 制品就绪后,把 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 三条经验

  1. 路线选择先看依赖面:库的 API 站在哪个生态上,比库"是干什么的"更决定适配路线——KStore 站在 kotlinx(有 ohos 制品线)走真编译,DesignSystem 站在 Compose(无 ohos 制品)走内核提取;同一份社区里两种路线都成立,判据是当天的制品可达性;
  2. 主题库的"内核"是数据不是逻辑:没有算法可抽时,验收底线从"行为一致"变成"数值逐字一致"——每个 token 带出处注释 + 测试抽样比对,把审查链路闭合到上游源码行;
  3. 协议层消灭 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。

参考文档

Logo

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

更多推荐