开源鸿蒙平台 CMP 三方库「AestheticDialogs」适配全流程
本文记录
AestheticDialogs接入 OpenHarmony 的完整过程,覆盖对话框设计系统盘点、模型/Token 核心移植、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机/真机验收。
AestheticDialogs是一个 Jetpack Compose 对话框设计系统(上游2.0.0,Apache-2.0):三层 UI 架构(Component → Variant → Primitive → Tokens),七个组件家族(Alert / Confirmation / Selection / Input / Content / Feedback / Notification)共用一套框架、一套主题与一套无障碍保证;1.x 时代的八种装饰对话框在 2.0 里收敛为 Feedback(Flat / Flash)与 Notification(Toaster / Rainbow / Connectify / Emoji / Emotion)两个家族。本次适配与存储/编码类库(KStore / qrose)有本质区别:对话框是交互式 UI 而非静态数据,渲染必须活在帧率循环里,所以适配切面选在"模型 → 渲染规格"——上游的 UiModel 层(永不持 lambda)、配色引擎、tone 渐变、间距/圆角/字号/动效刻度被移植为纯 Kotlin 编译成 OpenHarmony 动态库,每次打开对话框时 ArkTS 调桥拿回完整规格 JSON,再用 ArkUI 组件绘制;哪个颜色、多大圆角、什么渐变、按钮怎么随 tone 变,全部由 Kotlin 核心计算,ArkTS 只做布局与交互,不定义任何设计值。
项目地址: AtomGit/oh-tpc/AestheticDialogs.git
上游仓库: gabriel-TheCode/AestheticDialogs
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 CMP 适配
AestheticDialogs 2.0 为 Android 应用提供"对话框"这一件事的设计系统答案:AestheticAlertDialog(uiModel, onSignal) 一行弹窗,视觉决策(状态色、渐变、圆角、间距、字号、动效时长)全部由 Token 层输出;DialogTone 五档语义(Success / Error / Warning / Info / Neutral)驱动所有家族的状态表达;withBrand() 四个参数一行换品牌且状态色不可品牌化;暗色主题是同一套角色的纯重映射。上游把这些能力实现在 Jetpack Compose 上(AGP 9.2.1 / Kotlin 2.4.10 / Gradle 9.7.0),OpenHarmony 应用同样需要这套对话框语言,但上游构建不会生成 OpenHarmony 变体。
只把示例页面改成 ArkTS 是不够的:页面能画出来,却无法证明设计系统的决策引擎(tone → 按钮色映射、渐变 ramp、暗色重映射、token 刻度)真的在 OpenHarmony 设备上运行。适配需要同时验证模型层、配色引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 上游不是 KMP | 纯 Android 库(com.android.library + Compose 插件),没有 commonMain,不能像 KStore 那样"整体编译"。 |
| 模型层被 androidx 注解污染 | @Immutable、ImageVector、KeyboardType、Compose Color / Dp / TextStyle / Easing 散在模型与 token 文件里。 |
| 渲染层过不了 CMP 版本鸿沟 | 社区鸿蒙 CMP 在 1.9.2 一线,上游依赖的 Compose 运行时远新于此。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交互语义跨不了桥 | signal 回调、dismissOnBackPress / dismissOnClickOutside 双开关、软键盘避让是 UI 侧概念,桥只能同步一来一回。 |
| 工具链不一致 | 上游 Gradle 9.7.0 / AGP 9.2.1 与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:模型层与 Token 层的 ohos 编译(包名与文件名保持上游原样,色值/间距/混合系数逐字保留)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。渲染由 ArkTS 按核心输出的规格完成;上游 Android 模块(aestheticdialogs/、app/)留在仓库里零改动,随时可与上游 diff 对账。
1.2 库提供的能力
Demo 覆盖上游 Catalog 的全部入口,13 个对话框场景 + 5 种横幅 + 5 种横幅动画:
- Confirmation(Default / Destructive):一句问话两个答案;destructive 的确认键保证用 error 色填充,
isConfirming时按钮转圈并锁全对话框(Demo 模拟 1.5s 请求),完成后自动弹 Toaster 横幅; - Alert(Default):icon / title / message + 主次动作,tone 决定状态徽章;覆盖错误/离线/权限三种模式、glyph 覆盖(把 info 换成 search)、长文本滚动与返回键拦截;
- Selection(Single / Multiple):radio 行 / checkbox 行、搜索框、禁用行、空态、多选提交;
- Input(Text / Password):
errorText错误态、确认禁用、密码显隐切换; - Content(Default):中部内容 slot + 框架(自适应宽度、scrim、dismiss 契约、动作行);
- Feedback(Flat / Flash):Flat 安静卡片 + tone 徽章;Flash tone 渐变满填充 + 反色文案(“loud one”);
- Notification(Toaster / Rainbow / Connectify / Emoji / Emotion):五种横幅轮廓 + Slide / Fade / Scale / SlideHorizontal / None 五种出入场动画,顶/底锚点、4s 自动消失;
- 全局:暗色 Switch(切换即重发桥请求
mode:"dark")、TONE 五档循环药丸、桥状态卡(显示最近一次调用的 ok / kind / tone / scrim / surface / radius / elevation 实值——验收"核心真的在跑"的直接证据)。
桥响应(规格 JSON)的主要字段:
| 字段 | 作用 |
|---|---|
colors / toneColors | 完整语义配色(surface / content / border / action 四组角色)与当前 tone 的四角色。 |
toneGradient / toneRim | 三档渐变 ramp(stop 0 / 0.5 / 1),由 lerp(accent,inverse,0.22) → accent → lerp(accent,primary,0.14) 生成,混合系数与上游 ToneStyling.kt 逐字一致;垂直版供 Flash 填充,水平版供 Connectify 顶条。 |
typography / spacing / shapes / dimens / frame / banner | 六档字号、4dp 间距网格、圆角刻度、控件尺寸、宽度桶(480/560)、横幅规格——全部来自上游 tokens。 |
motion | 时长三档(120/220/320ms)+ 出入场 cubic-bezier 字符串。 |
actions / confirmStyle | 四种 emphasis 的按钮样式,以及按当前对话框解析出的确认键样式(tone → 按钮色映射这层设计决策留在 Kotlin)。 |
model | 规范化回显(默认值已应用),ArkTS 直接渲染,不必自己补默认。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 模型与 Token 层"逻辑搬移"进 example/nativeApp/src/commonMain(包名、文件名与上游一致),四类机械替换(Color→AestheticColor、Dp/sp→Double、TextStyle/Easing→TypeSpec/bezier 字符串、ImageVector/KeyboardType→glyph 名/自建枚举);色值、间距、混合系数逐字保留。 |
| 平台目标 | example/nativeApp 声明 jvm()(交叉验证)+ ohosArm64 + ohosX64,双 ABI sharedLib libohosaestheticdialogs.so。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 aestheticdialogs/、app/ 留盘不进 settings。 |
| 桥接稳定 | 1 个 C ABI(AestheticDialogSpec)+ UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| UI 完整 | 单页覆盖 7 家族全部变体;所有视觉值来自规格 JSON,ArkTS 零设计 token 硬编码(错误指示色除外——它不能依赖已宕机的 token 引擎)。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现,也不是"照着截图重写一套":每个对话框打开前都真实调用 Native 桥获取规格,Bridge 状态卡展示的 scrim / radius / elevation 等值即来自 Kotlin 核心。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点上游架构(三层 UI + UiModel 无 lambda)、确定"模型/Token 留 Kotlin、渲染归 ArkTS"切面
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1、删 daemon-jvm criteria
第 3 阶段:核心移植 ── model/foundation/tokens/variants 逻辑搬移 + 四类机械替换,包名不变
第 4 阶段:桥协议建模 ── MiniJson 树形解析器、SpecBuilder 规格输出、confirmStyle 规则以上游源码校准
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、CMake IMPORTED + $ORIGIN rpath
第 6 阶段:ArkUI 宿主与验证 ── 七家族 Demo、HAP、真机/真机安装、点击驱动验收和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
aestheticdialogs/ 上游 Android 库模块(留盘不进 settings,零改动)
app/ 上游 Catalog 演示模块(留盘不进 settings,零改动)
example/nativeApp/ Kotlin/Native 动态库(核心移植 + Bridge + MiniJson + SpecBuilder)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
docs/ scripts/ CLAUDE.md 上游文档与脚本(零改动)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :example:nativeApp,rootProject.name = "ohos_AestheticDialogs"。上游的 AGP、maven-publish、roborazzi、dokka、spotless、BCV 插件全部随根构建替换剥离——本 fork 不发 Maven Central,Android 侧构建回官方上游仓。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin 21.0.12.101,org.gradle.java.home 钉死) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(2.2.21 旧线要求;上游 9.7.0 wrapper 不可用) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.aestheticdialogs |
| 版本 | 2.0.0-ohos.1(Gradle 与 AppScope/app.json5 双处一致) |
export JAVA_HOME="/path/to/jdk-21"
java -version
上游根目录原有 gradle/gradle-daemon-jvm.properties(JBR 21 工具链 criteria),会与钉死的 Temurin 路径打架导致 daemon 起不来——适配时删除,JDK 由 org.gradle.java.home 唯一决定。
1.3 源码依赖度盘点
| 上游目录 | 文件 | 依赖形态 | 处置 |
|---|---|---|---|
model/ + 7 个 components/*/models/ | 8 | @Immutable 注解 + ImageVector + KeyboardType 三处 androidx | 逻辑搬移(结构、字段名、默认值逐字保留) |
foundation/ | 3 | Compose Color | 逻辑搬移(AestheticColor(Long ARGB) 替换) |
tokens/ | 7 | Dp / sp / TextStyle / Easing / Shape | 逻辑搬移(数值不变,类型机械替换) |
variants/ToneStyling.kt | 1 | @Composable + Brush | 改纯函数(ramp 三色 + 混合系数逐字保留) |
primitives/ + components/*/variants/ | 12 | Compose 渲染(Canvas、Dialog 窗口、TextField) | 不移植——渲染归 ArkTS;其中 AestheticGlyphs.kt 的几何数据(单位方框线段坐标)逐字翻译进 ArkTS Glyph.ets |
preview/ + test/ | — | Robolectric / Roborazzi | 留在上游模块(本仓不可构建) |
四类机械替换的对照:
| 上游类型 | 移植为 | 说明 |
|---|---|---|
Color(0xFF48D865) | AestheticColor(0xFF48D865L) | value class 包 ARGB Long,hex() 输出 #AARRGGBB |
48.dp / 20.sp | 48.0 / 20.0 | Double,数值不变 |
TextStyle(weight, size, lineHeight, letterSpacing) | TypeSpec(size, weight, lineHeight, letterSpacing) | weight 用 Int(400/500/600) |
LinearOutSlowInEasing / CubicBezierEasing(0.4f, 0f, 1f, 1f) | "cubic-bezier(0, 0, 0.2, 1)" / "cubic-bezier(0.4, 0, 1, 1)" | 字符串,ArkTS 侧解析四控制点 |
icon: ImageVector? | icon: String? | glyph 名(check/cross/exclamation/info/search/close/reveal/conceal) |
keyboardType: KeyboardType | DialogKeyboardType 枚举 | Text/Email/Phone/Number/Uri |
1.4 页面边界
标题区 AestheticDialogs 2.0 / 副标题 / Dark theme 开关
TONE 区 SUCCESS/ERROR/WARNING/INFO/NEUTRAL 五药丸 + CYCLE 循环按钮
桥状态卡 调用数、ok/kind/variant/tone/dark、scrim/surface/radius/elevation 实值、最近 signal
DIALOGS 组 13 个场景行(Confirmation×2 / Alert×4 / Selection×2 / Input×2 / Content / Feedback×2)
NOTIFICATIONS 组 横幅动画切换器(Slide/Fade/Scale/SlideHorizontal/None)+ 5 个横幅行
模态层 Stack overlay:scrim + 对话框卡片(宽度按宽度桶自适应、返回键/点外分别建模)
横幅层 顶部/底部悬浮 + 出入场动画 + 4s 自动消失
页面打开每个对话框前都先调 aestheticDialogSpec(json);signal(Confirmed / Cancelled / ItemClicked / SearchQueryChanged…)在 ArkTS 本地处理后写入桥状态卡的"最近 signal"行。
第 2 阶段:目标与依赖打通
2.1 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
本仓核心是"逻辑搬移"的纯 Kotlin,不依赖任何 kotlinx 制品线,因此沿用社区早期批准的钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款)。这条线的要求是 Gradle 8.14.1——上游 wrapper 是 9.7.0,必须替换;gradle.properties 钉 kotlin.native.ignoreDisabledTargets=true 与 kotlin.native.distribution.downloadFromMaven=true(KN 分发从 Maven 取,本机 ~/.konan 已有 2.2.21-1.0.0 预编译与 ohos sysroot 缓存,离线可链)。
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_AestheticDialogs"
include(":example:nativeApp")
gradle/libs.versions.toml 只剩一个插件条目:
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.2 编译核心
example/nativeApp/build.gradle.kts:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvm() // jvmTest 交叉验证
jvmToolchain(21)
ohosArm64 {
binaries.sharedLib { baseName = "ohosaestheticdialogs" }
}
ohosX64 {
binaries.sharedLib { baseName = "ohosaestheticdialogs" }
}
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
fun cxxShared(triple: String): File { /* 从 ~/.konan/dependencies 定位 libc++_shared.so */ }
tasks.register<Copy>("copyKnSoArm64") { /* debugShared 产物 + libc++ → harmonyApp/entry/libs/arm64-v8a */ }
tasks.register<Copy>("copyKnSoX64") { /* → entry/libs/x86_64 */ }
tasks.matching { it.name == "linkDebugSharedOhosArm64" }.configureEach { finalizedBy("copyKnSoArm64") }
tasks.matching { it.name == "linkDebugSharedOhosX64" }.configureEach { finalizedBy("copyKnSoX64") }
两处纪律:kotlin.experimental.ExperimentalNativeApi 的 optIn 只对 native 目标合法,本模块还有 jvm(),所以用 @file:OptIn 挂在 ohosMain 的 Bridge.kt 上而不是 compilerOptions 块;根构建全局禁用 Ohos*Test* 任务(ohos 测试目标不可用,测试统一 JVM 跑)。
实测:
./gradlew :example:nativeApp:jvmTest # BUILD SUCCESSFUL, 17 项
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# libohosaestheticdialogs.so: arm64 5,489,968 B ≈ 5.2MB;x86_64 4,966,808 B ≈ 4.7MB
2.3 上游同步机制
核心文件与上游模块保持文件级对应(包名、文件名一致)。上游发新版时:diff 上游 foundation/tokens/model/components/*/models → 同步 nativeApp commonMain 对应文件 → 跑 jvmTest → 重链 so。"上游零改动"是验收底线:git diff --stat HEAD -- aestheticdialogs app docs scripts 为空。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.thecode.aestheticdialogs/**(移植核心:UiModel + 配色引擎 + tokens + tone ramp)
↓ 规格计算
SpecBuilder.kt(请求解析 → UiModel 构建 → 规格输出)
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API aestheticDialogSpec(request)
↓
ArkUI Index.ets(目录页 + 模态 overlay + 横幅宿主)
请求与响应:
请求 : {"kind":"confirmation","mode":"dark","reducedMotion":false,
"brand":{"primary":"#...","onPrimary":"#","surface":"#","onSurface":"#"},
"model":{"variant":"destructive","title":"...","confirmLabel":"...","cancelLabel":"..."}}
响应 : {"ok":true,"kind":"...","variant":"...","tone":"...","isDark":false,
"colors":{...},"toneColors":{...},"toneGradient":[...],"toneRim":[...],"glyph":"...",
"frame":{...},"banner":{...},"dimens":{...},"spacing":{...},"shapes":{...},
"typography":{...},"motion":{...},"actions":{...},"confirmStyle":{...},
"model":{规范化回显}}
{"ok":false,"error":"..."}
kind 覆盖七家族;model 字段名 = 上游 UiModel 字段的 lowerCamelCase(selectedIds 数组、items[{id,label,supportingText,enabled}]、primaryAction{label,emphasis,enabled,loading} 等);mode 选 light/dark 配色;brand 同上游 withBrand()(status tone 不可品牌化的约束一并保留);reducedMotion 映射 motion.enabled。
3.2 confirmStyle:设计决策留在 Kotlin
确认按钮"什么颜色"是设计系统的核心观点,这层逻辑放 Kotlin 侧、ArkTS 不重复实现。规则以上游 variants 源码逐条校准:
| 组件 / 变体 | confirmStyle |
|---|---|
| alert | 按 primaryAction.emphasis 取条目;Primary 且 tone 非 Neutral 时 = tone accent / onAccent |
| confirmation.Default | Primary emphasis + tone:Neutral 时是品牌蓝 action.primary(不是 neutral 灰 accent——初版按直觉写成恒 tone accent,读上游 ConfirmationDialogVariants 后纠正) |
| confirmation.Destructive | error accent / onAccent(上游强制 tone=Error) |
| selection / input | actions.primary(动作行不传 tone,即品牌蓝) |
| feedback.Flat | tone 解析的 Primary(Neutral→品牌蓝) |
| feedback.Flash | 透明容器 + onAccent 文字与 1dp 描边(上游 DialogButtonPrimitive 是 containerColor=Transparent, BorderStroke(1dp, onGradient)——渐变面板本身即背景,按钮只描边) |
| content | 有 primaryAction 按其 emphasis,否则 actions.primary |
这条经验值得单独立规:每一条视觉规则都要能指回上游某个文件,猜的必须被源码推翻。
3.3 错误通道与 MiniJson
桥内整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("AestheticDialogSpec")
fun aestheticDialogSpec(request: String): String = try {
SpecBuilder.build(request)
} catch (t: Throwable) {
"""{"ok":false,"error":${MiniJson.jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、未知 kind / tone / variant、缺必填字段(如 alert 缺 primaryAction——上游 UiModel 里它非可选),全部走同一个 ok:false 通道。请求是嵌套 JSON(model 对象、items 数组、action 对象),所以 MiniJson 是树形递归下降解析器(JsonObject / JsonArray / JsonString / JsonNumber / JsonBool / JsonNull 六节点,全转义含 \uXXXX),而不是 cmp_qrose 时代的扁平 key-value 版本——但决策同源:C ABI 边界永远 const char* → const char*,不上 kotlinx.serialization,桥的 schema 一共 30 来个字段,150 行手写解析器比序列化插件的元数据负担划算。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libaestheticdialogs_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosaestheticdialogs.so)
│ AestheticDialogSpec
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只传布尔又会让页面复制设计决策,所以选择 C ABI + JSON——切面在"模型 → 规格",不在"规格 → 像素"。
4.2 Kotlin/Native 导出函数
@CName("AestheticDialogSpec")
fun aestheticDialogSpec(request: String): String // 七家族一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
AestheticDialogSpec | aestheticDialogSpec(req) | 配色引擎 + tone ramp + token 刻度 + confirmStyle 解析 + 模型规范化 |
llvm-nm -D 实测双 ABI so 均含 T AestheticDialogSpec 与 T libohosaestheticdialogs_symbols;手写 libohosaestheticdialogs_api.h(只声明 ExportedSymbols 结构与业务符号)与 KN 自动生成的头文件逐字一致。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"aestheticDialogSpec", nullptr, NapiAestheticDialogSpec, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string(两段式 JsToUtf8 取串)、OH_LOG_INFO 打请求字节数(hilog tag AestheticDialogsNapi——真机上验证桥调用的直接证据)、调 KN 函数并包装返回。KN 返回字符串必须经 libohosaestheticdialogs_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。nm_modname(aestheticdialogs_napi)、oh-package 依赖名(libaestheticdialogs_napi.so)、index.d.ts 导出名与 ArkTS import 'libaestheticdialogs_napi.so' 四处名字严格一致。
4.4 CMake 和类型声明
add_library(ohosaestheticdialogs SHARED IMPORTED)
set_target_properties(ohosaestheticdialogs PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(aestheticdialogs_napi SHARED napi_init.cpp)
target_link_libraries(aestheticdialogs_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosaestheticdialogs)
OHOS_ARCH 为空或 KN so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath + IMPORTED_NO_SONAME 防止 Windows 盘符路径写进 NEEDED;POST_BUILD 把 KN so 与 libc++_shared.so 拷到 NAPI so 旁,三件套同目录 dlopen 互见。
export const aestheticDialogSpec: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 打开对话框
private openDemo(id: string): void {
// 1. 组 model(字段 = 上游 UiModel lowerCamelCase)
// 2. 调桥取规格
const spec: SpecResponse = fetchSpec(kind, this.mode(), DEFAULT_BRAND, model);
if (!spec.ok) { this.lastCall = `ok=false error=${spec.error ?? 'unknown'}`; return; }
// 3. 激活模态 overlay,signal 本地处理
this.dlgSpec = spec;
this.activeDemo = id;
}
渲染层零设计 token 硬编码:卡片底 colors.surface.container、遮罩 colors.surface.scrim、圆角 shapes.dialog、标题 typography.title 逐档映射、渐变 toneGradient 直喂 linearGradient、动效 motion.easingEnter 解析四控制点后 curves.cubicBezierCurve(...) 现场构造、glyph 用 Glyph.ets 逐字翻译的单位方框线段几何(如 check = (0.24,0.53)→(0.43,0.71)→(0.77,0.31),圆头线帽,线宽 = 0.10×边长)。暗色切换 = 换 mode:"dark" 重发请求,全部规格随之翻转——上游"暗色是纯重映射"的设计在鸿蒙侧零额外代码兑现。
5.2 模态实现:Stack overlay 而非 @CustomDialog
@CustomDialog 把"返回键"与 autoCancel 耦合,无法独立表达上游 DialogDismissBehavior 的两个开关。页面 Stack overlay + onBackPress 分发才能分别建模 dismissOnBackPress / dismissOnClickOutside(alertLong demo 专门演示返回键拦截);对话框宽度按 frame 宽度桶自适应:min(屏宽 − 2×compactWindowMargin, widthMedium)。
5.3 错误边界与兜底规格
fetchSpec 整体 try/catch,桥或 JSON.parse 失败返回 ok=false 的 SpecResponse;emptySpec() 兜底值不是零而是上游 light 主题真值(字号 20/15/16/13/15/12、可见配色、圆角 24)——桥宕机时页面依然可读;pageSpec 为 null 时 BridgeCard 顶部渲染红色错误卡(错误指示色硬编码是刻意的:它不能依赖已宕机的 token 引擎)。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosaestheticdialogs)+ cxxShared + copyKnSo*
│ └── src/
│ ├── commonMain/kotlin/com/thecode/aestheticdialogs/ 核心移植(model/foundation/tokens/variants/glyphs)
│ ├── commonMain/kotlin/example/nativeapp/ MiniJson + SpecBuilder
│ ├── ohosMain/kotlin/example/nativeapp/Bridge.kt @CName("AestheticDialogSpec")
│ └── jvmTest/kotlin/ 17 项交叉验证
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.aestheticdialogs、versionName 2.0.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/libohosaestheticdialogs_api.h
├── src/main/cpp/types/libaestheticdialogs_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/
├── model/Spec.ets 协议 interface + 请求构造 + bezier/宽度解析
├── components/ Glyph / AestheticButton / AestheticDialogCard / AestheticBanner
└── pages/Index.ets ArkUI 验收页面(目录 + 模态 + 横幅)
6.2 构建、签名和安装
./gradlew :example:nativeApp:jvmTest # 17 项交叉验证
./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 中勾选自动生成,证书不进入仓库(仓库内 signingConfigs 保持空数组)。DevEco 真机(x86_64)接受未签名调试包,可先行验收再上真机:
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.aestheticdialogs -m entry
hdc shell hilog | grep AestheticDialogs # 桥调用打点:request bytes=...
四、完整代码对照
4.1 整体架构
com.thecode.aestheticdialogs/**(移植核心:UiModel + AestheticColors + tokens + tone ramp)
│ 五档 tone / light-dark 重映射 / withBrand / 渐变系数 0.22·0.14
SpecBuilder + MiniJson
│ 请求 JSON → UiModel → 规格 JSON(含 confirmStyle 设计决策)
libaestheticdialogs_napi.so
│ aestheticDialogSpec
Index.ets + components/*
│ 规格驱动渲染 + signal 本地处理
▼
ArkUI 页面(13 场景 + 5 横幅 + 5 动画)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_AestheticDialogs、Android 模块移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根) | 版本 2.0.0-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle.properties | JDK 21 钉路径、KN 开关、wrapper 8.14.1 |
nativeApp/.../foundation/AestheticColor.kt | value class(Long ARGB)+ lerp + #AARRGGBB 输出 |
nativeApp/.../AestheticColorSchemes.kt | light / dark / withBrand(色值逐字) |
nativeApp/.../tokens/* | Dimens / Spacing / Elevation / Shapes / Typography / Motion / Palette |
nativeApp/.../components/*/models/*.kt | 7 家族 UiModel + Signal(结构与上游逐字一致) |
nativeApp/.../example/nativeapp/MiniJson.kt | 树形递归下降 JSON 解析器 + writer |
nativeApp/.../example/nativeapp/SpecBuilder.kt | 七 kind 协议、confirmStyle、错误契约 |
nativeApp/.../ohosMain/.../Bridge.kt | @CName("AestheticDialogSpec"),异常不穿 ABI |
nativeApp/.../jvmTest/*.kt | 17 项交叉验证(含页面级请求回归) |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/model/Spec.ets | 协议 interface + 请求构造 + 兜底规格 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(24 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | aestheticLightColors() / aestheticDarkColors() | 两套语义配色(角色级) |
| 引擎 | withBrand(primary, onPrimary, surface, onSurface) | 一行换品牌,status tone 不可品牌化 |
| 引擎 | toneRamp(tone, colors) | 三色渐变 ramp(0.22 / 0.14 混合系数) |
| 引擎 | AestheticDimens / Spacing / Shapes / Typography / Motion | 全部布局刻度 token |
| 模型 | *UiModel(7 家族 sealed interface) | 无 lambda 的视觉状态;Signal 全部无参 data object |
| Native | AestheticDialogSpec | C ABI JSON 导出(唯一入口) |
| N-API | aestheticDialogSpec(req) | 向 ArkTS 暴露能力 |
| ArkTS | fetchSpec(kind, mode, brand, model) | 调桥 + 解析 + 错误兜底 |
4.4 ArkTS 与 Kotlin 的边界
const spec = JSON.parse(aestheticDialogSpec(JSON.stringify({
kind: 'confirmation', mode: this.isDark ? 'dark' : 'light',
brand: DEFAULT_BRAND,
model: { variant: 'destructive', title: 'Delete this album?',
message: 'This cannot be undone.',
confirmLabel: 'Delete album', cancelLabel: 'Keep it' }
}))) as SpecResponse;
// spec.confirmStyle.container === '#FFC22B22'(error accent,Kotlin 侧解析)
// spec.colors.surface.scrim / spec.shapes.dialog 直接驱动遮罩与圆角
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:渲染切面选在"模型 → 规格",不在"规格 → 像素"
qrose 能走"KN 光栅化 PNG 回传"是因为二维码的产物就是静态位图;对话框是交互式 UI,要响应点击、输入、动效,必须活在 ArkUI 的帧率循环里。而"设计值"(颜色/圆角/渐变/字号/动效时长)是纯数据,跨语言零损耗。所以 Kotlin 侧止步于规格 JSON,像素交给 ArkUI——这同时保住了"设计决策留在 Kotlin"与"交互不跨 FFI"两条底线。
决策 2:核心放 example/nativeApp 逻辑搬移,不 KMP 化库模块
上游 :aestheticdialogs 深度绑定 AGP / mavenPublish / roborazzi / BCV,改 KMP 会破坏其发布链且工程量大;qrose 能直接复用库模块是因为它本来就是 KMP。逻辑搬移保持包名与文件名不变,上游同步靠文件级 diff,配 17 项 jvmTest 钉住数值没有漂。
决策 3:工具线用 2.2.21 + Gradle 8.14.1
本仓核心零 kotlinx 依赖,不需要 KStore 那条 2.4.20 制品线;沿用社区早期批准的 2.2.21-1.0.0 钉法,代价是 Gradle 从 9.7.0 降到 8.14.1(上游 Android 构建本来就不在本仓进行,无附加成本)。两条工具链线的经验不能互相套用:钉哪条线由核心的依赖形态决定。
决策 4:MiniJson 手写、树形版
请求含嵌套对象与数组,qrose 的扁平解析器不够用;升级为六节点树形递归下降解析器(全转义、\uXXXX、科学计数法),但"不上 kotlinx.serialization"的决策同源——C ABI 边界永远字符串进字符串出。
决策 5:模态用 Stack overlay,dismiss 双开关独立建模
@CustomDialog 的返回键与 autoCancel 耦合;页面 overlay + onBackPress 分发才能分别表达 dismissOnBackPress=false(Blocking)与 dismissOnClickOutside=false,scrim 直接用 colors.surface.scrim。
决策 6:兜底规格用真值而不是零
emptySpec() 曾用全零占位,导致一次"静默白屏"事故(桥返回 ok:false 时字号全 0,文字全部不可见,页面只剩一个 Toggle)。修复后兜底值 = 上游 light 主题真值 + BridgeCard 红色错误卡——失败必须可见,这是 UI 桥接层的通用铁律。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11、JDK 21(Temurin)、DevEco Studio 5.1.0(18) 命令行工具(hvigor / hdc)与 DevEco 真机(x86_64,arm64 真机流程相同)。
6.2 JVM 交叉验证
./gradlew :example:nativeApp:jvmTest
17 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
SpecBuilderTest | 15 | 七 kind 完整请求、转义/中文标题、light/dark、withBrand、9 类错误请求 ok=false、默认值回显、渐变 stop 0/0.5/1、scrim #52000000/#99000000 |
PageRequestReproTest | 2 | 页面级最小请求必须带 primaryAction(缺 → 明确错误);带最小 action → ok=true 全字段返回 |
断言直接对着上游源码数值写死(4dp 间距网格、24/16 圆角、六档字号、tone 渐变系数、scrim 透明度 0.32/0.6),任何移植漂移立刻红。
6.3 HAP 验证
:example:nativeApp:jvmTest: BUILD SUCCESSFUL (17 tests)
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: AestheticDialogSpec / libohosaestheticdialogs_symbols 均为全局 T 符号(双 ABI)
copyKnSoArm64 / copyKnSoX64: 三件套就位 entry/libs/{arm64-v8a,x86_64}
BuildNativeWithCmake: finished(双 ABI 编出 libaestheticdialogs_napi.so)
CompileArkTS: finished
PackageHap: finished(entry-default-unsigned.hap,真机可直接安装)
install bundle successfully
start ability successfully
KN so arm64 约 5.2MB、x86_64 约 4.7MB(debugShared,含 Kotlin/Native 运行时)。
6.4 功能用例
- 冷启动后 hilog 出现
AestheticDialogsNapi: aestheticDialogSpec request bytes=232(页面级主题请求,桥真实在跑); - 首页渲染完整:标题、TONE 药丸(SUCCESS 选中态品牌蓝填充)、13 场景列表、Bridge 状态卡显示
ok kind=alert ... scrim=#52000000 surface=#FFFFFFFF radius=24 elev=12; - 点击 Confirmation:模态弹出——半透明 scrim(α=0.32)、白底 24dp 圆角卡片、“Publish this photo?”、品牌蓝胶囊 Publish + 描边 Not yet;
- 点击 Destructive:确认键 error 红填充;点击后按钮转圈 1.5s、全对话框禁用,完成后顶部弹 Toaster 横幅;
- Feedback Flash:tone 渐变满填充面板 + 透明描边按钮(上游
BorderStroke(1dp, onGradient)的复刻); - Selection Single:7 语言行含禁用项与搜索框,点选即回填;
- Input Password:显隐切换,<8 字符确认禁用;
- Alert 长文本:内容滚动,返回键不关闭(
dismissOnBackPress=false独立建模); - 五种横幅 + 五种动画逐一触发,4s 自动消失;
- 暗色 Switch:全部规格翻转(等价于 mode:“dark” 重新过桥),CYCLE 药丸用
toneColors渲染; - 每步操作的 Bridge 状态卡与"最近 signal"行实时留痕。
6.5 验证结论
模型层、配色引擎、token 刻度、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,点击驱动的截图确认目录页、模态对话框、Flash 渐变面板、暗色重映射与 Rainbow 横幅全部按 Kotlin 核心输出的规格渲染(见下节效果图)。
七、运行效果
7.1 运行截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/AestheticDialogs.git
cd AestheticDialogs
export JAVA_HOME="/path/to/jdk-21"
./gradlew :example:nativeApp:jvmTest # 17 项交叉验证
./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.aestheticdialogs -m entry
hdc shell hilog | grep AestheticDialogs
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算适配:必须让模型/Token 核心真正编译成
ohosArm64/ohosX64动态库,页面从桥取值渲染; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游的
gradle/gradle-daemon-jvm.properties(daemon JVM criteria)会与org.gradle.java.home钉死的路径打架,daemon 起不来——适配时删除; - 带有
jvm()目标的模块里,kotlin.experimental.ExperimentalNativeApi的 optIn 不能放compilerOptions(native-only),用@file:OptIn挂在 ohosMain 文件上; - JVM 后端要求 value class 加
@JvmInline,KN 侧则不需要——两目标共存时按最严的写; - ArkTS 严格模式组件成员名:
@Componentstruct 的成员不能用size/enabled/onClick/borderColor/borderWidth这类通用属性方法名(报 “not assignable to the same property in base type ‘CustomComponent’”),全局改名glyphSize/isEnabled/onClicked/strokeColor/strokeWidth并同步所有调用点; import { ICurve } from '@ohos.curves'在新 SDK 无此导出:模型层只存 bezier 四数值,使用点curves.cubicBezierCurve(...)现场构造(@kit.ArkUI);- ArkTS 禁止未类型化对象字面量(
arkts-no-untyped-obj-literals):model: {}改为显式EmptyModel类实例; Path组件的圆角线帽属性是strokeLineJoin不是strokeJoin;全局px2vp已弃用,改screen.width / densityPixels;- 静默白屏事故:页面级请求缺
primaryAction(上游必填)→ 桥ok:false→ 兜底规格全零 →fontSize(0)文字不可见、背景透明,页面只剩一个 Toggle。三层修复:请求补最小 action、兜底规格改上游真值、BridgeCard 红色错误卡——UI 桥的失败必须可见; - DevEco 打开工程后生成的
example/harmonyApp/.idea/(含 clangd 缓存)要单独追加 gitignore,根目录.idea规则盖不住嵌套目录; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free()。
8.2 已知问题
- Compose 渲染层(variants / primitives 的 @Composable 部分)未移植;ArkTS 视图按同一 Token 规格等价复刻,个别度量(文字基线节奏)允许有 ArkUI 排版差异;
- 上游
defaultMotion()读系统动画缩放设置;鸿蒙侧 reduced-motion 由请求参数显式传入,未接系统设置监听; - 无障碍:实现了基本可达性,上游
LiveRegionMode(Error 用 Assertive)的完整等价物未逐一映射; - InputDialog 的
keyboardType映射 ArkTS 输入风格,个别 Android 键盘变体无等价物未纳入枚举; - 本仓不能构建 Android APK;上游 Android 验证(roborazzi 截图、BCV、spotless)回官方上游仓,本仓以"上游文件零改动"保证可回溯;
- 签名材料由 DevEco 自动管理,证书绝不进入仓库。
8.3 未来优化方向
- 鸿蒙侧包化:把 components + Spec 整理为 HAR/HSP,让 ArkTS 工程直接依赖而不是走 so 三件套;
- 无障碍映射补全(liveRegion 等价物、屏幕朗读实测);
glyph几何改 SymbolGlyph / 资源化,减少自绘 Path;- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(arm64)验收矩阵与自动化截图脚本(
uitest dumpLayout+uiInput click的脚本化,本次真机验收已用此法)。
九、总结
9.1 核心难点回顾
上游 AestheticDialogs 2.0(纯 Android Compose,三层 UI 架构)
→ 判定切面:UiModel 无 lambda + Token 驱动 ⇒ 模型/Token 层可移植
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1,删 daemon-jvm criteria)
→ 根构建替换(cmp_qrose 模式,Android 模块留盘零改动)
→ 核心逻辑搬移 + 四类机械替换(色值/系数逐字保留)
→ ohosArm64 / ohosX64 sharedLib + jvm() 交叉验证
→ C ABI(@CName × 1)+ 规格协议 + confirmStyle 以上游源码校准
→ C++ N-API(DisposeString 纪律)
→ ArkTS 规格驱动渲染(零 token 硬编码)
→ ArkUI 真机/真机验收(13 场景 + 5 横幅 + 5 动画 + 暗色重映射)
9.2 封装层次
- 模型层:上游七家族 UiModel / Signal / DialogAction / DialogDismissBehavior(结构逐字);
- 引擎层:light / dark 配色、
withBrand、tone ramp(0.22/0.14)、全部 token 刻度; - Native 层:双 ABI 动态库并通过 1 个 C ABI 输出规格 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:协议类型、规格驱动渲染、signal 处理、错误兜底与桥状态卡;
- DevEco 层:CMake、HAP、签名、安装和设备运行。
9.3 三条经验
- 适配切面由库的本质输出决定:静态产物(二维码)可以 KN 光栅化回传位图;交互式 UI 的正确切面是"模型/Token 留 Kotlin、渲染归平台 UI"——AestheticDialogs 2.0 的"UiModel 永不持 lambda"架构让这个切面天然干净,上游每一次迭代都能机械同步,翻成 ArkTS 就是永久分叉;
- 每条视觉规则都要能指回上游源码:confirmStyle 的 tone→按钮色映射(Neutral→品牌蓝、Destructive→error、Flash→透明+onAccent 描边)初版凭直觉写错了两处,全部靠读上游 variants 源码纠正——设计系统适配里"猜"是负债;
- 失败必须可见:兜底值用真值而不是零、错误卡硬编码颜色、hilog 打请求字节数——"静默白屏"排查花了完整一轮日志+截图+JVM 复现,这三层防线能让同类故障在第一眼就被定位。
9.4 适配成果
当前适配已完成:
- AestheticDialogs
2.0.0-ohos.1:模型层与 Token 引擎的双 ABI Kotlin/Native 编译(包名不变、色值/系数逐字保留,上游 Android 模块零改动); - 1 个 C ABI 符号(
AestheticDialogSpec)和 C++ N-API 包装层(llvm-nm验证全局 T 符号,双 ABI); - DevEco Stage ArkUI 验收页面(13 对话框场景 + 5 横幅 + 5 动画 + 暗色重映射 + Bridge 状态卡,全部视觉值来自桥);
- 17 项 JVM 交叉验证全绿 + HAP 构建、真机安装、点击驱动验收与效果图;真机流程同路径(自动签名 +
hdc installsigned hap); - AtomGit 项目文档(四份 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)