本文记录 ComposeTheme 接入 OpenHarmony 的完整过程,覆盖设计系统 API 库盘点、鸿蒙 Compose 制品线选型、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

ComposeTheme 是 Compose Multiplatform 的"设计系统 API"库(上游 composablehorizons/ComposeTheme,MIT):buildComposeTheme {} 返回一个主题 @Composable,ComposeTheme 对象在任意后代里按 DesignToken 取值,ThemeBuilder DSL 支持 colors / textStyles / shapes 三组默认属性(10 色系 × 10 明度共 100 个颜色令牌、9 档文字样式、6 档形状)、任意类型的自定义 DesignProperty、extend {} 包装既有设计系统,以及 composetheme-material3 / composetheme-material 两个桥模块(extendMaterial3 / extendMaterial 把 Material 3 / Material 2 的 colorScheme、typography、shapes 映射进同一套令牌体系)。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别:社区 Maven 上已经出现完整的 HarmonyOS Compose 制品线(org.jetbrains.compose.* 1.12.0-0.1.0-07,runtime / ui / foundation / material / material3 / animation 全部带 ohosArm64 / ohosX64 klib 变体),而本库上游基于 Compose 1.8.1 构建、全库零 expect/actual、零平台源码集,因此不需要提取内核也不需要剥离 Compose——三个模块的全部 commonMain(buildComposeTheme 主题解析、ThemeBuilder DSL、默认令牌表、DefaultDebugIndication、Material 2 / Material 3 桥)被原样编译成 OpenHarmony KLIB。OHOS 目前没有 Compose composition 宿主,@Composable 取值访问器无法在 ArkTS 侧挂载,适配按照 compose-shimmer 先例为库内可无头驱动的真实对象(ThemeBuilder、令牌合并、ResolvedTheme 属性存储、M2/M3 桥)开出 JSON 门面,ArkUI 页面作为验收宿主。

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

上游仓库: composablehorizons/composetheme

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

ComposeTheme 为 Compose Multiplatform 应用提供"设计系统 API"这一件事的完整答案:buildComposeTheme { } 一行建主题,返回的 @Composable 包住 App 后,任意后代用 ComposeTheme.colors.red500 / ComposeTheme.textStyles.base / ComposeTheme.shapes.round 这样的扩展属性取默认令牌,用 ComposeTheme.colors[token] / ComposeTheme[property] 取自定义令牌与设计属性,extend { } 挂接任意既有设计系统,extendMaterial3 { colorScheme = lightColorScheme(primary = Color.Red) } 一段配置把 Material 3 的整套 colorScheme / typography / shapes 收编进同一套 API。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套主题基础设施,但上游构建(Compose 1.8.1 / Kotlin 2.1.21 / AGP 8.2 / Gradle 8.7)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示色块,却无法证明公共主题引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证主题解析引擎、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Compose 1.8.1 / Kotlin 2.1.21 / AGP 8.2(androidTarget + ios framework + js/wasmJs)与鸿蒙工具链无法共存于一个 Gradle 构建;根构建还有 Sonatype 发布与 dokka。
依赖矩阵库依赖 compose.foundation,桥模块依赖 compose.material / compose.material3,每一个都要有 ohos klib 变体才能解析。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
@Composable 跨不了桥ComposeTheme.colors[token] 等取值入口都是 @Composable,OHOS 没有 composition 宿主,无法在 ArkTS 侧直接调用。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:三个库模块全部 commonMain 的 ohos 编译、根构建替换(cmp_qrose 模式)和无头门面 + C ABI/N-API 桥接层。令牌语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的主题解析会话,对应桥接的全部入口:

  • 默认令牌快照(defaults):读取真实 DefaultComposeTheme——100 个颜色令牌(10 色系 × 10 明度)、9 档文字样式(xs 12sp 到 xl5 48sp)、6 档形状(round 4dp 到 full 100%)的计数与抽样值;
  • DSL 驱动(builder):真实构造 ThemeBuilder,设置名称、经真实 DefaultDebugIndication 工厂创建指示器节点、用真实 DesignTokens 工厂装自定义令牌、注册自定义 DesignProperty,并调用真实 buildComposeTheme 工厂入口;
  • 合并语义(merge):执行 buildComposeTheme 内部那三行 DesignTokens(name, builder.X.entries + DefaultComposeTheme.X.entries) 合并表达式——上游 README 的 WARNING(默认令牌无法被覆盖)在设备上肉眼可验证:覆盖 red500 解析回默认值 #FFFF4C4C,自定义 primary 正常通过;自定义属性经真实 ResolvedTheme.getProperty 属性存储回读;
  • 错误路径(missing):复现 DesignTokens.get @Composable 访问器抛出的缺失令牌报错原文(含主题名);
  • Material 桥(extend3 / extend):真实执行 extendMaterial3 / extendMaterial(两者本身是普通函数,无需 composition),读回映射后的 M3 五档形状(4/8/12/16/28dp)、真实 lightColorScheme 色板、Typography() 的 bodyLarge/titleLarge/labelSmall,以及 M2 的十二色令牌与 small/medium/large 形状;
  • 版本握手(version):themeVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op回显本次操作。
