本文记录 compose-stylesheet 接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的路线定性、社区 Compose 鸿蒙制品线选型、上游 commonMain 整库编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、无头样式解析门面、ArkUI 真机页面、HAP 构建和真机验收。

compose-stylesheet 是面向 Jetpack Compose / Compose Multiplatform 的灵活 UI 组件样式框架:用 ProvideStyleSheet(styleSheet) { ... } 在应用根部提供全局样式,用 Token("primaryColor", default = Color.Black) 定义设计令牌并按明暗模式覆写,用 content { } / surface { } 一类 DSL 定制组件样式,用 Tag("primaryText", text) 声明组件变体,内置 themeStyleSheet() 提供默认明暗主题,任意样式表经 StyleSheet.merge() 合并。本次适配与社区早期"零依赖内核提取"路线(qrose 的编码器隔离)有本质区别:这是一个纯 Compose UI 库——全部 62+ 个 commonMain 文件的语义都是 Compose(Token<Color>、ComponentStyleSet、@Composable 组件),没有可隔离的纯 Kotlin 内核。因此走社区为这类库确立的 compose-shimmer / eungabi 路线:上游 commonMain 原样整库编译成 OpenHarmony KLIB(依赖社区 Compose 鸿蒙线 1.12.0-0.1.0-07),库内新增两个无头门面(headless facade)把 internal 的样式解析引擎提升为 UI-free 公开面,经 @CName C ABI 桥出解析后的样式值(颜色/字号/间距/形状/五态样式),ArkUI Demo 用这些值复刻样式化 UI——每一个取色、取字号、取间距都来自上游真实的 StyleSheet token 链与 ComponentStyleSet 合并逻辑。

项目地址: AtomGit/oh-tpc/compose-stylesheet.git

上游仓库: mori-atsushi/compose-stylesheet

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

compose-stylesheet 为 Compose 应用提供"样式怎么组织"这一件事的完整答案:样式集中声明在 StyleSheet 而不是散落在每个组件调用点;设计令牌(颜色/字体/尺寸)是带名字、带默认值、可被样式表覆写的 Token;组件样式(button / text / surface / divider / topAppBar / icon / iconButton)通过 DSL 按公共样式 + Tag 变体两级定义;themeStyleSheet(useDarkMode) 内置一整套明暗语义色(15 个语义 token + gray/blue 基础色阶);StyleSheet.merge(default, custom) 让应用层只写增量。上游把这些能力全部放在 commonMain(Kotlin 1.9.21 / CMP 1.5.11,Android + 桌面 + iOS + macOS 七目标共享),OpenHarmony 应用同样需要这套样式语义,但上游构建不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画得很像,却无法证明公共样式引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:token 解析链、样式合并语义、组件样式集、Compose 依赖的 ohos 制品、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
没有可隔离内核全部代码是 Compose 语义(Token<Color>、ComponentCommonStyle、@Composable),qrose 式 composeMain 隔离后剩不下任何东西。
工具链代差上游 Kotlin 1.9.21 / Gradle 8.5 / CMP 1.5.11 与鸿蒙工具链差两代;社区 Compose 鸿蒙线 1.12.0-0.1.0-07 需要 Kotlin 2.4.20-0.1.0-07 消费。
依赖矩阵库只依赖 androidx.compose.{runtime,ui,foundation}(无 material3),但每个依赖都要有带 ohosArm64 / ohosX64 变体的社区制品;jvm 交叉验证线另需 Maven Central 的 *-desktop 制品。
解析入口是 @ComposableProvideStyleSheet / StyleSheet.getValue / getStyle 的公开入口全部依赖 composition,OHOS 没有社区 Compose 线的 composition 宿主。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和扁平 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:样式引擎的 ohos 整库编译、根构建替换(cmp_qrose 模式)、无头门面与 C ABI/N-API 桥接层。样式语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和用解析值渲染 ArkUI。

1.2 库提供的能力

上游 compose-stylesheet 0.0.3 的模块划分:

