开源鸿蒙平台 CMP 三方库「compose-stylesheet」适配全流程
本文记录
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 公开面,经@CNameC ABI 桥出解析后的样式值(颜色/字号/间距/形状/五态样式),ArkUI Demo 用这些值复刻样式化 UI——每一个取色、取字号、取间距都来自上游真实的StyleSheettoken 链与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 制品。 |
| 解析入口是 @Composable | ProvideStyleSheet / 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-core | StyleSheet / StyleSheetBuilder(DSL 与 merge)、Token / ReferenceToken(设计令牌与引用链)、Tag / TagModifier(组件变体)、ComponentStyleSet(公共样式 + Tag 样式集)、ComponentCommonStyle(背景/形状/尺寸/边框/内边距公共样式)、ContentStyle(内容样式,26 个文本字段)、ProvideStyleSheet / LocalStyleSheet(组合作用域) |
compose-stylesheet-theme | themeStyleSheet(useDarkMode) 默认主题、Colors(black/white + gray50-900 + blue50-900 基础色阶)、SemanticColors(15 个语义 token)、按钮/图标按钮/顶栏/分割线/表面五个组件主题、Surface.background / Surface.primary 表面 Tag |
component/compose-stylesheet-button | Button / IconButton,五态样式(default/pressed/hovered/focused/disabled)、ButtonLayout(图标位/间距)、IndicationStyle |
component/compose-stylesheet-text | Text(基于 BasicText,合并顺序:LocalContentStyle → tag 样式 → 调用参数),31 个样式字段 |
component/compose-stylesheet-appbar | TopAppBar + 布局五间距 + 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 阶段:项目初始化
先做三件定性的事:
- 依赖面盘点:全库 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 就够用。 - 可分离性判断:
Token<T>的值域是Color/Dp/TextUnit/Shape,ComponentCommonStyle的字段挂的是 Compose 类型,Text/Button本体是@Composable——没有 qrose 式的"纯 Kotlin 编码器",隔离不出内核。路线定性为整库编译 + 无头驱动。 - 查重: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()),不是静态文案 |
theme | dark、custom、o.<semantic>×15 | semantic(15 个语义 token 解析值)、palette(22 个基础色阶源 token)、contentColor(sheet 级内容色) |
resolve | component(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——自定义subTexttoken +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.kts | cmp_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 |
| NAPI | entry/src/main/cpp/{CMakeLists.txt, include/libohosstylesheet_api.h, napi_init.cpp, types/libstylesheet_napi/*} | C ABI 声明、N-API 胶水、d.ts |
| ArkUI | entry/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.kt | 4 处可空参数 ?: 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 对象地址、@Composablelambda、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 / Compose | 2.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 踩坑复盘
| # | 坑 | 对策 |
|---|---|---|
| 1 | eazytec 双仓不置顶 → 配置期 Unresolved reference: ohosArm64 | pluginManagement 与 dependencyResolutionManagement 都把社区 Maven 放最前 |
| 2 | Compose 1.6+ 把 TextStyle 的 textAlign/textDirection/lineBreak/hyphens 改为非空参数(Unspecified 哨兵),上游 1.5.11 可空签名直传 → 双线编译失败 | 4 处 ?: X.Unspecified 兜底 + 3 import,语义等价(两代 API 的"未设置");全仓唯一源码适配 |
| 3 | String.format 在 Kotlin/Native 不存在 | 颜色格式化用 toUInt().toString(16).padStart(8,'0') |
| 4 | CircleShape/RoundedCornerShape 在 foundation.shape 而非 ui.graphics;Color.isSpecified 与 Dp.isSpecified 是两个不同包的扩展 | 门面 import 逐个核对(graphics vs unit vs foundation.shape) |
| 5 | 1.12 线 RoundedCornerShape 的角是 CornerSize,底层 value 不可公开内省 | 药丸用与 RoundedCornerShape(50) 的结构相等性识别,其余 rounded 报 null、Demo 回退 50% |
| 6 | paddingValues 有自定义 getter 不能智能转换 | 局部变量承接后再判空 |
| 7 | 桥模块读 commonStyle/indication 报 “This API is for building a component” | 库的 @StyleSheetComponentApi opt-in 要求——nativeApp compilerOptions.optIn 补声明;getStyleForState、Surface.background/primary 等扩展成员需显式 import |
| 8 | Button 响应 "background":ff090a0c 缺引号 → 非法 JSON → ArkTS 解析抛异常闪退 | 门面 backgroundHex 返回裸 hex,桥侧补 q() 包引号;ArkTS 侧全部解析走防御性访问器,错误显示为红色横幅而不是崩溃 |
| 9 | ArkTS tabIndex 是 CustomComponent 基类保留属性名;scrollBarDisplayMode/BarDisplayMode 在当前 SDK 不存在 | 状态改名 currentTab;删除该属性用默认滚动条行为 |
| 10 | HAP 缺当前 ABI 的 NAPI so → 安装失败 9568347 | abiFilters 双 ABI + copy 任务三件套;模拟器 x86_64、真机 arm64 缺谁都装不上 |
| 11 | KN 字符串跨 N-API 泄漏 | napi_create_string_utf8 后 DisposeString 归还,绝不 free() |
| 12 | signingConfigs 填一半被 hvigor 拒绝(缺 material) | 仓库内保持空数组;真机签名由 DevEco 本地生成,证书/p12 绝不进仓库 |
8.2 已知问题
- OHOS 无社区 Compose 线的 composition 宿主:
ProvideStyleSheet与组件 @Composable 本体无法在鸿蒙进程内运行,交付形态是 klib + so + 样式值桥(社区共识)。 BrushStyle渐变不可内省(快照只报描边宽度,焦点环颜色由语义 tokenfocus单独取);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 核心难点回顾
- 路线定性:识别"纯 Compose UI 库没有可隔离内核",选整库编译 + 无头驱动路线,避免把适配做成"重写一个样式库"。
- 双制品线:社区 ohos 线与 Maven Central jvm 线并存,"换线不换版"不可行时如何取最接近版本并守住交叉验证闸门。
- 无头门面:把 internal 解析引擎提升为 UI-free 公开面,同时保证与 @Composable 入口走同一条代码路径。
- 状态合并保真:Button 五态在桥内按上游应用顺序逐字段回退,IconButton 直接用上游公开 API——语义等价可论证。
- 两代 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 三条经验
- 先定性再动工:库的类型(有内核 / 纯逻辑 / 纯 UI)决定路线,qrose / KStore / compose-shimmer 三条路线不要混用。
- 交叉验证是"内核没漂"的唯一可信证明:jvm 与 ohos 编译同一份 commonMain,上游测试零改动全绿才有说服力。
- 把错误通道做厚: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。
参考文档
更多推荐



所有评论(0)