各 op 专属字段defaults 的计数与抽样、builder 的指示器与属性、merge 的 resolved/missing、extend3 / extend 的色板与形状等。

引擎侧完整能力(buildComposeTheme 返回的 composable、全部 @Composable 扩展属性、MutableThemeProperties、DefaultDebugIndication 的 Modifier.Node 实现、M2/M3 桥注册的 MaterialTheme 包装)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留三个模块对上游制品线(compose 1.8.1)的编译交叉验证,证明 ohos .so 里执行的解析逻辑没有漂移。

1.3 实现适配

维度要求
代码复用composetheme / composetheme-material / composetheme-material3 的全部 commonMain 文件原样编译,不提取、不重写、零 expect/actual 需要补;新增源码只有三个 ohosMain 无头门面。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);samples(desktop 专用样例)留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律jvm 交叉验证线版本主体与上游完全一致(compose 1.8.1),ohos 线走社区 Compose 制品线;上游源码零改动。
UI 完整四 Tab 覆盖全部桥接入口:令牌 / 构建合并 / Material / 关于。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 尚无 Compose composition 宿主,@Composable API 已编入 so 但无挂载点;ArkTS Demo 把桥暴露的主题解析闭环画出来,数值全部来自 Kotlin 侧真实对象。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认社区 Compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、分线依赖
第 3 阶段:桥协议建模     ── 7 个 op 的 JSON 契约、key=value 列表字段、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 四 Tab 页面、真机数值呈现、错误边界
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
composetheme/                          上游核心引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
composetheme/src/ohosMain/             新增:ComposeThemeHeadless 门面 + 跨模块辅助函数
composetheme-material/                 上游 Material 2 桥(进 ohos 构建,ohosMain 加门面)
composetheme-material3/                上游 Material 3 桥(进 ohos 构建,ohosMain 加门面)
samples/ mkdocs.yml scripts/ art/      上游 desktop 样例与文档站,留盘不进 settings
example/nativeApp/                     Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :composetheme、:composetheme-material、:composetheme-material3 与 :example:nativeApp,rootProject.name = "ohos_composetheme"。上游的 AGP / dokka / nexus 发布插件、androidTarget、js/wasmJs、iOS framework 全部随根构建替换剥离——本 fork 不发 Maven Central。上游构建文件原样保存为 *.upstream(settings / 根与模块 build / catalog / properties / wrapper 七份)。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 钉死本机路径)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.6.1(上游 wrapper 8.7 升级;2.4.20 线支持 Gradle 9.x,社区先例 compose-shimmer / KStore 同款)
Compose 制品线社区 org.jetbrains.compose.* 1.12.0-0.1.0-07(ohos klib:runtime / ui / foundation / material / material3 / animation)
jvm 交叉验证线上游自洽线 compose 1.8.1(Maven Central,即上游 1.4.0 实际编译所用的版本体)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.composetheme
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
composetheme3compose.foundation(runtime/ui 传递),零平台 API整体编译(jvm + 双 ohos)
composetheme-material1foundation + compose.material + project(:composetheme)整体编译(零平台源码集)
composetheme-material31foundation + compose.material3 + project(:composetheme)整体编译(零平台源码集)
samples7(desktopMain)compose.desktop.currentOs 等 *-desktop 子制品留盘不进构建

expect 声明全库零处:三个模块只有 commonMain,没有 androidMain / iosMain / jvmMain,没有 import android.* / import platform.*,唯一的非 Compose 依赖是 kotlinx.coroutines.launch(经 foundation 传递、common 通用 API)。这意味着 ohos 侧不需要补任何 actual——适配面从"补平台缝"变成"给可无头驱动的内部对象开门面"。唯一的新增源码是三个 ohosMain 门面(见第 3 阶段)。

值得单独说明的是取值入口的形态:DesignTokens.get / ComposeTheme.get 都是 @Composable @ReadOnlyComposable,但它们底层调用的对象全部可以在 composition 外构造和驱动——ThemeBuilder(DSL 状态)、DesignTokens(令牌映射)、DefaultComposeTheme(默认表)、ResolvedTheme.getProperty(ComposeTheme.get 实际调用的非 composable 属性存储)、DefaultDebugIndication(真实 IndicationNodeFactory,create(MutableInteractionSource()) 不需要 composition)、extendMaterial3 / extendMaterial(本来就是普通函数)。这是无头门面路线成立的前提。

