本文记录 KMPalette 接入 OpenHarmony 的完整过程,覆盖动态主题库盘点、鸿蒙 kotlinx 制品线选型、Compose UI 隔离、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

KMPalette 是基于 kotlinx.coroutines + kotlinx.serialization + Compose Multiplatform 的 Kotlin Multiplatform 动态主题管理库:ThemeRegistry 注册主题族(light/dark 成对),ThemeController 以 StateFlow 状态机管理 SYSTEM/LIGHT/DARK 模式与显式主题选择并解析出最终主题,ThemeStore 是可插拔持久化接口,35 字段 Material3 Palette 颜色 token 加 typography/shapes/elevations 构成完整设计令牌模型,内置 9 套主题族。本次适配走的是社区最早验证的"内核隔离"路线(cmp_qrose 同款),与 KStore 的"真编译整库"路线有本质区别:这个库一半代码是 Compose Material3 UI,而社区 CMP ohos 制品(1.9.2)与上游构建(CMP 1.8.2 / Kotlin 2.2.0)无法对齐——因此纯 Kotlin 主题引擎(模型、注册表、控制器、9 主题族 630 个颜色常量)原样编译成 OpenHarmony 动态库,Compose 适配层整体隔离到不参与编译的 composeMain 源集,ArkUI 页面作为验收宿主,整页颜色由 Kotlin 引擎实时驱动。

一个前置澄清:KMPalette 名字里虽然带 Palette,但它不是图片取色库——“Palette” 指库内 35 字段的颜色 token 集,与 kmpalette 等位图取色库没有任何代码关系。

项目地址: AtomGit/oh-tpc/KMPalette-Theme-Library

上游仓库: iammohdzaki/KMPalette-Theme-Library

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

KMPalette 为 Kotlin Multiplatform 应用提供"主题管理"这一件事的完整答案:DefaultThemeRegistryFactory.createWithDefaults() 一行拿到预置 9 族注册表,ThemeController(registry, store, system, defaultThemeId) 一行接上持久化与系统深浅色,state: StateFlow<ThemeState> 让 UI 订阅当前解析结果。解析策略上游做得很细:SYSTEM 模式跟随系统深浅色,显式主题与模式明暗不匹配时在同族内自动换 light/dark 变体,默认主题按"默认族 → meta default 标记 → 首个匹配明暗"逐级回退。Compose 侧再由 DynamicThemeProvider + Material3Adapter 把 35 个颜色 token 逐字段灌进 MaterialTheme,ThemePickerBottomSheet 提供开箱即用的主题选择器。

OpenHarmony 应用同样需要这套动态主题能力,但上游构建(Kotlin 2.2.0 / CMP 1.8.2 / AGP 8.3.2 / Gradle 8.9)不会生成 OpenHarmony 变体。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 产物。
UI 层强绑定 ComposecommonMain 28 个文件里 8 个直接 import androidx.compose(适配器、Provider、选择器),鸿蒙没有官方 CMP 编译目标。
依赖矩阵引擎依赖 kotlinx-coroutines、kotlinx-serialization-json,二者都要有 ohos 变体才能解析。
工具链不一致上游 Kotlin 2.2.0 / Gradle 8.9 带 AGP 与 Android target,与鸿蒙工具链无法共存于一个 Gradle 构建。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
主题语义跨不了桥StateFlow<ThemeState> 订阅、持久化接口是 Kotlin 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:纯 Kotlin 主题引擎的 ohos 编译、根构建替换(cmp_qrose 模式)、palette 数据零漂移纯化和 C ABI/N-API 桥接层。主题解析语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON、持有持久化与系统深浅色、展示结果。

1.2 库提供的能力

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

  • 主题族清单(list):注册表 families() 返回 9 族,每族附 light/dark 双套 4 色预览(primary/secondary/tertiary/surfaceVariant,与上游 Compose 选择器的四圆预览同款);
  • 状态解析(state):控制器解析当前 ThemeState——35 色 palette、typography/shapes/elevations token、meta 一并返回,ArkUI 整页换色;
  • 模式切换(setMode):controller.setMode(SYSTEM/LIGHT/DARK),Dark 下当前族自动切 dark 变体;
  • 主题切换(setTheme):controller.setExplicitTheme(id),选族后明暗不匹配时上游逻辑自动在同族内换变体;
  • 系统深浅色联动(notifySystemDark):ArkTS 用媒体查询监听系统深浅色,推给 notifySystemDarkChanged,SYSTEM 模式下页面立即换变体;
  • 持久化闭环:每次响应带 persist 字段,ArkTS 写入 @ohos.data.preferences;杀进程重启后由首调请求回灌,恢复上次的模式与显式主题。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