模块能力
compose-stylesheet-coreStyleSheet / StyleSheetBuilder(DSL 与 merge)、Token / ReferenceToken(设计令牌与引用链)、Tag / TagModifier(组件变体)、ComponentStyleSet(公共样式 + Tag 样式集)、ComponentCommonStyle(背景/形状/尺寸/边框/内边距公共样式)、ContentStyle(内容样式,26 个文本字段)、ProvideStyleSheet / LocalStyleSheet(组合作用域)
compose-stylesheet-themethemeStyleSheet(useDarkMode) 默认主题、Colors(black/white + gray50-900 + blue50-900 基础色阶)、SemanticColors(15 个语义 token)、按钮/图标按钮/顶栏/分割线/表面五个组件主题、Surface.background / Surface.primary 表面 Tag
component/compose-stylesheet-buttonButton / IconButton,五态样式(default/pressed/hovered/focused/disabled)、ButtonLayout(图标位/间距)、IndicationStyle
component/compose-stylesheet-textText(基于 BasicText,合并顺序:LocalContentStyle → tag 样式 → 调用参数),31 个样式字段
component/compose-stylesheet-appbarTopAppBar + 布局五间距 + WindowInsets 处理
component/compose-stylesheet-{divider,icon,surface}Divider(粗细/颜色/方向)、Icon(着色)、Surface(背景 + 内容样式传播)
compose-stylesheet伞模块,api 聚合以上全部
sample/android上游 Android 示例(自定义 subText token + primary/secondary 文本 Tag,经 merge 合入主题)

1.3 实现适配

本 fork(0.0.3-ohos.1,rootProject.name = ohos_compose_stylesheet)交付:

  • 8 个库模块在 jvm() + ohosArm64() + ohosX64() 三目标下编译,commonMain 源码除一处 4 行的 API 兼容适配外零改动;
  • 库内仅有的两个新增源文件:core 的 HeadlessStyleSheet.ohos.kt(把 internal 的 StyleSheet.getValue/getStyle/contentStyle 与 ComponentCommonStyle 快照序列化提升为公开 UI-free 面)与 theme 的 ThemeHeadless.ohos.kt(逐行镜像 themeStyleSheet 构建体);
  • example/nativeApp:双 ABI Kotlin/Native sharedLib(libohosstylesheet.so),@CName("StylesheetDispatch") / @CName("StylesheetVersion") 扁平 JSON C ABI;
  • example/harmonyApp:DevEco Stage 工程,CMake N-API 包装(libstylesheet_napi.so)+ 四 Tab ArkUI 验收页面(主题色板 / 组件复刻 / 令牌合并 / 关于日志);
  • 上游 commonTest 零改动跑 jvmTest 交叉验证(24/24 全绿)。

二、实现路线图

阶段内容产出
0 · 查重与定性盘点模块与依赖边界(62+ commonMain 文件、0 个 expect/actual、0 个平台 API、无 material3);确认社区无 compose-stylesheet 鸿蒙仓路线结论:纯 Compose UI 库 → compose-shimmer / eungabi 整库编译路线
1 · 钉工具链Kotlin 2.4.20-0.1.0-07(新线,Gradle 兼容 9.x)+ 社区 Compose 1.12.0-0.1.0-07;eazytec 双仓置顶可解析 ohosArm64/ohosX64 的工具链
2 · 根构建替换上游 settings/build/gradle.properties/toml 存 *.upstream;重写为 cmp_qrose 模式;wrapper 8.5 → 9.6.1只 include 8 库模块 + :example:nativeApp
3 · 库目标与双线依赖8 模块统一 jvm() + ohosArm64() + ohosX64();compose 依赖按 target 线声明(ohosMain 走社区线、jvmMain/jvmTest 走 Maven Central 线)commonMain 整库编译进 ohos klib
4 · 无头门面与桥协议core/theme 各加一个 ohosMain 门面文件;StylesheetDispatch 扁平 JSON 协议(version/theme/resolve 三 op)UI-free 的样式解析公开面
5 · 原生桥接nativeApp 双 ABI sharedLib + copyKnSo 任务;CMake 编 libstylesheet_napi.so(IMPORTED + $ORIGIN rpath + POST_BUILD 拷 so 三件套)ArkTS 可 import 的 NAPI 库
6 · 示例与验证DevEco 四 Tab Demo;JVM 编译/测试交叉验证、ohos klib 编译、双 ABI 链接、ELF 符号检查、真机运行真机样式值与上游逐项一致

三、逐步实现过程

第 1 阶段:项目初始化