1.4 页面边界
Tab 切换        令牌 / 构建合并 / Material / 关于
令牌 Tab        真实 DefaultComposeTheme 快照:三组计数、6 个抽样色块、9 档文字样式表、6 档形状预览
构建合并 Tab    主题名 / 自定义 token / token 值输入区;builder op 结果行;
                merge op 结果(resolved 来源标注 + 被默认覆盖的尝试值 + missing);
                缺失 token 报错文案按钮与展示
Material Tab    M3:primary 输入 + 六色板 + 五档形状 + 三个文字样式;M2:五色板 + 三档形状 + body1/h1
关于 Tab        版本握手(真实 Native 调用)、工具链表、调用链分层、已知限制、仓库与社区链接

页面启动时先调 themeVersion() 做握手,再依次拉取 defaults / builder / merge / extend3 / extend 快照填充各 Tab。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 Compose 制品。用 .module 元数据逐个验证:

org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosArm64SourcesElements-published / ohosX64…
org.jetbrains.compose.material3:material3:1.12.0-0.1.0-07
  variants: 同上,依赖闭包含 material / foundation / runtime / ui / animation
org.jetbrains.compose.material:material:1.12.0-0.1.0-07
  variants: 同上

三个关键结论:其一,库需要的四件(foundation / material / material3 + 传递的 runtime / ui)在社区线上全部存在 ohosArm64 / ohosX64 变体——这是纯 Compose UI 库能走"真编译整库"路线的前提,qrose 时代(上游 Compose 1.12 / Kotlin 2.4 与社区线对不齐)只能把 Painter 剥进不编译的 composeMain;其二,社区线不发布 *-desktop 子制品,jvm 交叉验证目标不能从这条线解析,必须像 compose-shimmer / eungabi / AdaptiveStack 一样按目标分线;其三,上游 1.4.0 基于 Compose 1.8.1 构建,社区线版本体 1.12 更新,但库只用 foundation/material[3] 的稳定 API(IndicationNodeFactory、Modifier.Node、staticCompositionLocalOf 等),两线编译都绿即 API 面兼容的证据。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但社区 Compose 线 1.12.0-0.1.0-07 的依赖闭包钉在 stdlib 2.3.20 / Kotlin 工具链 2.4.20-0.1.0-07(material3 的 .module 里 requires 字段可见),旧线读不了新元数据。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:上游源码本来就是 Kotlin 2.1.21 编的,2.4.20 向后兼容;Gradle 从上游 8.7 升到 9.6.1(compose-shimmer / KStore 先例,2.4.20 线支持 Gradle 9.x),不像 2.2.21 旧线必须降 8.14.1。

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_composetheme"

include(":composetheme")
include(":composetheme-material")
include(":composetheme-material3")
include(":example:nativeApp")
2.3 编译真实引擎

composetheme/build.gradle.kts 收敛为纯 KMP 模块,Compose 依赖按目标线声明:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.compose.compiler)   // commonMain 有 @Composable,必须挂编译器插件
}

kotlin {
    applyDefaultHierarchyTemplate()

    jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }   // 交叉验证线

    ohosArm64()    // the adaptation
    ohosX64()

    sourceSets {
        ohosMain.dependencies {
            implementation(libs.compose.runtime)      // 社区线 1.12.0-0.1.0-07
            implementation(libs.compose.foundation)
        }
        jvmMain.dependencies {
            implementation(libs.compose.runtime.jvm)  // 上游线 1.8.1
            implementation(libs.compose.foundation.jvm)
        }
    }
}

composetheme-material / composetheme-material3 同构,分别加 compose.material / compose.material3 与 project(":composetheme")。依赖放 ohosMain / jvmMain 而不是 commonMain,是因为同一坐标在两条线上版本不同——“线切换,不是版本切换”。实测:

./gradlew :composetheme:compileKotlinOhosArm64 :composetheme:compileKotlinOhosX64          # BUILD SUCCESSFUL
./gradlew :composetheme-material3:compileKotlinOhosArm64 :composetheme-material:compileKotlinOhosArm64  # BUILD SUCCESSFUL

三个模块的全部 commonMain 文件一个没动。调试期踩过的三个编译错误都出在新增的 ohosMain 门面上(社区线 foundation 没有 shape.corner 子包、跨模块构造 internal 类、顶层函数引用 object 嵌套类),处置见 8.1。

2.4 样例与测试的处置:samples 需要 *-desktop 制品

上游没有 commonTest(样例在 :samples,desktop-only),交叉验证采用编译级:三个模块的 commonMain(上游原样)对上游制品线执行 compileKotlinJvm。:samples 需要 compose.desktop.currentOs(*-desktop 子制品),社区 ohos 线不发布,留盘不进 settings;根构建同时全局禁用 ohos 测试任务(本库无测试,防御性保留社区纪律):

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