state解析后的 ThemeState:mode、explicitThemeId、isDark 与完整 theme(id/familyId/displayName/palette 35 色/typography/shapes/elevations/meta)。
families9 个主题族的 id、displayName、lightId/darkId 与双套 4 色预览。
persist告诉 ArkTS 应当持久化的当前选择(mode + explicitThemeId)。

引擎侧完整能力(注册表 LinkedHashMap 保序、可重入锁、family 变体自动切换、pickDefault 回退链)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留上游两套测试类(ThemeControllerTest / ThemeRegistryTest)交叉验证,证明 ohos .so 里执行的主题逻辑没有漂移。

1.3 实现适配

维度要求
代码复用引擎 20 个纯 Kotlin 文件原样编译,不重写;9 个 palette 文件经脚本零漂移纯化(70 色/文件断言);仅新增 2 个平台 actual(PlatformLock 的 jvm/ohos 双实现,ohos 版为可重入自旋锁)。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);8 个 Compose 文件移入不参与编译的 composeMain(留作未来社区 CMP ohos 制品接回);samples/ 示例应用留盘不进构建。
桥接稳定使用 1 个 C ABI 和 UTF-8 JSON,签名永不变(新功能只加 JSON 字段);不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律kotlinx 换到与 Kotlin 2.2.21-1.0.0 对齐的 ohos 制品线(coroutines 1.10.2-1.0.0、serialization 1.9.1-1.0.0),上游测试零改动全绿。
UI 完整单页覆盖全部桥接入口:组件展示区、模式三段切换、9 主题族列表、状态行、持久化与系统深浅色联动。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。被隔离的 DynamicThemeProvider / Material3Adapter / ThemePickerBottomSheet 属于 Compose 适配层,等社区 CMP ohos 制品与上游版本对齐后可从 composeMain 原样接回;ArkTS Demo 是把桥暴露的主题引擎能力画出来。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 kotlinx ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:桥协议建模     ── 主题状态 JSON 契约、持久化与系统深浅色的职责划分、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── preferences 持久化、媒体查询联动、组件展示、主题族列表
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
theme-core/                           上游主题引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
theme-core/src/commonMain/            纯 Kotlin 核心:model / registry / controller / themes(20 文件)
theme-core/src/composeMain/           Compose 适配层隔离区:8 文件,不被任何 target 编译
theme-core/src/jvmMain/               新增:PlatformLock actual(ReentrantLock)
theme-core/src/ohosMain/              新增:PlatformLock actual(可重入自旋锁)
theme-core/src/{android,desktop,ios,wasmJs}Main/   上游平台源集,留盘不进构建
samples/                              上游 Compose 示例应用,留盘不进 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 :theme-core 与 :example:nativeApp,rootProject.name = "ohos_kmpalette"。上游的 AGP / Compose / vanniktech 发布插件、android/ios/wasmJs target、samples 模块全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 指向 Temurin 21,Windows)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 8.9;2.2.21 线社区钉法即 8.14.1)
kotlinxcoroutines 1.10.2-1.0.0、serialization-json 1.9.1-1.0.0(ohos 制品线,后缀与 Kotlin 线对齐)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.kmpalette
export JAVA_HOME="C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version

注意 gradle.properties 里的 org.gradle.java.home 只约束 Gradle 本身用的 JDK,gradlew.bat 启动器仍要求环境变量 JAVA_HOME——不设会直接报 JAVA_HOME is not set。

1.3 源码依赖度盘点
类别文件数依赖形态处置
纯 Kotlin 核心20coroutines + serialization,零 UI(model/tokens、registry、controller、ThemeStore/SystemThemeProvider 接口、themes/Utils)整体编译(jvm + 双 ohos)
palette 数据文件9(属上表 20 之列)lightColorScheme() / darkColorScheme() / Color() 构造后 toPalette()脚本纯化为直接 Palette(0xFF…L) 构造
Compose 适配层8androidx.compose material3/runtime/foundation + material-icons-extended移入 composeMain,不参与编译
上游平台源集4 目录android/desktop/ios/wasmJs 的 3 组 actual留盘不进构建