先做三件定性的事:

  1. 依赖面盘点:全库 grep import,外部依赖只有 androidx.compose.foundation(layout/shape/interaction/BasicText)、androidx.compose.runtime、androidx.compose.ui(graphics/text/unit/semantics),没有 material3、没有 material-icons(那在 sample 里)、没有任何 expect/actual、没有平台 API。这意味着社区 Compose 线只要发 runtime/ui/foundation 三件 klib 就够用。
  2. 可分离性判断:Token<T> 的值域是 Color/Dp/TextUnit/Shape,ComponentCommonStyle 的字段挂的是 Compose 类型,Text/Button 本体是 @Composable——没有 qrose 式的"纯 Kotlin 编码器",隔离不出内核。路线定性为整库编译 + 无头驱动。
  3. 查重:AtomGit oh-tpc 组织与社区仓中没有 compose-stylesheet 鸿蒙适配,无重复投稿。

第 2 阶段:目标与依赖打通

根构建四件套全部重写(原版存 *.upstream):

// settings.gradle.kts —— eazytec 双仓必须置顶,否则配置期报 Unresolved reference: ohosArm64
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()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories { /* 同上四个,eazytec 在前 */ }
}
rootProject.name = "ohos_compose_stylesheet"
// 8 个库模块 + :example:nativeApp;sample/android 需 AGP,留盘不进构建

版本目录采用双制品线:

[versions]
kotlin = "2.4.20-0.1.0-07"        # HarmonyOS Kotlin 线,提供 ohosArm64()/ohosX64()
compose = "1.12.0-0.1.0-07"       # 社区 Compose 鸿蒙线(ohos klib)
composeJvm = "1.11.0"             # jvm 交叉验证线(Maven Central *-desktop 制品)

[libraries]
compose-runtime      = { module = "org.jetbrains.compose.runtime:runtime",    version.ref = "compose" }
compose-foundation   = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" }
compose-runtime-jvm  = { module = "org.jetbrains.compose.runtime:runtime",    version.ref = "composeJvm" }
compose-foundation-jvm = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeJvm" }

每个库模块的目标与分线依赖(以 core 为例,其余模块同构 + project 依赖):

kotlin {
    applyDefaultHierarchyTemplate()
    jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }  // 交叉验证目标
    ohosArm64()
    ohosX64()                                                     // 适配本体

    sourceSets {
        ohosMain.dependencies { api(libs.compose.runtime); api(libs.compose.foundation) }
        jvmMain.dependencies  { api(libs.compose.runtime.jvm); api(libs.compose.foundation.jvm) }
        commonTest.dependencies { implementation(kotlin("test")) }
        jvmTest.dependencies   { implementation(libs.compose.runtime.jvm); implementation(libs.compose.foundation.jvm) }
    }
}

根构建全局禁用 ohos 测试任务(上游测试统一在 JVM 跑,作为"内核没漂"的闸门):

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

wrapper 从 8.5 升到 9.6.1(与 2.4.20 工具链线的已验证组合一致),gradle.properties 指向本机 JDK 21 并写入 version=0.0.3-ohos.1。