第 3 阶段:桥协议建模

3.1 JSON 契约
com.composables.composetheme/**(上游引擎 + M2/M3 桥,原样编译)
        ↓ ThemeBuilder / DesignTokens 合并 / ResolvedTheme / DefaultDebugIndication
门面层(ComposeThemeHeadless / ExtendMaterial3Headless / ExtendMaterialHeadless)
        ↓ 无头快照
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API themeVersion() / themeDispatch()
        ↓
ArkUI Index.ets(四 Tab)

请求与响应:

请求 : {"op":"version"}
       {"op":"defaults"}
       {"op":"builder","name":"LightTheme","overrides":"primary=FF42A5F5","props":"transitions=fast_200|faster_300"}
       {"op":"merge","overrides":"red500=FF00C853,primary=FF42A5F5","textStyles":"customBody=18:26",
        "shapes":"customRound=22","req":"red500,primary,nosuchtoken","propName":"transitions"}
       {"op":"missing","token":"primary","theme":"DarkTheme"}
       {"op":"extend3","primary":"FF6750A4"}
       {"op":"extend"}
响应 : {"ok":true,"op":"...","<op 专属字段>"}
       {"ok":false,"op":"...","error":"..."}

一个关键设计:列表型参数走逗号分隔的 key=value 列表字段(overrides / textStyles / shapes / props),而不是 ov.primary 这类点号前缀键——ArkTS 严格模式禁止对象字面量使用非标识符属性名(arkts-identifiers-as-prop-names),也禁止无接口对应的字面量(arkts-no-untyped-obj-literals),列表字段让请求对象能完整接口化(DispatchRequest),Kotlin 侧 pairs() 一行解析。

3.2 合并语义为什么是门面的核心

buildComposeTheme 返回的 composable 体内做三件事:构造 ThemeBuilder 并执行 builder lambda;用 DesignTokens(name, builder.X.entries + DefaultComposeTheme.X.entries) 把三组令牌与默认表合并(Kotlin 的 Map + Map,右侧默认值覆盖左侧 builder 值);把合并结果连同 builder 的自定义属性装进 ResolvedTheme,经 CompositionLocalProvider 提供并按需 extend 包装。前两步不需要 composition——门面用同一批真实对象执行同样的表达式,ArkTS 拿到的 resolved 值与将来 composition 宿主挂上后 ComposeTheme.colors[token] 读到的完全一致。这也让上游 README 的 WARNING 第一次可以在设备上直接验证:

{"name":"red500","argb":"#FFFF4C4C","source":"default","attemptedArgb":"#FF00C853"}
{"name":"primary","argb":"#FF42A5F5","source":"custom","attemptedArgb":"#FF42A5F5"}

source=default 证明 builder 对默认令牌的覆盖被合并丢弃(默认值 #FFFF4C4C 原样胜出),source=custom 证明新增令牌正常通过——两个 JSON 行就是上游合并语义的真机证据。自定义属性则经真实 ResolvedTheme.getProperty(ComposeTheme.get 的非 composable 底层)回读,验证属性存储链路。

suspend 与重组在门面里都不存在:令牌解析是纯同步数据操作,桥全程无协程、无帧时钟——这是它比动画类库(compose-shimmer 需要 HeadlessFrameClock)简单的地方。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("ThemeDispatch")
fun themeDispatch(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"op":"dispatch","error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、不支持的 op、颜色 hex 格式错误,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。缺失令牌的报错文案由门面按 DesignTokens.get 的原文复现(该异常只在 composition 内抛出):

Tried to access the value of the token called primary, but no tokens with that name are
defined within the colors property. You probably forgot to set a primary token in your
theme definition. The resolved theme was DarkTheme

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libcomposetheme_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoscomposetheme.so)
  │ ThemeDispatch / ThemeVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制合并语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("ThemeDispatch")
fun themeDispatch(request: String): String   // 7 个 op 一口进

@CName("ThemeVersion")
fun themeVersion(): String = "1.4.0-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
ThemeDispatchthemeDispatch(req)defaults 快照 + ThemeBuilder DSL + 令牌合并 + ResolvedTheme 回读 + M2/M3 桥
ThemeVersionthemeVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"themeDispatch", nullptr, NapiThemeDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"themeVersion", nullptr, NapiThemeVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscomposetheme_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscomposetheme_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohoscomposetheme SHARED IMPORTED)
set_target_properties(ohoscomposetheme PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(composetheme_napi SHARED napi_init.cpp)
target_link_libraries(composetheme_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoscomposetheme)

OHOS_ARCH 为空或 libohoscomposetheme.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const themeDispatch: (request: string) => string;
export const themeVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 启动握手与快照拉取
aboutToAppear(): void {
  this.versionText = themeVersion();   // 版本握手:来自真实 Native 调用
  this.refreshDefaults();              // 令牌 Tab 数据
  this.runBuilder();                   // 构建合并 Tab:builder op
  this.runMerge();                     // 构建合并 Tab:merge op(覆盖 red500 + 自定义 primary)
  this.refreshMaterial();              // Material Tab:extend3 + extend
}

private dispatch(req: DispatchRequest, onOk: (res: DispatchResponse) => void): boolean {
  const res = JSON.parse(themeDispatch(JSON.stringify(req))) as DispatchResponse;
  if (!res.ok) { this.lastError = `op=${res.op ?? '?'} ${res.error ?? 'unknown error'}`; return false; }
  onOk(res);
  return true;
}

merge 的请求把"尝试覆盖默认 red500 + 新增自定义 primary + 一个不存在的 token"三件事一次发给引擎,响应的 resolved 数组按 source / attemptedArgb 标注来源与被丢弃的尝试值。

5.2 页面交互

令牌 Tab 自上而下:三组令牌计数行(100 / 9 / 6)、6 个抽样色块(名称 + #AARRGGBB 文本,底色即该色值)、9 档文字样式表(名称 + 字号sp / 行高sp,按字号升序)、6 档形状(34vp 蓝色方块 + 实测圆角预览,full 显示 100%)。构建合并 Tab:主题名 / 自定义 token 名 / token 值 / red500 覆盖值四个输入框 + 两个触发按钮,builder 结果区展示指示器工厂名、真实创建的节点类名、buildComposeTheme 工厂已调用、属性注册表;merge 结果区逐条显示 resolved(来源标注)与被默认覆盖的尝试值,missing 单独成行;"模拟读取缺失 token"按钮展开真实报错文案。Material Tab:primary 十六进制输入 + 重新求值按钮,M3 六色板 / 五档形状 / 三个文字样式、M2 五色板 / 三档形状 / body1/h1 字号。关于 Tab:版本握手行、工具链表、ArkTS → NAPI → C ABI → Kotlin 引擎 → 社区 Compose 分层说明与仓库 / 社区链接。

5.3 对比度和错误边界

M3 / M2 色板行用色值本身作底色、按明度自动切换黑白前景文字;ok:false 时错误原文进页面顶部红色横幅(ErrorBanner)。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进横幅。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoscomposetheme)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.composetheme、versionName 1.4.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
        ├── src/main/cpp/CMakeLists.txt       IMPORTED 引入 KN so、$ORIGIN rpath
        ├── src/main/cpp/napi_init.cpp        N-API 导出、参数检查、DisposeString
        ├── src/main/cpp/include/libohoscomposetheme_api.h
        ├── src/main/cpp/types/libcomposetheme_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :composetheme:compileKotlinJvm :composetheme-material:compileKotlinJvm \
          :composetheme-material3:compileKotlinJvm                       # 交叉验证
./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 保持空数组)。

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.composetheme -m entry

四、完整代码对照

4.1 整体架构

com.composables.composetheme/**(上游引擎原样编译)
    │ buildComposeTheme / ThemeBuilder / DesignTokens / DefaultComposeTheme / ResolvedTheme
com.composables.composetheme.material[3]/**(上游 M2/M3 桥原样编译)
    │ extendMaterial / extendMaterial3 + ComposeTheme.colorScheme 等扩展
ohosMain 门面 × 3
    │ ComposeThemeHeadless / ExtendMaterial3Headless / ExtendMaterialHeadless
Bridge.kt + MiniJson
    │ ThemeDispatch / ThemeVersion
libcomposetheme_napi.so
    │ themeDispatch / themeVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(四 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_composetheme、samples 移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + 双 Compose 线(ohos 1.12.0-0.1.0-07 / jvm 1.8.1)
build.gradle.kts(根)版本 1.4.0-ohos.1、全局禁用 Ohos*Test* 任务
composetheme*/build.gradle.kts × 3纯 KMP:jvm + ohosArm64 + ohosX64,依赖按目标线声明
composetheme/src/ohosMain/.../ComposeThemeHeadless.ohos.kt核心门面 + 跨模块辅助(headlessThemeBuilder / headlessLookup / registeredPropertyValues)
composetheme-material3/src/ohosMain/.../ExtendMaterial3Headless.ohos.ktM3 桥门面
composetheme-material/src/ohosMain/.../ExtendMaterialHeadless.ohos.ktM2 桥门面
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、7 个 op、key=value 列表解析、MiniJson、错误通道
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鸿蒙适配主文档(18 节)