expect 声明全库只有 3 处:PlatformLock(注册表并发安全用,留在核心,jvm/ohos 各补 actual);PlatformSystemThemeProvider(库内零使用、只服务 Compose 消费方,随 Compose 层移出,ohos 无需实现);ThemePickerContainer(expect Composable,随 Compose 层移出)。

palette 纯化值得单独说明:上游 9 个主题族文件每个先构造 Compose ColorScheme 再经 toPalette() 逐字段 toArgb().toLong() 转成 35 个 Long。toPalette 的映射就是字段名一一对应,所以转换可以完全机械化——脚本把 Color(0xFF6750A4) 换成 0xFF6750A4L、删掉两段 scheme 构造、把 toPalette() 调用展开为直接 Palette(primary = primaryLight, …, isDark = false),每文件断言恰好 70 个颜色字面量、恰好删掉 2 个 scheme 块。9 个文件 630 个颜色常量零手抄、零漂移。

1.4 页面边界
标题区          KMPalette / 明暗胶囊(Dark / Light,随 isDark 变色)
主题卡          当前主题 displayName、familyId、注册族数
模式切换        System / Light / Dark 三胶囊(对应 setMode)
组件展示区      Filled / Tonal / Outlined / Text 按钮、TextInput、Slider、
                LoadingProgress / LinearProgress、Toggle、Display 字号样张
                ——全部颜色取自当前 palette,验证 35 色真实生效
主题族列表      9 族:4 圆重叠预览 + 名称 + 选中勾(对应 setTheme)
状态行          theme id / mode / explicitThemeId / isDark 实时回显

页面启动时先建 preferences、回灌持久化选择,再发起首个 bridge 调用;媒体查询监听系统深浅色变化。

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

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

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

org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2-1.0.0
  variants: ohosArm64Api-published / ohosX64Api-published / jvmApi-published / jvmRuntime-published
  metadataApiElements requires kotlin-stdlib 2.2.21-1.0.0   ← 与钉版逐字一致
org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.1-1.0.0
  variants: 同上(jvm + ohos 双线齐全)

三个关键结论:其一,ohos 制品的版本后缀与 Kotlin 构建号对齐(-1.0.0 对 2.2.21-1.0.0),stdlib 元数据逐字匹配,不存在 KStore 那种"2.2.21 读不了 2.4 元数据"的连锁升版;其二,ohos 制品同时发布 jvm 变体,jvm() 交叉验证用同一个版本号,不需要双线隔离;其三,Maven Central 上的官方 coroutines / serialization 没有 ohos 变体,版本必须带 -1.0.0 后缀、从 eazytec 解析。上游声明的 coroutines 1.9.0 / serialization 1.6.3 顺带升到 1.10.2 / 1.9.1——commonMain 只用 StateFlow / runBlocking / @Serializable 等稳定 API,上游测试零改动全绿证明无漂移。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

社区已审核的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的同款)。上游源码本来就是 Kotlin 2.2.0 编的,2.2.21 与之同代,源码级兼容成本为零;coroutines 1.10.2-1.0.0 的元数据也正好要求 stdlib 2.2.21-1.0.0,工具链与依赖天然对齐。Gradle 方面 2.2.21 线社区钉 8.14.1(上游 8.9 顺带升级),wrapper 四件套随根构建替换。

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

include(":theme-core")
include(":example:nativeApp")
2.3 编译真实引擎

theme-core/build.gradle.kts 从 106 行(android + compose + publish 全家桶)收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.serialization)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {
            implementation(libs.coroutines.core)          // 1.10.2-1.0.0
            implementation(libs.serialization.json)      // 1.9.1-1.0.0
        }
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

根构建统一禁用 ohos 测试任务(测试统一在 JVM 跑,上游测试文件零改动保留):

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

实测::theme-core:compileKotlinJvm、:theme-core:jvmTest、compileKotlinOhosArm64 / compileKotlinOhosX64 全部 BUILD SUCCESSFUL,引擎一个文件没动,新增的只有 PlatformLock 两个 actual。

PlatformLock 的 ohos actual 不是一行空实现:上游 DefaultThemeRegistry 的 registerAll 在持锁状态调用 register 再加锁、registerFamily(vararg) 嵌套三层,JVM 上游就是 ReentrantLock、iOS 是 NSRecursiveLock——ohos 必须同样可重入,非重入锁直接死锁。实现用 kotlin.concurrent.AtomicInt 三字段(held/depth/owner)+ Worker.current.id 做 owner 追踪的可重入自旋锁,零 cinterop、零额外依赖:

