开源鸿蒙平台 CMP 三方库「shadcn」适配全流程
本文记录
shadcn-multiplatform接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的适配面盘点、鸿蒙工具链选型、源集隔离、设计令牌引擎、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
shadcn-multiplatform是构建在 compose-unstyled 原语上的 Compose Multiplatform shadcn/ui 组件库:按钮、弹窗、侧栏、表格……全部组件的颜色、尺寸、阴影、排版都来自一份 Figma 令牌导出(imports/design-tokens.json),再由构建期代码生成器变成 Compose 主题。本次适配与 KStore(真编译整库)是同一条社区路线的两个分支:KStore 的内核是纯 Kotlin 存储引擎,ohos 制品线补齐后可以原样编译;而 shadcn-multiplatform 是纯 Compose UI 库,鸿蒙 Kotlin 工具链(2.2.21-1.0.0)没有能与上游 CMP1.11.1/ Kotlin2.4.0对齐的 Compose 制品,组件代码跨不过工具链鸿沟。但这个库有一个天然的可移植内核——设计令牌引擎:Figma 导出是整个主题体系的单一事实源,语义色、色阶、圆角、阴影、排版全部是数据。因此本次适配把令牌引擎做成纯 Kotlin 运行时(运行时解析嵌入的令牌导出、归一{tw.colors.neutral.950}别名),全部 Compose 代码隔离进不参与编译的composeMain等工具链对齐后接回;ArkTS 应用通过 C ABI/N-API 桥一次性拉取全量令牌 JSON,直接驱动 ArkUI 写出 shadcn 风格界面,真机四 Tab 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/shadcn-multiplatform.git
上游仓库: jershell/shadcn-multiplatform
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
shadcn-multiplatform 为 Compose Multiplatform 应用提供整套 shadcn/ui 组件:commonMain 里是组件实现(40+ 个组件目录)与生成主题(GeneratedThemePalettes / GeneratedTypography 等),样式的唯一事实源是 Figma 令牌导出——mode 节按 light/dark 两套给出约 35 项语义色(background / primary / muted-foreground / sidebar* / chart*……),tw 节是 22 组 Tailwind 色阶(50…950),rdx 节是 33 组 Radix 色阶(1…12,light/dark 双份),tokens 节是命名尺寸,effect 节是 shadow-2xs..shadow-2xl 阴影(多层),typography 节是 text-xs..text-9xl 13 档 × 9 字重。KMP 项目把这些能力编译到 Android、iOS、桌面与 Web。OpenHarmony 应用同样需要这套设计体系,但上游构建(Kotlin 2.4.0 / Gradle 9.5.1 / AGP 9.1.0 / CMP 1.11.1)不会生成 OpenHarmony 变体。
UI 库的适配比存储库多一层障碍:引擎库的 commonMain 是纯 Kotlin,制品线补齐就能编译;UI 库的 commonMain 每一个文件都 import androidx.compose.*,而鸿蒙 Kotlin 是 2.2.21-1.0.0 的分支工具链——上游 Compose 1.11 需要 Kotlin 2.4 编译器,社区 CMP ohos 线只有 1.9.2 基线,两套对不齐,降级等于把全部组件重写一遍。适配必须回答"这个库跨过鸿沟的到底是什么"。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| Compose 制品缺失 | 上游 CMP 1.11.1 / Kotlin 2.4.0 与鸿蒙 Kotlin 2.2.21 无交集;社区 CMP ohos 线 1.9.2 基线不兼容,组件代码无法编译。 |
| 工具链不一致 | 上游 Gradle 9.5.1 / AGP 9.1.0 与鸿蒙 Kotlin 插件无法共存于一个 Gradle 构建。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 令牌语义跨不了 UI | Theme[ColorProps][ColorTokens.xxx]、resolve<N>Colors(variant, state) 是 Compose 侧概念,ArkTS 侧只能消费数据。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:设计令牌引擎的 ohos 编译(Figma 导出运行时解析 + 别名归一)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。组件渲染语义留在上游 Compose 代码里隔离保管,ArkTS 只负责拉取令牌、换算颜色通道和展示。
1.2 库提供的能力
Demo 提供一次真实的设计令牌会话,对应桥的全部输出段:
- 语义色两套:light / dark 各约 35 项(
background、foreground、primary、ring、sidebar*、chart*……),值格式 Figma 风格#RRGGBBAA; - Tailwind 色阶:22 组
50..950十一档(neutral/gray/blue/red……,单值的white/black不是色阶、语义色里已有); - Radix 色阶:33 组
1..12十二档,light / dark 双 mode; - 语义尺寸:
radius-xs..radius-4xl、radius-full、border-width、stroke-width({tokens.N}别名已在引擎内解析为数值); - 原始尺寸:
tokens节全部命名数值(Figma 的逗号小数键"0,5"归一为"0.5"); - 阴影:
shadow-2xs..shadow-2xl等令牌,多层的(如shadow-lg两层)拆成有序层列表,每层带offsetX/offsetY/radius/spread/color; - 排版:
text-xs..text-9xl13 档 × 9 字重(thin…black),每档带fontSize/lineHeight/letterSpacing/fontWeight/fontFamily等完整参数。
响应字段:
| 字段 | 作用 |
|---|---|
ok / mode | 成功标志与回显模式(请求 `{“mode”:“light” |
colors | light/dark 语义色映射,键与上游 ColorTokens 一一对应。 |
dims | 语义尺寸(radius-md → 8.0……)。 |
shadows | 令牌 → 层列表。 |
typography / scales / rdx | 排版矩阵、Tailwind 色阶、当前 mode 的 Radix 色阶。 |
error | ok:false 时的原因原文。 |
引擎侧全部能力(别名解析、双 mode 语义表、多层阴影、逗号小数归一)都在 :uikit 的 commonMain 里随编译进入 .so,JVM 侧保留 9 项 jvmTest 交叉验证用例(对照 Figma 导出与 Compose 生成主题双基准),证明 ohos .so 里执行的解析逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码纪律 | 上游 Compose 代码零改动,整体 git mv 进 composeMain(不编译,历史保留);新增源码只有令牌引擎一个包。 |
| 单一事实源 | 引擎运行时解析 design-tokens.json(TokenJson.kt 嵌入,:uikit:embedTokenJson 可再生),不手工抄值。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);demoApp 四端入口与 build-plugin 留盘不进构建。 |
| 桥接稳定 | 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | fork 零第三方依赖(手写 MiniJson),Kotlin 2.2.21-1.0.0 钉版来自社区已审钉法。 |
| UI 完整 | 单页四 Tab 覆盖全部桥输出:语义色彩 / 色阶 / 圆角与阴影 / 排版与组件(ArkTS 版 shadcn Button/Badge/Card)。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。鸿蒙侧交付的是设计令牌引擎——组件级 UI 等上游 Compose 与鸿蒙工具链对齐后从
composeMain接回;ArkTS Demo 证明的是"令牌数据原生可用 + 用它能直接搭出 shadcn 风格界面"。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose 制品缺口、确定"令牌引擎"交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:源集隔离 ── commonMain→composeMain、平台源集→compose* 目录、令牌导出嵌入
第 4 阶段:令牌引擎 ── MiniJson、{alias} 别名解析、ShadcnTokens 七个只读视图
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主与验证 ── 四 Tab 页面、HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
uikit/ 上游 UI 库(令牌引擎进 ohos 构建,jvm + ohosArm64 + ohosX64)
uikit/src/commonMain/.../tokens/ 新增:令牌引擎(MiniJson + ShadcnTokens + TokenJson 嵌入)
uikit/src/composeMain/ 上游全部 Compose 组件与主题代码,git mv 整体迁入,不参与编译
uikit/src/composeAndroid|composeJvm| 上游平台源集同步迁入(jvmMain→composeJvm、androidMain→composeAndroid、
uikit/src/composeIos|composeWeb| iosMain→composeIos、webMain→composeWeb、commonTest→composeTest、
uikit/src/composeTest|composeJvmTest/ jvmTest→composeJvmTest),与 composeMain 同等隔离
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson 请求解析)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
demoApp/ + build-plugin/ 上游四端 Demo 与 Figma 代码生成插件,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/ 本文章与真机效果图(doc/img/)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :uikit 与 :example:nativeApp,rootProject.name = "ohos_shadcn-multiplatform"。上游的 Compose / AGP / kotlinx-serialization / buildConfig / vanniktech 发布插件、build-plugin 组合构建、四个 demoApp 模块全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose 同款已审钉法) |
| JDK | 21(Temurin jdk-21.0.12.101-hotspot,org.gradle.java.home 钉死) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 9.5.1 降级;2.2.21 旧线不支持 Gradle 9,同时删除 Gradle 9 专属的 gradle-daemon-jvm.properties) |
| 依赖 | 零第三方依赖(令牌引擎不依赖 kotlinx 库;fork 的 libs.versions.toml 只剩 kotlin 一行) |
| DevEco product | 5.1.0(18) 兼容,hvigor modelVersion 5.1.0 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.shadcn |
export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot" # Windows/Git Bash
java -version
1.3 源码依赖度盘点
| 目录 | 文件数 | 依赖形态 | 处置 |
|---|---|---|---|
uikit/src/commonMain(上游) | 145 | 每个文件都 import Compose / compose-unstyled | 整体迁入 composeMain(不编译,git mv 历史保留) |
uikit/src/androidMain 等平台源集 | 5 | 平台 actual(ResizableCursor、平台依赖) | 同步迁入 compose* 目录 |
uikit/src/commonTest / jvmTest | 4 | Compose UI 测试 | 迁入 composeTest / composeJvmTest(不编译) |
imports/design-tokens.json | 1 | 数据(542KB,Figma Tokens 导出) | 嵌入引擎(TokenJson.kt 分块常量) |
expect 声明全库只有 1 处(Modifier.resizableHoverCursor),四个平台 actual 全部随宿主代码隔离;ohos 侧不需要任何 actual——令牌引擎是纯 Kotlin,零 expect。上游的 build-plugin(importThemeFromFigmaTokens 代码生成)留在树里但退出构建:它的产物(Generated*.kt)已经在 composeMain 里静态保管,鸿蒙侧的对应物是运行时解析同一份 JSON。
1.4 页面边界
标题区 ShadcnTokens · OpenHarmony / 副标题(shadcn/ui 设计令牌 · Kotlin/Native ohosArm64/ohosX64)
模式行 深色模式 Toggle(切换即以另一 mode 重新请求桥,全页重渲染)
状态行 令牌统计(如 "dark 模式 · 语义色 35 项 · 色阶 55 组 · 阴影 14 层")
Tab 切换 语义色彩 / 色阶 / 圆角与阴影 / 排版与组件
语义色彩 35 项色块网格(色块 + 键名 + hex 原文),按主题自身的 background/foreground 着色
色阶 tw 22 组(50..950)与 radix 33 组(1..12)选择器 + 色带
圆角与阴影 radius-xs..radius-4xl 圆角方块 + shadow 令牌卡片(ArkTS 单层 .shadow() 取第一层)
排版与组件 text-xs..text-4xl 字号字重样例 + ArkTS 版 shadcn Button(五变体)/ Badge / Card
页面启动时 aboutToAppear 以 light 模式请求桥,统计行数字全部来自真实 Native 响应而非静态文字。
第 2 阶段:目标与依赖打通
2.1 制品缺口验证(本适配的路线分岔点)
先验证生态:eazytec 社区 Maven 上有没有能与上游对齐的 Compose ohos 制品。结论是没有——社区 CMP ohos 线是 1.9.2 基线(配合旧 Kotlin),上游是 CMP 1.11.1 / Kotlin 2.4.0 / compose-unstyled 2.9.0,版本矩阵无交集。对照 KStore 的经验(kotlinx 制品线补齐后真编译整库),UI 库此刻的正确路线不是等制品,而是回到 cmp_qrose 已审模式:隔离 UI、把数据内核带过去。这也决定了工具链选择——没有新制品线的牵引,就用社区最早批准的钉法 Kotlin 2.2.21-1.0.0。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
settings.gradle.kts 把 eazytec 双仓写在 pluginManagement 与 dependencyResolutionManagement 最前(顺序错了配置期直接 Unresolved reference: ohosArm64):
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_shadcn-multiplatform"
include(":uikit")
include(":example:nativeApp")
Gradle 从上游 9.5.1 降到 8.14.1:2.2.21 旧线的 Kotlin Gradle 插件不支持 Gradle 9。降级伴随两个清理:删除 Gradle 9 专属的 gradle/gradle-daemon-jvm.properties(daemon JVM criteria 会按文件里的 foojay 链接拉 JDK,与钉死的 org.gradle.java.home 冲突),gradle.properties 收敛为 qrose 同款键集(kotlin.native.distribution.downloadFromMaven=true 等)。这与 KStore 的"2.4.20 线沿用 9.x"相反——Gradle 策略跟着工具链线走,两条线的经验不能互相套用。
gradle/libs.versions.toml 精简到一行插件:
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.3 编译令牌引擎
uikit/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游构建里的 android namespace、Compose resources、buildConfig、vanniktech 发布段全部随重写消失。实测:
./gradlew :uikit:compileKotlinJvm :uikit:compileKotlinOhosArm64 :uikit:compileKotlinOhosX64 # BUILD SUCCESSFUL
commonMain 里现在是令牌引擎一个包,纯 Kotlin 零依赖,三平台同一份代码。
第 3 阶段:源集隔离
3.1 迁移矩阵
上游 commonMain 的 145 个 Kotlin 文件加 composeResources 全部 git mv(历史保留,rename 检出 100%):
uikit/src/commonMain/kotlin/** → uikit/src/composeMain/kotlin/** 全部 Compose 组件与主题
uikit/src/commonMain/composeResources/ → uikit/src/composeMain/composeResources/
uikit/src/androidMain/ → uikit/src/composeAndroid/
uikit/src/jvmMain/ → uikit/src/composeJvm/
uikit/src/iosMain/ → uikit/src/composeIos/
uikit/src/webMain/ → uikit/src/composeWeb/
uikit/src/commonTest/ → uikit/src/composeTest/ (Compose UI 测试)
uikit/src/jvmTest/ → uikit/src/composeJvmTest/
composeMain 不是声明的源集——它只是磁盘目录,任何目标都不编译它。这样上游代码一个字节不改地保留,等 Compose 工具链对齐后把目录改回名字即可接回。demoApp/(四端入口)与 build-plugin/(Figma 代码生成)同样留盘不进 settings.gradle.kts。
3.2 令牌导出嵌入(踩到 JVM 常量池上限)
引擎的数据源是 imports/design-tokens.json(542KB,Figma Tokens 导出)。Kotlin/Native 的资源加载在 sharedLib 形态下不可靠,所以选择编译期嵌入:新增 :uikit:embedTokenJson 任务把 JSON 转成 TokenJson.kt 常量。第一版单常量直接撞 JVM class 文件上限——常量池单条 UTF8 最大 65535 字节,编译器报 UTF8 string too large。修正为分块常量 + 懒拼接:按 15000 字符切块(最坏 3 字节/字符仍小于上限),每块一个 const,运行时 joinToString(""):
// uikit/src/commonMain/.../tokens/TokenJson.kt(生成文件,37 块)
internal val TOKEN_JSON: String by lazy { TOKEN_CHUNKS.joinToString("") }
private val TOKEN_CHUNKS: Array<String> = arrayOf(
"""{
"font": {
""",
// …… 共 37 块
)
任务里两个细节:剥离 UTF-8 BOM;$ 替换为 ${'$'}(原始字符串里只有 $ 会触发插值,分块后转义永不跨块)。换令牌导出后重跑一次任务即可再生。
第 4 阶段:令牌引擎(技术难点)
4.1 Figma 导出的真实形状
动手写引擎前先把 542KB 导出的六个节摸清(这步不能省,别名规则全藏在这里):
| 节 | 内容 | 示例 |
|---|---|---|
mode | light/dark 两套语义令牌,颜色和尺寸混在同一节 | primary = {tw.colors.neutral.900}、radius-md = {tokens.8}、semantic-background = #696867ff(裸 hex) |
tw.colors | 22 组 Tailwind 色阶 + 单值 white/black | neutral.900 = #171717ff |
rdx.colors | light mode / dark mode 两子节,各 33 组 1…12 | light mode.blue.8 = #5eb1efff |
tokens | 命名数值,小数键用逗号(Figma 的 locale 怪癖) | "8" = 8、"0,5" = 0.5、"-0,8" = -0.8 |
effect["box shadow"] | 阴影令牌,单层是对象、多层是 "0" "1" 编号子节点 | shadow-lg 两层(radius 6/15、offsetY 4/10、spread -4/-3) |
typography | 13 档 × 9 字重,每属性是 {type,value} 包装 | text-xs.regular.fontSize = {dimension,12} |
三条别名规则:{tw.colors.neutral.950} 从根按段下行;{tokens.8} 同法落进 tokens 节;{rdx.colors.black.5} 是无模式 rdx 别名——rdx.colors 下只有 light mode / dark mode 两个子节,没有直接的 black,必须回退进 light mode 子节解析(上游 Figma 插件生成的代码同样用 lightMode 变体,行为一致)。
4.2 MiniJson 与 ShadcnTokens
引擎两个文件。MiniJson.kt 是手写递归下降 JSON 解析器(对象/数组/字符串/数字/布尔/null,处理转义与 Unicode),不引 kotlinx.serialization——fork 零第三方依赖,.so 自包含(与 qrose 同款取舍)。ShadcnTokens.kt 是公共 API,七个只读视图:
public object ShadcnTokens {
public fun semanticColors(mode: ShadcnTokenMode): Map<String, String> // 35 项 #RRGGBBAA
public fun semanticDimensions(mode: ShadcnTokenMode): Map<String, Double> // radius-md → 8.0
public fun dimensions(): Map<String, Double> // tokens 节,"0,5"→"0.5" 归一
public fun shadows(): Map<String, List<ShadcnShadowLayer>> // 令牌 → 层列表
public fun typography(): Map<String, Map<String, ShadcnTypographyStyle>> // 13 档 × 9 字重
public fun colorScales(): Map<String, Map<String, String>> // tw 22 组
public fun radixScales(mode: ShadcnTokenMode): Map<String, Map<String, String>> // rdx 33 组
}
别名解析的通用下行器(含 rdx 无模式回退):
private fun resolveAlias(path: String): Any? {
var node: Any? = root
for (segment in path.split('.')) {
val map = node.asMap() ?: return null
node = map[segment] ?: map["light mode"].asMap()?.get(segment) ?: return null
}
return node
}
阴影解析同时兼容单层(type == "custom-shadow" 直接取 value)与多层(编号子节点按数值排序逐层取);语义尺寸把 {tokens.8} 解析成数值;排版把每属性的 {type,value} 包装拆开。
4.3 双基准交叉验证
commonTest 里 9 项用例把引擎钉在两个基准上:Figma 导出真值(light primary = #171717ff = neutral.900、dark background = #0a0a0aff、radius-md = 8、shadow-lg 两层 6/15、text-xs.regular 12/16/400)与 Compose 生成代码(GeneratedThemePalettes 的 dark primary 正是 TwColors.neutralToken200,与 JSON 的 {tw.colors.neutral.200} 一致)。JVM 与 ohos 编的是同一份引擎,jvmTest 全绿即"内核没漂"。
第 5 阶段:原生桥接
5.1 三层桥接
ArkTS
│ JSON string({"mode":"dark"})
▼
C++ N-API entry(libshadcn_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosshadcn.so)
│ ShadcnRender
▼
UTF-8 JSON + DisposeString
令牌是静态数据,一次性全量拉取比逐项查询合理——桥只导出一个函数,请求里唯一字段是 mode。
5.2 Kotlin/Native 导出函数
@CName("ShadcnRender")
fun shadcnRender(request: String): String = try {
val mode = if (MiniJson.parse(request).str("mode").equals("dark", true))
ShadcnTokenMode.DARK else ShadcnTokenMode.LIGHT
render(mode) // colors/dims/shadows/typography/scales/rdx 六段拼装
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ShadcnRender | shadcnRender(req) | 七个引擎视图的完整 JSON 快照(light/dark 二选一) |
请求解析沿用 cmp_qrose 审核模式的扁平 MiniJson(顶层字符串字段),响应六段由手写 JSON writer 拼装(转义、Double/Int 格式与 ArkTS JSON.parse 对齐)。异常整体 try/catch 收进 ok:false 通道,Kotlin 异常永不跨 FFI。
5.3 C++ N-API 与 CMake
napi_property_descriptor desc[] = {
{"shadcnRender", nullptr, NapiShadcnRender, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调三步:napi_typeof 校验 string、OH_LOG_INFO 打字节数、调 ShadcnRender(const char*) 包装返回。KN 返回字符串必须经 libohosshadcn_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosshadcn_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号)。CMake 侧 IMPORTED 引入 KN so(IMPORTED_NO_SONAME TRUE)、$ORIGIN rpath、-Wl,--no-undefined、链接 libace_napi.z.so + libhilog_ndk.z.so,OHOS_ARCH 为空或 KN so 缺失直接 FATAL_ERROR(提示先跑 link 任务),POST_BUILD 把 KN so 与 libc++_shared.so 拷到 NAPI so 旁边。
Gradle 侧 example/nativeApp 出双 ABI sharedLib(baseName = "ohosshadcn"),copyKnSoArm64 / copyKnSoX64 被 linkDebugShared* finalizedBy,把 libohosshadcn.so 连同 ~/.konan/dependencies 里对应 triple 的 libc++_shared.so 一起回填 harmonyApp/entry/libs/{arm64-v8a,x86_64}/。
5.4 颜色通道换算(ArkTS 侧最大的坑)
桥输出的颜色保持 Figma 字节序 #RRGGBBAA(#0000001a 是 10% 透明度的黑),而 ArkUI ResourceColor 要 #AARRGGBB。Index.ets 里一个 toArgb() 统一换算(容错 6 位 #RRGGBB 补 ff),所有颜色——包括阴影层颜色——展示前都必须经过它。
第 6 阶段:ArkUI 宿主与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosshadcn)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 1 + 扁平 MiniJson(cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.shadcn、versionName 1.0.3-dev-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/libohosshadcn_api.h
├── src/main/cpp/types/libshadcn_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :uikit:jvmTest # 9 项交叉验证
./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 保持空数组,勾选后由 IDE 回填)。
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.shadcn -m entry
四、完整代码对照
4.1 整体架构
imports/design-tokens.json(Figma 导出,单一事实源)
│ :uikit:embedTokenJson 分块嵌入(37 块,绕 65535 常量池上限)
uikit/src/commonMain/.../tokens/**(令牌引擎,纯 Kotlin)
│ MiniJson 解析 + {alias} 归一 + 七个只读视图
Bridge.kt + 扁平 MiniJson
│ ShadcnRender(@CName × 1)
libshadcn_napi.so
│ shadcnRender(request) → JSON
Index.ets
│ JSON.parse + toArgb 通道换算 + @State 更新
▼
ArkUI 真机四 Tab 页面(语义色彩 / 色阶 / 圆角与阴影 / 排版与组件)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_shadcn-multiplatform、demoApp/build-plugin 移出 |
gradle/libs.versions.toml | 只剩 Kotlin 2.2.21-1.0.0 一行插件 |
build.gradle.kts(根) | 版本 1.0.3-dev-ohos.1、全局禁用 Ohos*Test* 任务 |
uikit/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,embedTokenJson 任务 |
uikit/src/commonMain/.../tokens/MiniJson.kt | 手写 JSON 解析器(fork 零依赖的关键) |
uikit/src/commonMain/.../tokens/ShadcnTokens.kt | 七视图 API + 别名解析 + 逗号小数归一 |
uikit/src/commonMain/.../tokens/TokenJson.kt | 生成文件:37 块常量 + 懒拼接 |
uikit/src/composeMain/ 等 | 上游全部 Compose 代码隔离保管(不编译,git mv 历史保留) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、六段 JSON 拼装、错误通道 |
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 四 Tab 验收页面 + toArgb |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(23 节) |
doc/img/shadcn-tokens-demo-dark.png | 真机效果图 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | ShadcnTokens.semanticColors(mode) | light/dark 语义色(≈35 项,#RRGGBBAA) |
| 引擎 | ShadcnTokens.semanticDimensions(mode) | radius-* 族、border-width 等(别名已解析) |
| 引擎 | ShadcnTokens.dimensions() | tokens 节原始数值("0,5" → "0.5" 归一) |
| 引擎 | ShadcnTokens.shadows() | 阴影令牌 → 有序层列表 |
| 引擎 | ShadcnTokens.typography() | text-xs..text-9xl × 9 字重 |
| 引擎 | ShadcnTokens.colorScales() / radixScales(mode) | Tailwind 22 组 / Radix 33 组 |
| 上游对照 | Theme[ColorProps][ColorTokens.primary] | Compose 侧同义概念(隔离在 composeMain) |
| Native | ShadcnRender | C ABI JSON 导出(唯一符号) |
| N-API | shadcnRender(req) | 向 ArkTS 暴露能力 |
| ArkTS | toArgb(figmaHex) | #RRGGBBAA → #AARRGGBB 通道换算 |
4.4 ArkTS 与 Kotlin 的边界
const raw = shadcnRender(JSON.stringify({ mode: this.isDark ? 'dark' : 'light' }));
const res = JSON.parse(raw) as TokenResponse;
// res.colors.primary === '#171717ff'(light)—— 展示前经 toArgb() 换算
@CName("ShadcnRender")
fun shadcnRender(request: String): String = try {
val mode = parseMode(request)
buildString {
append("{\"ok\":true,\"mode\":\""); append(mode.json)
append("\",\"colors\":").append(colorsJson(mode))
/* dims / shadows / typography / scales / rdx 五段同法拼装 */
append("}")
}
} catch (t: Throwable) { errorJson(t) }
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:源集隔离而不是降级到社区 CMP 1.9.2
把整库降到 CMP 1.9.2 基线意味着重写全部组件对 compose-unstyled 2.9 / nav3 / 1.11 API 的依赖——那是另一个项目,不是适配。隔离方案让上游代码零改动保管在 composeMain(git mv 历史完整),鸿蒙侧交付当前真正能用的部分(令牌引擎);等上游 Compose 与鸿蒙工具链版本对齐,把目录名改回去即可整体接回。
决策 2:令牌引擎运行时解析 Figma 导出,而不是手工抄值
design-tokens.json 是上游钦定的单一事实源(AGENTS 源头层级第一条)。手工把 542KB 抄成分发数据,上游每次重导出都要人工同步,必然漂移。嵌入 + 运行时解析让"换令牌"退化为"重跑一次 embedTokenJson";别名归一({tw.colors.*} / {rdx.colors.*} / {tokens.*})在引擎内一处实现,桥与 ArkTS 拿到的永远是已解析值。
决策 3:Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(cmp_qrose 已审钉法)
本库没有 kotlinx 制品线牵引(引擎零依赖),没有理由离开社区最早批准的钉法。代价是 Gradle 必须从 9.5.1 降 8.14.1 并删除 gradle-daemon-jvm.properties——与 KStore 的 2.4.20/9.x 组合相反,再次证明 Gradle 策略跟着工具链线走。
决策 4:手写 MiniJson,不引 kotlinx.serialization
fork 的依赖表只剩 Kotlin 一行:引擎零第三方库、.so 自包含、没有制品线可达性问题。解析对象是自家导出的固定形状 JSON,手写递归下降约 200 行,与 qrose 桥的取舍一脉相承。
决策 5:引擎放 :uikit commonMain 而不是塞进 nativeApp
引擎归库、桥归示例::uikit 出的 KLIB 未来可以被任何鸿蒙 Kotlin 消费者复用(不止这一个 Demo);jvm() 目标与 ohos 编同一份引擎,9 项 jvmTest 交叉验证才成立。nativeApp 只做协议换装(Kotlin 视图 → JSON),没有业务逻辑。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 10(x64)、JDK 21(Temurin,org.gradle.java.home 钉死)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与 HarmonyOS 真机(arm64)。
6.2 JVM 交叉验证
./gradlew :uikit:jvmTest
9 项用例全部通过(对照 Figma 导出与 Compose 生成主题双基准):
| 用例 | 钉住什么 |
|---|---|
lightSemanticColorsMatchFigmaExport | light 语义色 7 项真值(含 rdx 无模式别名 background-color = #0000004d、裸 hex 透传) |
darkSemanticColorsMatchFigmaExport | dark 语义色 6 项真值(primary = neutral.200 = #e5e5e5ff,与生成代码一致) |
semanticColorsCoverThePalette | light/dark 键集合一致、数量 ≥ 35 |
semanticDimensionsMatchFigmaExport | radius 族 9 项 + border-width/stroke-width,light/dark 全等 |
rawDimensionsNormalizeCommaDecimalKeys | "0,5" 键归一为 "0.5" 且值为 0.5 |
shadowsAreLayeredLikeTheFigmaExport | shadow-xs 单层(radius 2/offsetY 1)、shadow-lg 两层(6/15、spread -4) |
typographyMirrorsTheComposeTextStyleScale | text-xs.regular 12/16/400/Inter、text-base.regular 16/24 |
tailwindScalesHaveElevenShades | neutral 11 档三真值;单值 white/black 被跳过 |
radixScalesHaveTwelveSteps | gray 12 档(1 = #fcfcfcff)、blue.8、dark gray.1 |
6.3 HAP 验证
:uikit:compileKotlinJvm / OhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ShadcnRender / libohosshadcn_symbols 均为全局 T 符号
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
hvigorw assembleHap: BUILD SUCCESSFUL(CAdapter 的 api.cpp:448 警告为已知无害项)
hdc install: install successfully
aa start: start ability successfully
debugShared 未 strip 的 libohosshadcn.so arm64 约 4.8MB、x86_64 约 4.5MB(含 Kotlin/Native 运行时与 542KB 嵌入令牌),每个 ABI 三件套(libshadcn_napi.so / libohosshadcn.so / libc++_shared.so)齐全。
6.4 功能用例
- 启动默认 light 模式,状态行显示真实统计(语义色 35 项 · 色阶 55 组 · 阴影 14 层),数字来自 Native 响应而非静态文字;
- 打开深色模式开关,页面以 dark 重新请求桥整体重渲染——背景变
#0a0a0aff、前景#fafafaff、危险色#f87171ff,与 JVM 用例断言逐字相同; - 语义色彩 Tab:35 项色块(背景/前景/主要/次要/静音/强调/危险/边框/输入/圆环/侧栏 8 项/图表 5 项……),每块附键名与 hex 原文;
- 色阶 Tab:Tailwind 22 组(50…950)与 Radix 33 组(1…12)切换,色带按档渲染;
- 圆角与阴影 Tab:
radius-xs..radius-4xl圆角方块(取dims数值为 vp)与 shadow 令牌卡片(ArkTS 单层.shadow()取第一层,多层与 spread 有文字提示); - 排版与组件 Tab:
text-xs..text-4xl字号字重样例 + ArkTS 版 shadcn Button 五变体 / Badge / Card,全部颜色圆角来自桥令牌——证明"拿令牌能直接搭出 shadcn 风格 ArkUI"; - 任何一环失败(理论不该发生)会走
ok:false通道在状态行显示错误原文。
6.5 验证结论
令牌引擎、嵌入数据、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的令牌会话:明暗双模式语义色、双体系色阶、圆角/阴影/排版矩阵和令牌驱动的 shadcn 风格组件(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/shadcn-multiplatform.git
cd shadcn-multiplatform
export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
./gradlew :uikit:compileKotlinJvm :uikit:compileKotlinOhosArm64 :uikit:compileKotlinOhosX64 # klib
./gradlew :uikit:jvmTest # 9 项交叉验证
./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.shadcn -m entry
hdc shell hilog | findstr Shadcn
八、遗留问题与改进方向
8.1 踩坑复盘
- UI 库适配的第一问是"内核是什么":shadcn-multiplatform 的可移植内核不是组件而是设计令牌——先盘点出这一点,路线(隔离 + 引擎)自然成立;
settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 2.2.21 旧线的 Kotlin 插件不支持 Gradle 9:除了 wrapper 降到 8.14.1,Gradle 9 专属的
gradle-daemon-jvm.properties必须删除,否则按 foojay 链接拉 JDK 与钉死的java.home打架; - JVM 常量池单条 UTF8 上限 65535 字节:542KB JSON 嵌成单常量,编译器报
UTF8 string too large(KCode 层面合法、ASM 写常量池时才炸);按 15000 字符分块 + 懒拼接解决,且$转义必须在分块之后做,保证转义序列永不跨块; - Figma 导出的
tokens节小数键是逗号("0,5"、"-0,8",locale 怪癖),引擎必须归一为"0.5";文件头还可能有 BOM,嵌入前要剥离; - rdx 无模式别名(
{rdx.colors.black.5})在rdx.colors下解析不到——那里只有light mode/dark mode两个子节;回退light mode子节与上游生成插件行为一致,light/dark 的background-color都指向同一值; - 颜色字节序是 ArkTS 侧最大的坑:桥输出 Figma 的
#RRGGBBAA,ArkUI 要#AARRGGBB,不换算透明度整体错位;一个toArgb()门卫统一处理,阴影层颜色同理; - 鸿蒙 CAdapter 不导出
@CName业务符号给 ArkTS,必须走 CMake NAPI 包装;KN 链接期api.cpp:448的-Wreturn-type警告是 CAdapter 已知无害项; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI(模拟器是 x86_64),每个 ABI 三件套缺一不可;KN so 依赖的libc++_shared.so要从~/.konan/dependencies一并拷入; - DevEco 打开工程后生成的
example/harmonyApp/.idea/、.clangd、.clang-tidy不在根锚定 gitignore 覆盖内,要单独追加,否则混进提交; - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内保持空数组不引用,证书绝不进入 AtomGit。
8.2 已知问题
- 鸿蒙侧交付的是设计令牌引擎,不是 Compose 组件:40+ 组件仍在
composeMain隔离,等上游 Compose 与鸿蒙 Kotlin 工具链版本对齐后接回; - 上游生成代码与 Figma 导出在
chart-1..3上有历史分歧(生成代码映射 tw blue 800/900/100,导出指向 radix blue 8/9/10,色值不同);引擎按上游钦定的"导出为唯一事实源"取 radix 一侧,分歧已在适配文档记录,等上游修正导出后重跑embedTokenJson即归一; - ArkTS
.shadow()不支持多层堆叠与 spread,Demo 渲染每令牌第一层并文字提示; - Tailwind 单值
white/black不是色阶,不在colorScales()(语义色表里已有对应项); - android / wasmJs / iOS 目标与四端 demoApp、Maven Central 发布一并剥离(源文件留树),本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.shadcn匹配的签名。
8.3 未来优化方向
- 评估
.har形态发布令牌引擎,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 基于
toArgb之上的令牌消费层沉淀一套 ArkTS 版 shadcn 组件主题(Button/Badge/Card 已在 Demo 验证); - 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
- 跟踪上游 Compose 的 ohos 支持进展,工具链对齐后把
composeMain整体接回。
九、总结
9.1 核心难点回顾
上游 shadcn-multiplatform(145 个 Compose 文件 + Figma 令牌导出)
→ 盘点适配面:Compose 制品缺失,内核 = 设计令牌
→ 工具链钉版 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1,cmp_qrose 已审钉法)
→ 根构建替换 + 源集隔离(commonMain→composeMain,平台源集→compose*,git mv 历史保留)
→ 令牌导出分块嵌入(37 块,绕 JVM 常量池 65535 上限)
→ 令牌引擎(MiniJson + {alias} 归一 + 七视图)
→ C ABI(@CName × 1)+ 全量令牌 JSON
→ C++ N-API(DisposeString 纪律)+ ArkTS toArgb 通道换算
→ ArkUI 真机四 Tab 验收(明暗双模式 / 色阶 / 圆角阴影 / 排版组件)
9.2 封装层次
- 引擎层:MiniJson 解析、别名归一、双 mode 语义表、多层阴影、逗号小数归一、七个只读视图;
- 数据层:
TokenJson.kt分块常量(embedTokenJson可再生,Figma 导出仍是唯一事实源); - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、模式切换重请求、
toArgb换算、令牌驱动的 shadcn 风格组件; - DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 库适配的答案不是翻译组件,而是找出"单一事实源"带过工具链鸿沟:shadcn 的样式体系全部溯源于一份 Figma 导出,令牌过河,组件留在原地等工具链——两边的代码都没有重写,这是"零漂移"的唯一保证;
- Gradle 策略跟着工具链线走:2.2.21 旧线降 8.14.1(本次)、2.4.20 新线留 9.x(KStore),两条线的经验不能互相套用;降级时还要清理 Gradle 9 专属配置文件;
- 交叉验证钉双基准:9 项
jvmTest同时对照 Figma 导出真值与 Compose 生成代码,darkprimary= neutral.200 这种"生成代码与导出一致"的断言,才是"引擎没漂、上游也没漂"的完整证据链。
9.4 适配成果
当前适配已完成:
- 设计令牌引擎的双 ABI Kotlin/Native 编译(约 35 项语义色 × 2 套、22 组 Tailwind 色阶、33 组 Radix 色阶、语义/原始尺寸、11 个阴影令牌 14 层、13 档 × 9 字重排版,全部运行时解析自嵌入的 Figma 导出);
- 上游 145 个 Compose 文件零改动隔离保管(
git mv历史 100% 保留,工具链对齐后可整体接回); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面(明暗切换、语义色彩、双体系色阶、圆角阴影、排版与 shadcn 风格组件);
- 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)