4.3 关键 API 对照

层次API作用
引擎buildComposeTheme { }主题工厂:builder 执行 + 三组令牌合并 + ResolvedTheme 装配
引擎ThemeBuilderDSL 状态:name / indication / colors / textStyles / shapes / properties / extend
引擎DesignTokens(entries + defaults)令牌合并(Map + Map,默认值胜出——上游 WARNING 的来源)
引擎ResolvedTheme.getPropertyComposeTheme[property] 的非 composable 属性存储
引擎DefaultComposeTheme100 色 / 9 文字 / 6 形状默认令牌表
引擎DefaultDebugIndication默认指示器(IndicationNodeFactory + Modifier.Node)
M3 桥extendMaterial3 { }colorScheme / typography / shapes 映射 + MaterialTheme 包装注册
M2 桥extendMaterial { }lightColors / Typography / Shapes 映射(十二色令牌)
门面ComposeThemeHeadless 等 × 3defaults / builder / merge / missing / extend3 / extend 快照
NativeThemeDispatch / ThemeVersionC ABI JSON 导出
N-APIthemeDispatch(req) / themeVersion()向 ArkTS 暴露能力
ArkTSdispatch(req, onOk)触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(themeDispatch(JSON.stringify({
  op: 'merge',
  overrides: `red500=FF00C853,${this.customTokenText}=${this.customValueText}`,
  textStyles: 'customBody=18:26', shapes: 'customRound=22',
  req: `red500,${this.customTokenText},nosuchtoken`,
  propName: 'transitions'
})));
// res.resolved[0] = {name:'red500', argb:'#FFFF4C4C', source:'default', attemptedArgb:'#FF00C853'}
// res.resolved[1] = {name:'primary', argb:'#FF42A5F5', source:'custom', ...}
val mergedColors = DesignTokens(
    _colors.name,
    builder.colors.entries + DefaultComposeTheme.colors.entries,   // buildComposeTheme 同款表达式
)
val theme = ResolvedTheme(builder.name, builder.indication, allProperties, builder.themeWrapper)
val propertyValue = theme.getProperty(DesignProperty<String>("transitions"))

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:真编译整库而不是提取零依赖内核