这一步结束时 :compose-stylesheet:compileKotlinJvm 九个模块全绿——但 text 模块暴露出唯一的源码级问题(见 8.1 踩坑 #2):上游按 Compose 1.5.11 的可空签名把 textAlign?.value 直传 TextStyle 构造参数,而 1.6+ 起这些参数是非空 + Unspecified 哨兵。补 4 处 ?: X.Unspecified 兜底与 3 个 import 后双线编译通过,这也是全仓唯一的源码适配。

第 3 阶段:桥协议建模

ArkTS 与 Kotlin 之间只传扁平 UTF-8 JSON 字符串(请求侧与 qrose / compose-shimmer 桥同款约束),不传对象地址;响应侧是手写 JSON,ArkTS 用 JSON.parse 解析。错误永不跨 FFI:任何异常都变成 {"ok":false,"op":...,"error":"..."}。

op请求键响应要点
version—version=0.0.3-ohos.1、upstream、kotlin、compose——版本来自真实 Native 调用(StylesheetVersion()),不是静态文案
themedark、custom、o.<semantic>×15semantic(15 个语义 token 解析值)、palette(22 个基础色阶源 token)、contentColor(sheet 级内容色)
resolvecomponent(text/button/iconButton/surface/divider/appbar/icon)、tags(逗号分隔)、enabled/pressed/hovered/focused、dark/custom/o.*各组件扁平样式快照:颜色(aarrggbb)、字号(sp)、间距/尺寸(dp)、common{...} 公共样式、button/iconButton 的五态

两个关键语义:

  • custom:true 镜像上游 sample/android 的 SampleStyleSheet.kt——自定义 subText token + text(primary)=24sp / text(secondary)=14sp+subText 色,经真实 StyleSheet.merge(theme, custom) 生效;
  • o.<name>:"aarrggbb" 把覆盖实现为额外一张 override sheet 参与 StyleSheet.merge(theme, custom?, override?)——即 token 覆盖走的是上游真实的 ReferenceToken 重绑机制,不是响应改色。

样式表构建入口是 theme 模块的无头门面(themeStyleSheetOf(dark)),逐行镜像 themeStyleSheet(useDarkMode) 的构建体(同一批 token 赋值、同一批组件主题按同一顺序 merge)。

第 4 阶段:原生桥接(技术难点)

无头门面(库模块 ohosMain)——这是本适配的技术核心。core 的解析入口 StyleSheet.getValue/getStyle 是 internal,ComponentCommonStyle 的背景/形状/尺寸/边框字段也是 internal,跨模块不可见;门面必须放进库模块:

// compose-stylesheet-core/src/ohosMain/.../headless/HeadlessStyleSheet.ohos.kt
object StyleSheetHeadless {
    fun <T> resolve(sheet: StyleSheet, token: Token<T>): T = sheet.getValue(token)
    fun <CS : Any, SB : StyleBuilder<CS>> resolveStyle(
        sheet: StyleSheet, component: Component<CS, SB>, tags: List<Tag<CS>> = emptyList(),
    ): CS { /* TagModifier 重建后调 sheet.getStyle —— 与 @Composable 版同一条代码路径 */ }

    fun contentStyle(sheet: StyleSheet): ContentStyle = sheet.contentStyle
    fun backgroundHex(sheet: StyleSheet, style: ComponentCommonStyle): String?
    fun outlineWidth(sheet: StyleSheet, style: ComponentCommonStyle): Float?
    fun snapshot(sheet: StyleSheet, style: ComponentCommonStyle): CommonStyleSnapshot
}

CommonStyleSnapshot 是纯值快照:背景(aarrggbb)、形状(rectangle/rounded/circle + 圆角百分比)、宽高/最小最大尺寸(dp)、四向内边距(dp,按 LTR 解析 start/end)、边框/外描边宽度。

桥模块(example/nativeApp):双 ABI binaries.sharedLib { baseName = "ohosstylesheet" },@CName 导出两个 C 符号;finalizedBy 的 Copy 任务把 libohosstylesheet.so 与 Konan 依赖里对应 triple 的 libc++_shared.so 一起拷进 HAP 工程的 entry/libs/<abi>/。Button 的五态状态合并严格按上游 internal getStyleForState 的应用顺序(focused→hovered→pressed→disabled 依次应用、后者逐字段覆盖)在桥内做回退;IconButton 则直接调用上游公开的 IconButtonStyle.getStyleForState()。

N-API 层(CMake + napi_init.cpp):手写 libohosstylesheet_api.h(只声明 ExportedSymbols 结构与业务符号,不拖生成的整份头);libohosstylesheet.so 以 IMPORTED + IMPORTED_NO_SONAME + $ORIGIN rpath 引入;stylesheet_napi 链接 libace_napi.z.so / libhilog_ndk.z.so / ohosstylesheet 并开 -Wl,--no-undefined;POST_BUILD 把 KN so 与 libc++_shared.so copy 到 NAPI 输出目录。KN 返回的字符串经 napi_create_string_utf8 转成 ArkTS 字符串后必须用 libohosstylesheet_symbols()->DisposeString(raw) 归还 Kotlin 运行时,绝不 free()。

第 5 阶段:ArkUI 宿主

example/harmonyApp 是标准 DevEco Stage 工程(compatibleSdkVersion 5.1.0(18),abiFilters [arm64-v8a, x86_64] 双 ABI,bundle org.terminator.ohos.stylesheet,仓库内 signingConfigs 保持空数组、真机签名由 DevEco 本地生成)。ArkTS 侧只 import NAPI 库:

import { stylesheetDispatch, stylesheetVersion } from 'libstylesheet_napi.so'

const theme = JSON.parse(stylesheetDispatch('{"op":"theme","dark":true}'))
const bg = '#' + theme.semantic.background          // dark: '#090a0c'
const primary = JSON.parse(stylesheetDispatch(
  '{"op":"resolve","component":"text","tags":"primary","custom":true}'))
// primary.fontSize == 24(来自上游 sample 同一条 merge 语义)

四个 Tab 的验收面:

Tab内容
主题15 个语义色块 + gray/blue 色阶(22 块)全部由桥解析值渲染;明暗开关触发全量重解析
组件TopAppBar(背景/64dp/20sp 标题)、primary/secondary 文本 Tag、Divider、Button(药丸圆角/18sp/最小 40dp)与五态行(focused 带 3dp 焦点环)、IconButton 圆形五态;每个组件旁标注桥返回的真实数值
令牌custom 样式表开关(primary=24sp/secondary=14sp+subText);background/inverseSurface/focus 三个语义 token 的色片覆盖,全页联动
关于版本握手、最近一次请求/响应原文、AtomGit 仓库与社区链接

页面入口做了防御性解析:桥返回 ok:false 或字段缺失时顶部显示红色错误横幅(可复制),而不是闪退——这本身也是排障工具(见 8.1 #12)。

第 6 阶段:示例与验证

# JVM 交叉验证:上游 commonTest 零改动
.\gradlew.bat :compose-stylesheet-core:jvmTest :component:compose-stylesheet-text:jvmTest
# → 24 个测试 0 失败(core 22 + text 2)

# ohos klib 编译 + 双 ABI 动态库(自动拷入 entry/libs/<abi>/)
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# → arm64 ≈30MB / x86_64 ≈28MB(Compose 运行时静态链接)

随后 DevEco 打开 example/harmonyApp 构建 HAP、真机安装运行,逐项核对样式值(见六、测试与验证)。

四、完整代码对照

4.1 整体架构

ArkTS Index.ets(四 Tab Demo)
   │ 扁平 JSON 字符串
   ▼
libstylesheet_napi.so(CMake N-API 包装,$ORIGIN rpath)
   │ const char* StylesheetDispatch(const char*)
   ▼
libohosstylesheet.so(Kotlin/Native sharedLib,双 ABI)
   │ StyleSheetHeadless(core 门面)/ themeStyleSheetOf(theme 门面)
   ▼
上游 StyleSheet token 链 + ComponentStyleSet 合并 + themeStyleSheet 构建体
   │ 解析值快照
   ▼
{"ok":true, "semantic":{...}, "common":{...}, "states":{...}}

4.2 文件清单

类别文件说明
重写(原版存 .upstream)settings.gradle.kts、build.gradle.kts、gradle.properties、gradle/libs.versions.toml + 9 个模块 build.gradle.ktscmp_qrose 模式根构建、双线版本目录、jvm+ohos 三目标
升级gradle/wrapper/*8.5 → 9.6.1
库内新增(仅两个源文件)compose-stylesheet-core/src/ohosMain/.../headless/HeadlessStyleSheet.ohos.kt(211 行)无头解析门面 + 快照序列化
compose-stylesheet-theme/src/ohosMain/.../ThemeHeadless.ohos.kt(50 行)主题镜像构建
桥模块example/nativeApp/build.gradle.kts + src/ohosMain/.../Bridge.kt(514 行)@CName + MiniJson + 三 op
NAPIentry/src/main/cpp/{CMakeLists.txt, include/libohosstylesheet_api.h, napi_init.cpp, types/libstylesheet_napi/*}C ABI 声明、N-API 胶水、d.ts
ArkUIentry/src/main/ets/{entryability, pages/Index.ets}(798 行)四 Tab Demo + 防御性解析
文档README.md / README.OpenHarmony.md / README.OpenHarmony_CN.md / README.upstream.md中文首页、英文摘要、中文主文档、上游原文
唯一源码适配component/compose-stylesheet-text/.../Text.kt4 处可空参数 ?: Unspecified + 3 import(Compose 1.6+ 参数非空化)

4.3 关键 API 对照

上游 API桥内调用点响应字段
themeStyleSheet(useDarkMode) 构建体themeStyleSheetOf(dark)theme.semantic.*、theme.contentColor
StyleSheet.getValue(token)(internal)StyleSheetHeadless.resolve所有颜色/字号/间距数值
StyleSheet.getStyle(component, tags)(internal)StyleSheetHeadless.resolveStyle各 resolve 响应主体
StyleSheet.merge(...)buildSheet()(theme+custom+override)custom / o.* 的联动效果
Colors.semantic.*(15 token)semanticTokens 映射表theme.semantic.*
Colors.{black,white,gray*,blue*}(22 token)paletteTokens 映射表theme.palette.*
Tag("primary", text) / TagModifier +桥内按逗号重建resolve(component=text, tags="primary")
Surface.background / Surface.primary字面量匹配resolve(component=surface, tags=...)
ButtonStyle 五态 + internal getStyleForState桥内同序逐字段回退button.states.* / button.requested
IconButtonStyle.getStyleForState(public)直接调用上游iconButton.states.*
ComponentCommonStyle(internal 字段)StyleSheetHeadless.snapshot各响应 common{...}

4.4 ArkTS 与 Kotlin 的边界

  • 只传扁平 UTF-8 JSON:请求侧手写 MiniJson(引用感知的逗号切分),响应侧 JSON.parse;Kotlin 对象地址、@Composable lambda、Indication、Brush 一律不过桥。
  • 颜色统一 aarrggbb 8 位十六进制;长度 dp、字号 sp 均为数值。
  • TextUnit 只序列化 sp(Em 忽略);BrushStyle 渐变不可内省,边框/外描边只报宽度;padding 按 LTR 解析。
  • 每个导出函数整体 try/catch,异常走 {"ok":false,...} 通道;KN 字符串 DisposeString 归还。

五、关键决策说明

决策 1:整库编译(shimmer/eungabi 路线)而不是内核抽取(qrose 路线)

库的类型决定路线。compose-stylesheet 没有可隔离的纯 Kotlin 内核——隔离后剩不下任何东西;整库编译才能证明上游代码在 ohos 上是真的。代价是 so 体积(≈30MB,Compose 运行时静态链接),与 compose-shimmer(31.4MB)同量级。

决策 2:工具链钉 Kotlin 2.4.20-0.1.0-07 + Gradle 9.6.1

社区 Compose 1.12.0-0.1.0-07 的 klib 需要 2.4.20 线消费;该线兼容 Gradle 9.x(旧线 2.2.21 必须配 8.14.1,且读不了 2.4 元数据)。上游 wrapper 8.5 与新线不匹配,升 9.6.1(shimmer 已验证组合)。

决策 3:Compose 依赖按 target 双线声明

社区线不发 *-desktop 子制品,jvm 交叉验证线必须走 Maven Central。理想中的"换线不换版"(jvm 也用上游 1.5.11)不可行:1.5.11(Kotlin 1.9 时代)的 TextStyle 签名与 metadata 无法被 2.4.20 compose compiler 消费。取 1.11.0——同代、有 desktop 制品,并在文档声明这个偏差。两线编译的是同一份 commonMain,"内核没漂"由 jvmTest 闸门保证。

决策 4:无头门面放库模块 ohosMain 而不是桥模块

getValue/getStyle 与 ComponentCommonStyle 的字段是 core 的 internal,同模块才可见——这与 compose-shimmer 的 ShimmerHeadless 同款;桥模块因此保持零库内知识。theme 的 themeStyleSheetOf 逐行镜像 themeStyleSheet 构建体,上游改主题时必须同步镜像(README.OpenHarmony_CN.md 改动表已声明)。

决策 5:Button 状态合并在桥内逐字段回退,IconButton 直接用上游公开 API

ButtonStyle.getStyleForState 是 internal、IconButtonStyle.getStyleForState 是 public。桥内回退严格按上游"focused→hovered→pressed→disabled 依次应用、后者覆盖"的顺序(disabled→pressed→hovered→focused→default 首个非空字段胜出)实现;布局类字段取 default 态(主题只在 default 定义布局,行为等价)。

决策 6:jvmTest 交叉验证 + 测试零改动 + 手写 MiniJson

上游 commonTest(StyleSheetTest/ContentStyleTest/ComponentStyleTest/ComponentStyleBuilderTest/BorderSetterTest/TextStyleTest)零改动跑在 jvm target,24/24 全绿;若 jvm 全绿而 ohos 行为异常,问题只能在门面/桥层。MiniJson 手写(qrose/KStore/shimmer 同款),不把 kotlinx.serialization 插件与运行时拖进 KN so。

六、测试与验证

6.1 测试环境

项值
构建机Windows 11 x64,JDK 21(Temurin),Gradle 9.6.1
Kotlin / Compose2.4.20-0.1.0-07 / ohos 线 1.12.0-0.1.0-07(jvm 线 1.11.0)
设备OpenHarmony/HarmonyOS 真机(arm64),DevEco Studio 构建 HAP
制品源maven.eazytec-cloud.com(社区 Maven)+ Maven Central

6.2 JVM 交叉验证

:compose-stylesheet-core:jvmTest          22 tests, 0 failures
:component:compose-stylesheet-text:jvmTest  2 tests, 0 failures

九个模块 compileKotlinJvm 全绿;ohos 侧 compileKotlinOhosArm64 / compileKotlinOhosX64(含两个门面)全绿。

6.3 HAP 验证

  • linkDebugSharedOhosArm64 / OhosX64 成功,libohosstylesheet.so(arm64 ≈30MB / x64 ≈28MB)与 libc++_shared.so 三件套自动落入 entry/libs/<abi>/;
  • ELF 动态符号表双 ABI 均确认导出 StylesheetDispatch / StylesheetVersion / libohosstylesheet_symbols;
  • KN so 的 DT_NEEDED 依赖清单与已真机验证的 compose-shimmer 完全一致(22 项);
  • DevEco 构建 HAP、真机安装运行成功。

6.4 功能用例

用例期望结果
version 握手StylesheetVersion() 返回 0.0.3-ohos.1✅
light 语义色background=ffffffff、onBackground=ff090a0c(gray900)、focus=ff54aeff(blue300)、primarySurface=ffeceeef(gray100)✅ 真机逐项一致
dark 语义色background=ff090a0c、onBackground=ffffffff、divider=ff2d3135(gray700)、focus=ff0969da(blue500)✅
文本 Tag(custom)primary=24sp、secondary=14sp+subText 色✅
Button药丸圆角 50%、内容 18sp、最小 40dp、五态背景随 inverseSurface 系变化✅
IconButton圆形 40dp、五态色、focused 3dp 焦点环✅
TopAppBar背景=background token、minHeight=64dp、标题 20sp✅
o.inverseSurface 覆盖按钮五态底色全页联动(真实 ReferenceToken 重绑)✅

6.5 验证结论

样式引擎(token 解析、Tag 合并、五态合并、主题构建)在 OpenHarmony arm64 真机上完成编译、链接、执行;真机解析值与上游同参数推算逐项一致;上游测试在 jvm 零改动全绿,证明 commonMain 内核没有漂移。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

# JVM 编译 + 交叉验证测试
.\gradlew.bat :compose-stylesheet-core:jvmTest :component:compose-stylesheet-text:jvmTest

# ohos klib
.\gradlew.bat :compose-stylesheet:compileKotlinOhosArm64 :compose-stylesheet:compileKotlinOhosX64

# 双 ABI so(自动拷入 harmonyApp/entry/libs/<abi>/)
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# HAP(DevEco 工具链,example/harmonyApp 目录)
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

# 真机
hdc -t <序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <序列号> shell aa start -a EntryAbility -b org.terminator.ohos.stylesheet -m entry

八、遗留问题与改进方向

8.1 踩坑复盘

#坑对策
1eazytec 双仓不置顶 → 配置期 Unresolved reference: ohosArm64pluginManagement 与 dependencyResolutionManagement 都把社区 Maven 放最前
2Compose 1.6+ 把 TextStyle 的 textAlign/textDirection/lineBreak/hyphens 改为非空参数(Unspecified 哨兵),上游 1.5.11 可空签名直传 → 双线编译失败4 处 ?: X.Unspecified 兜底 + 3 import,语义等价(两代 API 的"未设置");全仓唯一源码适配
3String.format 在 Kotlin/Native 不存在颜色格式化用 toUInt().toString(16).padStart(8,'0')
4CircleShape/RoundedCornerShape 在 foundation.shape 而非 ui.graphics;Color.isSpecified 与 Dp.isSpecified 是两个不同包的扩展门面 import 逐个核对(graphics vs unit vs foundation.shape)
51.12 线 RoundedCornerShape 的角是 CornerSize,底层 value 不可公开内省药丸用与 RoundedCornerShape(50) 的结构相等性识别,其余 rounded 报 null、Demo 回退 50%
6paddingValues 有自定义 getter 不能智能转换局部变量承接后再判空
7桥模块读 commonStyle/indication 报 “This API is for building a component”库的 @StyleSheetComponentApi opt-in 要求——nativeApp compilerOptions.optIn 补声明;getStyleForState、Surface.background/primary 等扩展成员需显式 import
8Button 响应 "background":ff090a0c 缺引号 → 非法 JSON → ArkTS 解析抛异常闪退门面 backgroundHex 返回裸 hex,桥侧补 q() 包引号;ArkTS 侧全部解析走防御性访问器,错误显示为红色横幅而不是崩溃
9ArkTS tabIndex 是 CustomComponent 基类保留属性名;scrollBarDisplayMode/BarDisplayMode 在当前 SDK 不存在状态改名 currentTab;删除该属性用默认滚动条行为
10HAP 缺当前 ABI 的 NAPI so → 安装失败 9568347abiFilters 双 ABI + copy 任务三件套;模拟器 x86_64、真机 arm64 缺谁都装不上
11KN 字符串跨 N-API 泄漏napi_create_string_utf8 后 DisposeString 归还,绝不 free()
12signingConfigs 填一半被 hvigor 拒绝(缺 material)仓库内保持空数组;真机签名由 DevEco 本地生成,证书/p12 绝不进仓库

8.2 已知问题

  • OHOS 无社区 Compose 线的 composition 宿主:ProvideStyleSheet 与组件 @Composable 本体无法在鸿蒙进程内运行,交付形态是 klib + so + 样式值桥(社区共识)。
  • BrushStyle 渐变不可内省(快照只报描边宽度,焦点环颜色由语义 token focus 单独取);TextUnit 仅 sp;padding 按 LTR。
  • 上游 icon 模块 namespace 复用了 ...stylesheet.text(上游瑕疵),本 fork 保持原样。
  • sample/android、.github/workflows 为上游遗留,未改造不进构建。

8.3 未来优化方向

  • 等社区 composition 宿主落地后,ProvideStyleSheet + 组件本体直接上真机(klib 已就绪)。
  • 门面协议若被业务侧广泛使用,评估抽成 .har(NAPI so + d.ts)发布。
  • 跟进上游 API 变化:每次同步上游时 ThemeHeadless.ohos.kt 必须与 ThemeStyleSheet.kt 逐行对照。
  • Demo 补充 uitest 自动化截图脚本(对齐社区仓做法)。

九、总结

9.1 核心难点回顾

  1. 路线定性:识别"纯 Compose UI 库没有可隔离内核",选整库编译 + 无头驱动路线,避免把适配做成"重写一个样式库"。
  2. 双制品线:社区 ohos 线与 Maven Central jvm 线并存,"换线不换版"不可行时如何取最接近版本并守住交叉验证闸门。
  3. 无头门面:把 internal 解析引擎提升为 UI-free 公开面,同时保证与 @Composable 入口走同一条代码路径。
  4. 状态合并保真:Button 五态在桥内按上游应用顺序逐字段回退,IconButton 直接用上游公开 API——语义等价可论证。
  5. 两代 Compose 签名差:4 行 ?: Unspecified 兜底是全仓唯一源码适配,语义等价且已记入改动表。

9.2 封装层次

上游 commonMain(样式语义,唯一真源)
  ↑ 整库编译
ohos klib(ohosArm64/ohosX64)
  ↑ 同模块门面(HeadlessStyleSheet / ThemeHeadless)
@CName C ABI(StylesheetDispatch/StylesheetVersion,扁平 JSON)
  ↑ CMake N-API(libstylesheet_napi.so)
ArkTS Demo(解析值渲染 + 防御性解析)

9.3 三条经验

  1. 先定性再动工:库的类型(有内核 / 纯逻辑 / 纯 UI)决定路线,qrose / KStore / compose-shimmer 三条路线不要混用。
  2. 交叉验证是"内核没漂"的唯一可信证明:jvm 与 ohos 编译同一份 commonMain,上游测试零改动全绿才有说服力。
  3. 把错误通道做厚:FFI 边界整体 try/catch + ArkTS 防御性解析 + 可复制的错误横幅,让真机排障从"闪退看日志"变成"屏幕上读错误"。

9.4 适配成果

项值
交付源码 + ohos klib(双 ABI)+ libohosstylesheet.so / libstylesheet_napi.so + HAP Demo
版本0.0.3-ohos.1(上游 0.0.3,Apache-2.0)
源码改动仅 Text.kt 4 行兜底 + 3 import;库内新增 2 个门面文件;上游测试零改动 24/24
验证JVM 编译/测试 ✅、ohos klib ✅、双 ABI 链接 + ELF 符号 ✅、真机运行与数值核对 ✅
文档README 四件套(中文主文档 20541 字)+ 本文
仓库https://atomgit.com/oh-tpc/compose-stylesheet

适配过程 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