// theme-core/src/ohosMain/kotlin/com/zaki/dynamic/core/registry/PlatformLock.ohos.kt
actual class PlatformLock {
    private val held = AtomicInt(0)        // 0 = free, 1 = held
    private val depth = AtomicInt(0)       // re-entry depth of the owner
    private val owner = AtomicInt(UNOWNED)

    actual fun lock() {
        val me = Worker.current.id
        if (held.compareAndSet(0, 1)) { owner.value = me; depth.value = 1; return }
        if (owner.value == me) { depth.incrementAndGet(); return }
        while (!held.compareAndSet(0, 1)) { /* spin */ }
        owner.value = me; depth.value = 1
    }
    actual fun unlock() {
        check(owner.value == Worker.current.id)
        if (depth.decrementAndGet() == 0) { owner.value = UNOWNED; held.value = 0 }
    }
    private companion object { const val UNOWNED = -1 }
}
2.4 Compose 隔离:composeMain 不是删除

8 个 Compose 文件用 git mv 移入 src/composeMain/kotlin(保留历史)——composeMain 不是任何 target 的标准源集名,Kotlin 插件不会编译它,但这些文件原样留在树里:等社区 CMP ohos 制品与上游版本对齐,把它们移回 commonMain、恢复 compose 插件配置即可接回。上游 4 个平台源集目录(androidMain/desktopMain/iosMain/wasmJsMain)同理留盘——对应 target 不存在即不参与构建。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.zaki.dynamic.core/**(上游引擎原样编译)
        ↓ ThemeRegistry / ThemeController / StateFlow<ThemeState> / 9 主题族
Bridge.kt(@CName × 1,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API kmpaletteTheme()
        ↓
ArkUI Index.ets(单页滚动)

请求与响应:

请求 : {"op":"state|list|setMode|setTheme|notifySystemDark",
        "mode":"SYSTEM|LIGHT|DARK", "themeId":"ocean_blue_light"|null,
        "isSystemDark":true, "savedMode":"LIGHT", "savedThemeId":"..."}
响应 : {"ok":true,
        "state":{"mode":"…","explicitThemeId":…,"isDark":…,
                 "theme":{"id","familyId","displayName","palette":{35 色 #AARRGGBB},
                          "typography","shapes","elevations","meta"}},
        "families":[{"id","displayName","lightId","darkId",
                     "lightPreview":["#…",×4],"darkPreview":["#…",×4]}],
        "persist":{"mode":"…","explicitThemeId":…}}
       {"ok":false,"error":"…"}

三个职责划分设计:持久化与系统深浅色由 ArkTS 持有——preferences 与媒体查询是平台能力,桥保持纯 Kotlin、可在 JVM 侧测试,每次请求可带 savedMode/savedThemeId/isSystemDark、每次响应带 persist 回灌;控制器是进程级单例——注册表一次构建 9 族,后续请求只做增量操作;首调等待——ThemeController 在 init 协程里异步 store.load(),桥首次调用用 runBlocking + withTimeoutOrNull(2s) 轮询 store 的 loadCalled 标志,保证第一次响应就是恢复后的选择而非 SYSTEM 默认值。

3.2 载荷为什么是 35 色全景而不是"当前主题一个色"

响应把 palette 35 个颜色全量返回(#AARRGGBB 字符串),ArkUI 页面的背景、卡片、按钮、滑轨、进度、文字全部从这一个对象取色——这样 Demo 验收的就是引擎输出的完整设计令牌,而不是某几个代表性颜色。families 的 4 色预览(primary/secondary/tertiary/surfaceVariant)与上游 Compose ThemePickerBottomSheet 的 ThemeFamilyItem 四圆预览逐字段同款,light/dark 各一套,ArkTS 按当前 isDark 取用。

3.3 错误通道

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

@CName("KmpaletteTheme")
fun kmpaletteTheme(request: String): String = try {
    Bridge.handle(MiniJson.parseObject(request))
} catch (t: Throwable) {
    """{"ok":false,"error":${Json.str(t.message ?: t.toString())}}"""
}

非法 JSON、未知 op、非法 mode,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。

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

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

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;逐能力导出多个 C 符号又会让签名随功能演进膨胀,所以选择单 C 符号 + JSON 协议——新功能只加 JSON 字段,C 签名永不变。

4.2 Kotlin/Native 导出函数
@CName("KmpaletteTheme")
fun kmpaletteTheme(request: String): String   // state/list/setMode/setTheme/notifySystemDark 一口进
C ABI 符号ArkTS 方法覆盖内核
KmpaletteThemekmpaletteTheme(req)注册表构建、状态解析、setMode、setExplicitTheme、notifySystemDarkChanged、持久化选择输出

桥内不引 kotlinx.serialization 解析请求——手写 MiniJson(扁平对象解析器)+ 手写 JSON builder,桥协议与 serializer 插件版本完全解耦(cmp_qrose 同款决策)。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"kmpaletteTheme", nullptr, NapiKmpaletteTheme, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

回调统一三步:napi_typeof 校验必须是 string(JsToUtf8 两次调用先量长度再 std::vector 拷贝,规避 API 26 NAPI 头的 const 兼容坑)、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskmpalette_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskmpalette_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。

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

add_library(kmpalette_napi SHARED napi_init.cpp)
target_link_libraries(kmpalette_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoskmpalette)

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

export const kmpaletteTheme: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 初始化与请求分发
private async init(): Promise<void> {
  const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
  const prefs = preferences.getPreferencesSync(ctx, { name: 'kmpalette_store' });
  this.prefs = prefs;

  const req: Record<string, Object> = { 'op': 'state', 'isSystemDark': this.sysDark };
  const raw = prefs.getSync('selection', '') as string;
  if (raw !== '') {
    const sel = JSON.parse(raw) as SelectionJson;      // 回灌上次持久化的选择
    req['savedMode'] = sel.mode;
    if (sel.explicitThemeId !== null) { req['savedThemeId'] = sel.explicitThemeId; }
  }

  const mq = mediaquery.matchMediaSync('(prefers-color-scheme: dark)');
  this.sysDark = mq.matches;
  mq.on('change', (r) => { this.sysDark = r.matches; this.call('notifySystemDark'); });

  this.send(JSON.stringify(req));
}

private send(request: string): void {
  this.apply(kmpaletteTheme(request));                 // JSON 进、JSON 出、@State 刷新整页
}

每次 apply 成功后把 resp.persist 写回 preferences 并 flush(),持久化闭环就此合拢。

5.2 页面交互

单页自上而下:标题行(KMPalette + 明暗胶囊)、当前主题卡(displayName / familyId / 注册族数)、System/Light/Dark 三胶囊、组件展示区(四种按钮、输入框、滑条、双进度、开关、Display 字号样张——每类控件取 palette 的不同字段,验证 35 色全景生效)、9 主题族列表(4 圆重叠预览用负 margin 实现上游同款视觉,点击 setTheme 传当前明暗对应的变体 id)、状态行实时回显 theme/mode/explicit/dark。

5.3 联动与错误边界

SYSTEM 模式下系统深浅色切换 → 媒体查询回调 → notifySystemDark → 当前族自动换 light/dark 变体、整页换色;Light/Dark 显式模式下页面不换主题(上游 notifySystemDarkChanged 只在 SYSTEM 模式重解析,语义一致)。ok:false 时错误原文进状态行;Native 调用抛错同样被 try/catch 收进状态行与 hilog。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoskmpalette)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/Bridge.kt         @CName × 1 + MiniJson + 单例控制器
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.kmpalette、versionName 0.0.6-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/libohoskmpalette_api.h
        ├── src/main/cpp/types/libkmpalette_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :theme-core:jvmTest                                        # 上游测试交叉验证
./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 中勾选自动生成,证书写入本机,不进入仓库(仓库内 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.kmpalette -m entry

四、完整代码对照

4.1 整体架构

com.zaki.dynamic.core.model/**(上游模型原样编译)
    │ Palette 35 色 / TypographyTokens / ShapeTokens / ElevationTokens
com.zaki.dynamic.core.registry/**(上游注册表原样编译 + ohos 可重入锁 actual)
    │ ThemeRegistry / DefaultThemeRegistry / 9 主题族工厂
com.zaki.dynamic.core.controller/**(上游控制器原样编译)
    │ ThemeController:StateFlow<ThemeState> / resolve / pickDefault
theme-core/src/composeMain/**(Compose 适配层隔离,不编译)
Bridge.kt + MiniJson
    │ KmpaletteTheme
libkmpalette_napi.so
    │ kmpaletteTheme
Index.ets
    │ JSON parse + @State 更新 + preferences + mediaquery
    ▼
ArkUI 真机页面(单页滚动,整页颜色由引擎驱动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_kmpalette、samples 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 + ohos kotlinx 制品线钉版 + serialization 插件别名
build.gradle.kts(根)版本 0.0.6-ohos.1、全局禁用 Ohos*Test* 任务
theme-core/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,serialization 插件模块级声明
theme-core/src/ohosMain/.../PlatformLock.ohos.kt可重入自旋锁 actual(嵌套加锁的刚需)
theme-core/src/jvmMain/.../PlatformLock.jvm.ktReentrantLock actual
tools/convert_palettes.pypalette 纯化脚本(70 色/文件断言,零漂移)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、单例控制器、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(25 节)

4.3 关键 API 对照

层次API作用
引擎DefaultThemeRegistryFactory.createWithDefaults()预置 9 族注册表(桥单例构建)
引擎ThemeController.stateStateFlow<ThemeState> 订阅(不跨 C ABI,桥取 .value)
引擎setMode / setExplicitTheme模式与显式主题,family 变体自动切换
引擎notifySystemDarkChanged系统深浅色输入(ArkTS 媒体查询驱动)
引擎ThemeStore(接口)持久化接缝,桥内为内存实现、ArkTS preferences 回灌
NativeKmpaletteThemeC ABI JSON 导出(单符号,签名永不变)
N-APIkmpaletteTheme(req)向 ArkTS 暴露能力
ArkTSsend(op)组请求、调桥、@State 全页刷新

4.4 ArkTS 与 Kotlin 的边界

const resp = JSON.parse(kmpaletteTheme(JSON.stringify({
  op: 'setTheme', isSystemDark: this.sysDark,
  themeId: this.isDark ? family.darkId : family.lightId
}))) as RespJson;
// resp.ok === true;resp.state.theme.palette.primary 即引擎解析出的当前主色
"setTheme" -> engine.controller.setExplicitTheme(
    MiniJson.str(q, "themeId")?.takeIf { it.isNotBlank() }?.let(::ThemeId)
)
// 上游 resolve():显式主题与模式明暗不匹配时,在同族内换成对应 light/dark 变体

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

五、关键决策说明

决策 1:内核隔离(composeMain)而不是真编译整库

这个库 28 个 commonMain 文件里 8 个直接依赖 Compose Material3 与 material-icons-extended,Material3Adapter 的存在意义就是把 token 灌进 Compose MaterialTheme。社区 CMP ohos 制品停在 1.9.2,与上游 CMP 1.8.2 / Kotlin 2.2.0 无法在编译期对齐;把 Compose 层"编译进去"不可行,"删掉"则丢失接回能力。所以走 cmp_qrose 验证过的第三条路:引擎原样编译、Compose 层整体移入不参与编译的 composeMain 源集、git 历史保留移动轨迹——与 KStore"真编译整库"的区别不在工程能力,而在库的 UI 依赖占比。

决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1(社区已审核线)

qrose / haze 已把 2.2.21-1.0.0 + Gradle 8.14.1 跑通并过审;coroutines 1.10.2-1.0.0 的元数据恰好要求 stdlib 2.2.21-1.0.0,依赖与工具链互相锁定。上游 Kotlin 2.2.0 与 2.2.21 同代,源码零改动;Gradle 从上游 8.9 升到 8.14.1 是该线的社区标配。

决策 3:palette 用脚本纯化而不是手抄

9 个文件 630 个颜色常量如果手工改写,抄错一位色值都难以肉眼发现。转换脚本按"字段同名一一映射"生成纯 Kotlin Palette 构造,并断言每文件恰好 70 个颜色、恰好删除 2 个 scheme 块——生成过程可复跑、结果可校验,"零漂移"是断言出来的不是承诺出来的。

决策 4:持久化与系统深浅色放 ArkTS 侧

ThemeStore 是可插拔接口,桥内用内存实现 + 请求播种;真正的持久化(@ohos.data.preferences)与系统深浅色(媒体查询)由 ArkTS 持有。这让桥保持纯 Kotlin、零 cinterop、JVM 可测,同时把平台能力归还平台——preferences 在 ArkTS 侧是官方姿势,Kotlin 侧为此引 OHOS cinterop 反而增加不可测面。

决策 5:PlatformLock 必须可重入 + jvmTest 交叉验证

上游注册表嵌套加锁决定了 ohos actual 必须可重入(wasmJs 式空实现在此不成立:ohos 上控制器跑在多线程的 Dispatchers.Default,NAPI 线程与协程线程真实并发);原子量 + Worker id 的自旋锁 30 行内解决且零依赖。交叉验证沿用上游两套测试类零改动跑在 jvmTest——引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码,测试绿了"内核没漂"才有说服力。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

./gradlew :theme-core:compileKotlinJvm :theme-core:jvmTest

上游两套测试类零改动全部通过:

测试类钉住什么
ThemeControllerTest控制器状态机:模式切换、显式主题、family 变体自动切换、默认回退链
ThemeRegistryTest注册表语义:注册顺序、按 id 取主题、族注册联动

依赖换线(coroutines 1.9.0→1.10.2-1.0.0、serialization 1.6.3→1.9.1-1.0.0)后上游测试照跑全绿,commonMain 只用稳定 API,无漂移。

6.3 构建与符号验证

:theme-core:compileKotlinJvm / jvmTest: BUILD SUCCESSFUL
compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL in 58s
copyKnSoArm64 / copyKnSoX64: 三件套落 harmonyApp/entry/libs/<abi>/
llvm-nm -D: KmpaletteTheme / libohoskmpalette_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libkmpalette_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully / start ability successfully

libohoskmpalette.so debugShared(未 strip,含 Kotlin/Native 运行时与 coroutines)arm64 约 6.8MB、x86_64 约 6.3MB,配 libc++_shared.so 约 1.8MB / 1.7MB;HAP 内每个 ABI 三件套齐全。

6.4 功能用例

  1. 启动后整页颜色来自引擎解析的默认主题(red_flame 族,上游 Demo 同款默认),明暗胶囊跟随系统;
  2. 点 Light 胶囊 → setMode(LIGHT),状态行 mode=LIGHT,页面保持 light 变体;点 Dark → 当前族自动切 dark 变体(theme id 从 *_light 变 *_dark),整页换色;
  3. 点 9 个主题族任一行 → setTheme 传对应变体 id,按钮、卡片、滑条、进度、开关、输入框全部随 35 色 palette 换色,选中族出现高亮边框与对勾;
  4. SYSTEM 模式下在系统设置切深色/浅色 → 媒体查询触发 notifySystemDark,页面立即换变体;Light/Dark 显式模式下不换(与上游语义一致);
  5. 选 Dark + ocean_blue 后杀进程重启 → 首屏恢复 mode=DARK 与 ocean_blue,持久化闭环成立;
  6. 状态行实时回显 theme=… mode=… explicit=… dark=…,与 hdc shell hilog | grep Kmpalette 输出一致;
  7. 输入框、滑条、开关等交互控件的操作色(primary/surfaceVariant 等)随主题实时变化,无硬编码残色。

6.5 验证结论

主题引擎、序列化运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的动态主题会话:9 族切换、模式三段、系统深浅色联动与重启持久化闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/KMPalette-Theme-Library.git
cd KMPalette-Theme-Library
export JAVA_HOME="C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
./gradlew :theme-core:compileKotlinJvm :theme-core:jvmTest        # klib + 交叉验证
./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.kmpalette -m entry
hdc shell hilog | grep Kmpalette

八、遗留问题与改进方向

8.1 踩坑复盘

  1. gradlew.bat 启动 Gradle 需要环境变量 JAVA_HOME——org.gradle.java.home 只管 Gradle 进程用的 JDK,不管启动器;Git Bash 里 export JAVA_HOME=... 后管道会掩盖真实退出码,排障时要去掉管道看原始输出;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. HarmonyOS Kotlin 2.2.21 线的 kotlin.concurrent 有 AtomicInt / AtomicReference 但没有 AtomicBoolean——布尔门闸用 AtomicInt(0/1) 替代;
  4. @OptIn 用的 ExperimentalNativeApi 在 kotlin.experimental 包(既不是 kotlin.native 也不是 kotlin.native.concurrent);Worker.current.id 返回 Int 不是 Long;Kotlin 没有 \f 字符转义(写 '\u000C')——三个都是编译器硬错,本仓逐一实测后修正;
  5. 上游注册表嵌套加锁,ohos 的 PlatformLock actual 必须可重入,空实现或非重入锁直接死锁;
  6. eazytec 的 kotlinx 版本后缀必须与 Kotlin 线对齐(-1.0.0 对 2.2.21-1.0.0),Maven Central 官方制品没有 ohos 变体,写错仓库解析不到 klib;
  7. ArkTS 侧 preferences 的 API 形态与新版文档不同:getPreferencesSync 第二参是 { name: '...' } Options 对象而非字符串;get() 返回 Promise,同步读要用 getSync——直接照新文档写会连报两条编译错;
  8. ArkTS 编译器不允许跨 await 对组件属性做空值收窄(this.prefs 赋值后再调用仍报 possibly null)——改走局部 const;
  9. build-profile.json5 的 signingConfigs 留空数组、勾选 DevEco 自动签名回填,证书材料绝不进入仓库;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 根构建的 /build/ 与 .hvigor 等产物目录要按 harmonyApp 嵌套路径单独 gitignore(example/harmonyApp/entry/libs 等),否则构建产物混进提交。

8.2 已知问题

  • Compose 适配层(DynamicThemeProvider / Material3Adapter / ThemePickerBottomSheet)未上鸿蒙,隔离在 composeMain;ArkTS Demo 承担选择器与适配层职责;
  • StateFlow<ThemeState> 不跨 C ABI:桥是同步一次一调用,订阅式推送过不了 C 签名,ArkTS 以操作后响应全量回读观察状态;
  • 桥的控制器是进程级单例(Demo 场景足够),多 store / 多控制器场景需扩展请求字段;
  • 首次调用有最多 2s 的持久化加载等待(实测毫秒级),超时则回落 SYSTEM 默认并照常工作;
  • android / ios / desktop / wasmJs 目标剥离(源文件留在树里),samples 留盘不进构建,发布链路剥离,本 fork 不发 Maven Central。

8.3 未来优化方向

  • 社区 CMP ohos 制品与上游版本对齐后,把 composeMain 8 文件移回 commonMain 接回完整 Compose 适配层;
  • 增加 registerFamily / register 的桥接入口,支持 ArkTS 动态注册自定义主题族;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 theme-core(commonMain 28 文件)
    → 盘点出 20 纯 Kotlin + 8 Compose 的边界
    → 验证 ohos kotlinx 制品线(coroutines / serialization,后缀与 Kotlin 线对齐)
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1,社区已审核线)
    → 根构建替换(cmp_qrose 模式)+ Compose 8 文件隔离 composeMain
    → palette 脚本纯化(9 文件 630 色,断言零漂移)
    → ohosArm64 / ohosX64 KLIB + PlatformLock 可重入 actual
    → C ABI(@CName × 1)+ 单例控制器 + JSON 全景
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + preferences + 媒体查询
    → ArkUI 真机单页验收(9 族切换 / 模式三段 / 系统深浅色联动 / 重启持久化)

9.2 封装层次

  • 引擎层:上游原版 ThemeRegistry / ThemeController / 35 色 Palette 模型 / 9 主题族 / pickDefault 回退链;
  • 数据层:palette 纯化后的 630 个颜色常量(脚本生成、断言校验);
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、preferences 持久化、媒体查询联动、请求分发与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线由库的 UI 依赖占比决定:KStore 全库零 UI,真编译整库是正解;KMPalette 一半是 Compose 适配层,内核隔离(composeMain)才是既保引擎又留接回能力的解——两条路线共享同一套工具链钉法与桥接纪律,但边界画法必须按库情裁衣;
  2. 数据文件的"零漂移"要靠工具而不是细心:630 个颜色常量的迁移用脚本生成 + 结构断言(每文件 70 色、2 个 scheme 块),转换可复跑、错误可在生成期暴露;
  3. expect/actual 是适配面的显微镜:全库 3 处 expect 里,PlatformLock 留核心暴露出"必须可重入"的真约束,另外两处随 Compose 移出后 ohos 零负担——先盘 expect 再定边界,比通读全部源码更快看清适配成本。

9.4 适配成果

当前适配已完成:

  • KMPalette 0.0.6(fork 版本 0.0.6-ohos.1)主题引擎的双 ABI Kotlin/Native 编译(引擎 20 文件零重写,palette 9 文件脚本纯化零漂移);
  • HarmonyOS kotlinx 制品线在主题库上的完整落地(coroutines 1.10.2-1.0.0 + serialization 1.9.1-1.0.0,ohos/jvm 双线);
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 单页验收页面(组件展示、模式三段、9 族列表、状态行、持久化、系统深浅色联动);
  • 上游测试零改动全绿的 JVM 交叉验证 + HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