早期仓库抽内核或剥离 Compose(qrose 把 Painter 移进不编译的 composeMain),是因为当时的社区线与上游版本对不齐。本次适配先验证制品线:foundation / material / material3 的 ohos klib 全部存在于社区 Compose 1.12.0-0.1.0-07,且本库上游基于更旧的 Compose 1.8.1、只用稳定 API——障碍已消失,再剥离等于把主题引擎的手脚拆掉。真编译让 buildComposeTheme、令牌合并、属性存储、M2/M3 桥全部是上游原件。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 升到 9.6.1

社区 Compose 线的依赖闭包钉在 stdlib 2.3.20 / 工具链 2.4.20,旧钉法 2.2.21 读不了新元数据。Gradle 方面 2.4.20 线支持 9.x(compose-shimmer / KStore 先例),上游 wrapper 8.7 直接升 9.6.1——只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。

决策 3:根构建替换(cmp_qrose 模式),samples 留盘

上游根构建(AGP / compose multiplatform 插件别名 / dokka / nexus 发布 + PGP 签名)对鸿蒙没有保留价值,整个替换为极简 ohos 构建,上游文件保存为 *.upstream。samples 是 desktop-only 样例(compose.desktop.currentOs),社区 ohos 线不发布对应制品,留盘不进 settings.gradle.kts——交叉验证降为三个库模块的 compileKotlinJvm 编译级验证。

决策 4:无头门面而不是运行 composition

OHOS 没有 Compose composition 宿主,理论上可以在 KN so 里手搓 Recomposer + 自定义帧时钟跑一个无 UI 的 composition,但那要把渲染管线的运行时假设(主线程调度、帧时钟)全部带进无头环境,风险高且没有先例。门面路线(compose-shimmer 已审先例)只调用不需要 composition 的真实对象——ThemeBuilder、DesignTokens 合并、ResolvedTheme.getProperty、DefaultDebugIndication 工厂、extendMaterial3——合并语义与 composition 内完全一致(同一批对象、同一批表达式),宿主出现后删除门面即可,commonMain 零影响。

决策 5:编译级交叉验证 + 上游源码零改动

本库上游没有 commonTest,交叉验证的有效形态是"同一份 commonMain、两条制品线都编译通过":jvm 对上游线(compose 1.8.1,Maven Central)证明源码没被适配改动,ohos 对社区线(1.12.0-0.1.0-07)证明新线 API 兼容。上游源码一个字节不动(含那个上游自带的 data class copy 可见性警告),是这套证据链成立的前提。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :composetheme:compileKotlinJvm :composetheme-material:compileKotlinJvm \
          :composetheme-material3:compileKotlinJvm      # BUILD SUCCESSFUL

