开源鸿蒙平台 KMP 三方库「KMPalette」适配全流程
本文记录
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 字段 Material3Palette颜色 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 层强绑定 Compose | commonMain 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)。 |
families | 9 个主题族的 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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin 21,Windows) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.9;2.2.21 线社区钉法即 8.14.1) |
| kotlinx | coroutines 1.10.2-1.0.0、serialization-json 1.9.1-1.0.0(ohos 制品线,后缀与 Kotlin 线对齐) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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 核心 | 20 | coroutines + serialization,零 UI(model/tokens、registry、controller、ThemeStore/SystemThemeProvider 接口、themes/Utils) | 整体编译(jvm + 双 ohos) |
| palette 数据文件 | 9(属上表 20 之列) | lightColorScheme() / darkColorScheme() / Color() 构造后 toPalette() | 脚本纯化为直接 Palette(0xFF…L) 构造 |
| Compose 适配层 | 8 | androidx.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 方法 | 覆盖内核 |
|---|---|---|
KmpaletteTheme | kmpaletteTheme(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.toml | Kotlin 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.kt | ReentrantLock actual |
tools/convert_palettes.py | palette 纯化脚本(70 色/文件断言,零漂移) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、单例控制器、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(25 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | DefaultThemeRegistryFactory.createWithDefaults() | 预置 9 族注册表(桥单例构建) |
| 引擎 | ThemeController.state | StateFlow<ThemeState> 订阅(不跨 C ABI,桥取 .value) |
| 引擎 | setMode / setExplicitTheme | 模式与显式主题,family 变体自动切换 |
| 引擎 | notifySystemDarkChanged | 系统深浅色输入(ArkTS 媒体查询驱动) |
| 引擎 | ThemeStore(接口) | 持久化接缝,桥内为内存实现、ArkTS preferences 回灌 |
| Native | KmpaletteTheme | C ABI JSON 导出(单符号,签名永不变) |
| N-API | kmpaletteTheme(req) | 向 ArkTS 暴露能力 |
| ArkTS | send(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 功能用例
- 启动后整页颜色来自引擎解析的默认主题(red_flame 族,上游 Demo 同款默认),明暗胶囊跟随系统;
- 点 Light 胶囊 →
setMode(LIGHT),状态行mode=LIGHT,页面保持 light 变体;点 Dark → 当前族自动切 dark 变体(themeid 从*_light变*_dark),整页换色; - 点 9 个主题族任一行 →
setTheme传对应变体 id,按钮、卡片、滑条、进度、开关、输入框全部随 35 色 palette 换色,选中族出现高亮边框与对勾; - SYSTEM 模式下在系统设置切深色/浅色 → 媒体查询触发
notifySystemDark,页面立即换变体;Light/Dark 显式模式下不换(与上游语义一致); - 选 Dark + ocean_blue 后杀进程重启 → 首屏恢复
mode=DARK与 ocean_blue,持久化闭环成立; - 状态行实时回显
theme=… mode=… explicit=… dark=…,与hdc shell hilog | grep Kmpalette输出一致; - 输入框、滑条、开关等交互控件的操作色(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 踩坑复盘
gradlew.bat启动 Gradle 需要环境变量JAVA_HOME——org.gradle.java.home只管 Gradle 进程用的 JDK,不管启动器;Git Bash 里export JAVA_HOME=...后管道会掩盖真实退出码,排障时要去掉管道看原始输出;settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- HarmonyOS Kotlin 2.2.21 线的
kotlin.concurrent有AtomicInt/AtomicReference但没有AtomicBoolean——布尔门闸用AtomicInt(0/1)替代; @OptIn用的ExperimentalNativeApi在kotlin.experimental包(既不是kotlin.native也不是kotlin.native.concurrent);Worker.current.id返回Int不是Long;Kotlin 没有\f字符转义(写'\u000C')——三个都是编译器硬错,本仓逐一实测后修正;- 上游注册表嵌套加锁,ohos 的
PlatformLockactual 必须可重入,空实现或非重入锁直接死锁; - eazytec 的 kotlinx 版本后缀必须与 Kotlin 线对齐(
-1.0.0对2.2.21-1.0.0),Maven Central 官方制品没有 ohos 变体,写错仓库解析不到 klib; - ArkTS 侧
preferences的 API 形态与新版文档不同:getPreferencesSync第二参是{ name: '...' }Options 对象而非字符串;get()返回 Promise,同步读要用getSync——直接照新文档写会连报两条编译错; - ArkTS 编译器不允许跨
await对组件属性做空值收窄(this.prefs赋值后再调用仍报 possibly null)——改走局部 const; build-profile.json5的signingConfigs留空数组、勾选 DevEco 自动签名回填,证书材料绝不进入仓库;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 根构建的
/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 制品与上游版本对齐后,把
composeMain8 文件移回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 三条经验
- 适配路线由库的 UI 依赖占比决定:KStore 全库零 UI,真编译整库是正解;KMPalette 一半是 Compose 适配层,内核隔离(composeMain)才是既保引擎又留接回能力的解——两条路线共享同一套工具链钉法与桥接纪律,但边界画法必须按库情裁衣;
- 数据文件的"零漂移"要靠工具而不是细心:630 个颜色常量的迁移用脚本生成 + 结构断言(每文件 70 色、2 个 scheme 块),转换可复跑、错误可在生成期暴露;
- 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+ serialization1.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。
参考文档
更多推荐


所有评论(0)