三个模块的上游 commonMain(ComposeTheme.kt / ComposeTheme.Defaults.kt / DefaultIndication.kt / ExtendMaterial.kt / ExtendMaterial3.kt)对上游制品线编译全绿,零改动(唯一告警是上游自带的 data class copy() 可见性提示,非适配引入)。ohos 侧同一批源码对社区线编译同样全绿——"同一份源码、两条制品线"的证据链闭合。

6.3 HAP 验证

:composetheme:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:composetheme-material / -material3:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ThemeDispatch / ThemeVersion / libohoscomposetheme_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libcomposetheme_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

KN so 因全量静态链接 Compose 运行时显著大于存储类库:debugShared 未 strip 时 arm64 约 38.5MB、x86_64 约 37.1MB(KStore 同口径约 14.8MB——差的正是 Compose runtime / foundation / material[3]);每个 ABI 三件套(libcomposetheme_napi.so / libohoscomposetheme.so / libc++_shared.so)由 Copy 任务与 CMake POST_BUILD 双重保证同目录。

6.4 功能用例

  1. 启动后版本握手行出现 themeVersion() → 1.4.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 令牌 Tab 计数行为 colors 令牌数 100、textStyles 令牌数 9、shapes 令牌数 6——与上游 ComposeTheme.Defaults.kt 的令牌定义逐一对账(10 色系 × 10 明度);
  3. 抽样色块 red500 #FFFF4C4C / blue500 #FF6699FF / gray900 #FF474747 等与上游默认表常量一致;
  4. 文字样式表 xs 12sp/16sp 到 xl5 48sp/48sp 单调递增,形状表 round 4dp 到 full 100%;
  5. 构建合并 Tab:red500 行显示 来源=default 且"builder 尝试值(被默认覆盖) #FF00C853"——上游 WARNING 真机成立;primary 行显示 来源=custom;nosuchtoken 进 missing;
  6. 自定义属性回读行显示 set-via-headless-builder(经真实 ResolvedTheme.getProperty);
  7. "模拟读取缺失 token"展开的报错文案含 The resolved theme was DarkTheme(主题名参与报错);
  8. Material Tab:把 primary 改为 FF6750A4 重新求值,M3 色板 primary 同步变化、onPrimary 保持白;五档形状 4/8/12/16/28dp、bodyLarge 16sp/24sp;M2 区 primary #FF6200EE、三档形状 4/8/16dp、body1 16sp、h1 96sp;
  9. 每个 Tab 的数据在页面切回时保持(@State 持有真实快照,非静态演示数据)。

6.5 验证结论

主题解析引擎、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的主题会话:版本握手、默认令牌快照、DSL 驱动、合并语义(默认不可覆盖 + 自定义通过)、缺失令牌报错与 M2/M3 桥(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/ComposeTheme.git
cd ComposeTheme
export JAVA_HOME="/path/to/jdk-21"
./gradlew :composetheme:compileKotlinOhosArm64 :composetheme-material:compileKotlinOhosArm64 \
          :composetheme-material3:compileKotlinOhosArm64                              # klib
./gradlew :composetheme:compileKotlinJvm :composetheme-material:compileKotlinJvm \
          :composetheme-material3:compileKotlinJvm                                    # 交叉验证
./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.composetheme -m entry
hdc shell hilog | grep ComposeTheme

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让三个库模块真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 社区线 foundation klib 没有 androidx.compose.foundation.shape.corner 子包(DpCornerSize / PercentCornerSize 解析不到)——形状数值改用稳定的公共 API Shape.createOutline(Size(100/200), Ltr, Density(1f)) 求值,dp 角在两种尺寸下半径不变、百分比角随尺寸翻倍,一次调用同时拿到数值与类型;
  4. ThemeBuilder 构造器是核心模块 internal,M2/M3 门面(跨模块)不能直接构造——核心门面提供公开 headlessThemeBuilder() 工厂与 headlessLookup / registeredPropertyValues 等辅助,跨模块驱动同一批真实对象;
  5. 顶层辅助函数引用 object 内嵌套类必须写全限定(ComposeThemeHeadless.ShapeSnapshot),否则 ohos 编译报 Unresolved;
  6. Kotlin infix 优先级陷阱:name to x and MASK 实际解析为 (name to x) and MASK,二元位运算要显式加括号;
  7. Compose 依赖必须按目标线声明(ohosMain 社区线 / jvmMain 上游线):社区线不发布 *-desktop 子制品,坐标放 commonMain 会让 jvm 目标解析失败——线切换,不是版本切换;
  8. ArkTS 严格模式两条硬规则:对象字面量必须对应显式接口(arkts-no-untyped-obj-literals),属性名必须是合法标识符(arkts-identifiers-as-prop-names)——ov.primary 这类点号键与 Record 字面量都过不了编译,列表参数改用逗号分隔的 key=value 列表字段,请求对象全部接口化;
  9. 根构建的 /build/ 是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.hvigor/、entry/build/、entry/libs/(so 三件套)要单独追加 gitignore,否则混进提交;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内保持 signingConfigs 空数组 + product 引用 "default",证书绝不进入 AtomGit。

8.2 已知问题

  • OHOS 无 Compose composition 宿主:buildComposeTheme 返回的主题 composable 与全部 @Composable 取值访问器已编入 so 但无 ArkUI 侧挂载点,Demo 经无头门面驱动同样的底层对象;
  • 缺失令牌的报错文案由门面按 @Composable 访问器原文复现(真实异常只会在 composition 内抛出);
  • extendMaterial / extendMaterial3 注册的 MaterialTheme 包装 lambda 是核心模块 internal,无头侧不反查——门面以"桥已执行 + 令牌/属性已映射"为验收面;
  • 上游 1.4.0 的 buildComposeTheme builder lambda 是 @Composable(1.3→1.4 行为变化),门面绕过该入口直接驱动 ThemeBuilder,合并语义与之一致;
  • android / js / wasmJs / iOS 目标与 Sonatype 发布、dokka 一并剥离(源文件留在树里),本 fork 不发 Maven Central;
  • samples(desktop 样例)未接入 ohos 构建,mkdocs.yml 文档站与 scripts/ 同为上游遗留。

8.3 未来优化方向

  • 跟踪社区 Compose composition 宿主:出现后删除三个 ohosMain 门面与 nativeApp 桥,commonMain 的 @Composable API 直接接入,example/harmonyApp 换成真实 Compose 页面;
  • 把门面快照改为带状态的主题会话(同一 builder 生命周期内多次 resolve),更贴近真实 App 用法;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 composetheme + -material + -material3(全部 commonMain,零 expect/actual)
    → 验证社区 Compose ohos 制品线(foundation / material / material3)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 8.7 → 9.6.1)
    → 根构建替换(cmp_qrose 模式,依赖按目标分线:ohos 社区线 / jvm 上游线)
    → ohosArm64 / ohosX64 KLIB + 三个无头门面
    → C ABI(@CName × 2)+ key=value 列表字段载荷
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析(接口化请求,绕开严格模式两条硬规则)
    → ArkUI 真机四 Tab 验收(令牌 / 合并语义 / M2/M3 桥 / 版本握手)

9.2 封装层次

  • 引擎层:上游原版 buildComposeTheme、ThemeBuilder DSL、DesignTokens 合并、ResolvedTheme 属性存储;
  • 默认表层:上游原版 DefaultComposeTheme(100 色 / 9 文字 / 6 形状)与 DefaultDebugIndication;
  • 桥层:上游原版 extendMaterial3 / extendMaterial 与 ComposeTheme.colorScheme 等扩展;
  • 门面层:三个 ohosMain 无头门面把上述对象提升为 JSON 快照;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理四 Tab 页面状态、请求分发、数值渲染与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:社区 Compose 线(1.12.0-0.1.0-07)补齐 foundation / material / material3 的 ohos klib 后,纯 Compose 库的正确答案从"剥离 Compose"(qrose 时代)变成"真编译整库 + 无头门面"(compose-shimmer 先例)——路线选择必须基于当天的制品可达性,而不是历史结论;
  2. @Composable 不是铁板一块:把"取值入口是 @Composable"拆开看,入口底层的对象(builder、令牌映射、属性存储、桥函数)大多不需要 composition——无头门面的边界画在"真实对象 + 同款表达式"上,既不复制语义也不虚构运行时;上游 README 里"默认令牌无法覆盖"这条 WARNING,正是靠门面第一次在真机上被肉眼验证;
  3. ArkTS 严格模式是桥协议的隐性设计约束:arkts-no-untyped-obj-literals / arkts-identifiers-as-prop-names 决定了列表参数必须走 key=value 列表字段而不是前缀键——跨语言协议设计要把最严的消费方(ArkTS 编译器)当作第一公民,否则 Kotlin 侧再优雅也要返工。

9.4 适配成果

当前适配已完成:

  • ComposeTheme 1.4.0 真实引擎的双 ABI Kotlin/Native 编译(上游三个模块 commonMain 文件零改动,零 expect/actual,新增源码仅三个 ohosMain 无头门面);
  • 社区 Compose 制品线 1.12.0-0.1.0-07 在主题类库上的完整落地(runtime / foundation / material / material3,ohosArm64 + ohosX64 双变体);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 四 Tab 验收页面(默认令牌、DSL 驱动、合并语义、缺失令牌报错、M2/M3 桥、版本握手);
  • 三个模块 JVM 编译交叉验证全绿(上游源码对上游制品线)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 18 节)和本地效果图。

适配过程 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

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