开源鸿蒙平台 CMP 三方库「compose-material-dialogs」适配全流程
本文记录
compose-material-dialogs接入 OpenHarmony 的完整过程,覆盖 Compose UI 库的依赖度盘点、纯 Kotlin 内核提取、自实现公历对账 java.time、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
compose-material-dialogs是 Android Jetpack Compose 的 Material 风格对话框库:MaterialDialog容器 + 标题/消息/自定义视图 + 输入框 + 三种列表(纯文本/单选/多选)+ 日历日期选择器 + 表盘时间选择器 + 颜色选择器。本次适配与 KStore 的"真编译整库"路线有本质区别:上游 0.9.0 的源码几乎 100% 是@ComposableUI(依赖androidx.compose1.2.1 / Kotlin 1.7.10 / AGP 8.0-alpha,并通过 desugaring 使用java.time),鸿蒙工具链上既没有兼容的 Compose 制品、也没有这些依赖的 ohos 变体。因此本 fork 走内核提取路线:把每个对话框背后的决策与算法——按钮何时可用、输入何时合法、多选集合如何演化、日历哪格可点、表盘指针吸附到哪个锚点、颜色前景该黑该白——提取为 6 个纯 Kotlin 引擎编译进 OpenHarmony 动态库,ArkUI 页面作为验收宿主。这与社区早期 qrose / sheets-compose-dialogs / FlexibleBottomSheet 的路线一致,而与 KStore(ohos kotlinx 制品线齐全后真编译整库)互为对照:路线选择由当天的制品可达性决定。项目地址: AtomGit/oh-tpc/compose-material-dialogs.git
上游仓库: vanpra/compose-material-dialogs
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-material-dialogs 为 Android Compose 应用提供"对话框"这一件事的完整答案:MaterialDialog(dialogState) { ... } 一个容器,title/message 装内容,input() 挂输入框,listItems*() 挂三种列表,datepicker()/timepicker()/colorChooser() 挂三个官方选择器,按钮区 positiveButton/negativeButton 收尾,autoDismiss 与 waitForPositiveButton 控制交互节奏。上游 Maven 坐标 io.github.vanpra.compose-material-dialogs:{core,datetime,color} 在 Android 生态广泛使用。
鸿蒙应用同样需要这套对话框决策能力,但上游构建(Gradle 7.5 / AGP 8.0.0-alpha04 / Kotlin 1.7.10 / Compose 1.2.1)不会生成 OpenHarmony 变体,且鸿蒙 Kotlin 工具链(2.2.21-1.0.0)没有兼容的 Compose 运行时——两套工具链无法共存于一个 Gradle 构建。
| 障碍 | 具体问题 |
|---|---|
| UI 不可编译 | 全部源码是 @Composable,鸿蒙侧无 Compose 制品;上游还是 Compose 1.2.1 老线 |
| 依赖不可达 | androidx.compose.*、Accompanist Pager、androidx.core.graphics.ColorUtils 均无 ohos 变体 |
| java.time 不可用 | 上游日历基于 LocalDate/WeekFields + coreLibraryDesugaring;Kotlin/Native common 无 java.time |
| 测试基建失效 | 上游测试是 Android instrumented + Shot 截图测试,全部依赖设备与 AGP |
| 构建系统冲突 | buildSrc 注册的 common-library 插件引用 AGP,鸿蒙工具链配置期即卡死 |
| 语言边界 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON |
因此,本项目把边界放在:对话框决策逻辑的内核提取、根构建替换(cmp_qrose 模式)、双 ABI 动态库与 C ABI/N-API 桥接。选择/校验/日历/时钟/颜色语义完全由提取的内核负责并与上游逐位对齐,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 用一个 ArkUI 单页 6 Tab 覆盖桥的全部 25 个 op:
- 对话框 Tab(input 引擎):SHOW/HIDE 生命周期;输入框每键实时校验(
required空输入时错误消息出现、POSITIVE 按钮禁用);POSITIVE/NEGATIVE/NEUTRAL 三按钮按下时的事件流(fired串如input("vanpra"))、autoDismiss × disableDismiss组合、隐藏后回调表清空的 reset 语义; - 列表 Tab(list 引擎 × 3 会话):纯文本列表点击即关、单选列表未选时 POSITIVE 禁用、多选列表集合顺序保持与 disabled 行拒点;
- 颜色 Tab(color 引擎):19 主色 → 10 子色两级导航与返回;ARGB 页四滑条联动
#AARRGGBB大字与黑/白前景自动切换(androidxColorUtils复合+亮度语义复刻); - 日期 Tab(date 引擎):月网格(前导空位、跨月选中不显示)、翻月边界 no-op、
min/max日期禁用、年份模式跳页、选中日Mon, Oct 19表头; - 时间 Tab(time 引擎):12h 单圈 / 24h 双圈表盘锚点(含 12↔00 交换)、分钟 5 步锚点、AM/PM 与范围钳制、
time.nearest手势吸附查询、小时选中后自动进入分钟屏; - 关于 Tab:版本握手(
ping返回 fork 版本,来自真实 Native 调用而非静态文字)与架构分层说明。
响应信封字段(全扁平标量 + 逗号串,无嵌套对象):
| 字段 | 作用 |
|---|---|
ok / op / id | 操作是否成功、回显 op 与会话 id;失败时 error 带原文 |
showing / positiveEnabled / callbacks | 对话框生命周期与按钮门控镜像 |
fired / liveEvent | 正键按下触发的回调事件串 / 免正键模式的即时事件 |
text / valid / isError | 输入引擎状态 |
selected | 列表选择(多选逗号串 "0,2"、单选索引) |
hex / argb / channels / foreground | 颜色引擎状态(含黑/白前景判定) |
cells / selectedStr / headerText | 日历月格(day:enabled:selected 编码)、选中日与表头 |
timeDisplay / screen / clamped / hourRange / minuteRange | 时钟状态与上游范围分支 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 Compose 源码一行不改、留盘隔离;决策逻辑提取为 6 个纯 Kotlin 引擎(零 Compose、零 Android、零第三方依赖,约 1500 行),每文件 KDoc 标注上游源文件与语义映射 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证;example/nativeApp 出双 ABI 动态库 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 :app :color :datetime :test-utils 留盘不进 settings;buildSrc 改名 buildSrc.upstream(Gradle 会自动编译 buildSrc,必须改名) |
| 桥接稳定 | 单一 @CName C ABI 和 UTF-8 JSON,会话制,25 个 op;异常永远不跨 FFI;响应生成后自校验 |
| 依赖纪律 | 内核仅依赖 Kotlin stdlib;java.time 由自实现 CivilDate(java.time 同算法)替代并由 JVM 随机对账钉死 |
| UI 完整 | ArkUI 单页覆盖全部 25 个桥入口:6 Tab + 底部桥日志环形缓冲 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit,git 托管地址不出现其他平台字样 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现,也不是上游 API 的 ArkTS 复刻——像素级 UI 由 ArkTS 原生控件重绘,不在保真范围;设备上执行的决策逻辑与 Android 上游逐位一致。
二、实现路线图
第 1 阶段:项目初始化 ── 模块盘点、Compose/纯逻辑依赖度普查、查重、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、
Gradle 7.5→8.14.1、buildSrc 改名隔离
第 3 阶段:内核提取 ── 6 引擎 + 289 色板 + CivilDate 写入 core/commonMain,
196 项 jvmTest 钉语义(含 java.time 随机对账)
第 4 阶段:桥协议建模 ── CmdBridge 单入口、会话制、25 op、MiniJson、扁平响应、错误通道
第 5 阶段:原生桥接 ── 双 ABI sharedLib、copyKnSo 回填、CMake IMPORTED、N-API、DisposeString
第 6 阶段:ArkUI 宿主 ── DevEco Stage 工程、6 Tab 页面、桥日志面板、真机验收
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
core/src/commonMain/ 新增:6 内核引擎 + ColorPalette + CivilDate(进 ohos 构建)
core/src/jvmTest/ 新增:196 项交叉验证测试
core/src/main|androidTest/ 上游 Android Compose 源码(留盘,不注册任何 source set)
example/nativeApp/ 新增:Kotlin/Native 桥(Bridge.kt + MiniJson + 会话 + copyKnSo)
example/harmonyApp/ 新增:DevEco Stage 工程(CMake N-API + ArkTS 6 Tab Demo)
app/ color/ datetime/ test-utils/ 上游模块(留盘不进 settings.gradle.kts)
buildSrc.upstream/ 上游 buildSrc 改名隔离(原名会被 Gradle 自动编译并卡死配置期)
README.md / README.OpenHarmony.md / README.OpenHarmony_CN.md 鸿蒙适配文档三件套
README.upstream.md 上游原文保存(git mv 保留历史)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :core 与 :example:nativeApp,rootProject.name = "ohos_compose_material_dialogs"。上游的 spotless / dokka / vanniktech 发布 / Shot 截图测试链全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 钉死 Adoptium 21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 wrapper 7.5 必须升级):KMP 2.2.21 线要求 8.x,且 RepositoriesMode/版本目录 alias 特性 7.5 不可用 |
| 内核依赖 | 仅 Kotlin stdlib(无 kotlinx、无 Compose) |
| DevEco product | 5.1.0(18) 兼容 / API 26 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.composematerialdialogs |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度普查
逐文件标注"纯逻辑 / Compose UI / Android 平台",结论决定每个模块的处置:
| 上游模块 | 文件 | 纯逻辑占比 | 处置 |
|---|---|---|---|
:core MaterialDialog.kt / Buttons / Input / Lists | 6 文件 ≈1220 行 | ~30%:状态机、按钮门控、校验、选择集合 | 提取 3 引擎;UI 留盘 |
:color ColorPicker.kt / ColorPalette.kt | 2 文件 ≈770 行 | ~25%:289 色板、ARGB/hex、亮度前景 | 提取引擎+色板转译;AndroidView 棋盘格留盘 |
:datetime date/ 9 文件 time/ | 9 文件 ≈1980 行 | ~20%:日历数学(java.time)、锚点/范围/AM-PM | 提取 2 引擎 + CivilDate;Pager/Canvas 留盘 |
:app / :test-utils | demo / 测试基建 | 0% | 留盘不注册 |
三条硬依赖在提取时必须等价替换:java.time → 自实现 CivilDate;androidx.core.graphics.ColorUtils(compositeColors/calculateLuminance)→ 逐行复刻;WeekFields.of(locale).firstDayOfWeek → 显式构造参数(默认 7=周日,Locale.US 同值)。
1.4 页面边界
标题区 COMPOSE-MATERIAL-DIALOGS · OPENHARMONY / 版本握手行(engine 0.9.0-ohos.1 · kotlin 2.2.21-1.0.0)
Tab 切换 对话框 / 列表 / 颜色 / 日期 / 时间 / 关于(横滚 TabChip)
对话框 Tab SHOW/HIDE、输入框(每键发 input)、三按钮(press)、fired/liveEvent 事件串、状态镜像区
列表 Tab 三会话并列(plain/single/multi),行点击/勾选镜像 + 正键门控
颜色 Tab 主色 19 圆网格 → 子色 10 圆(返回键)、ARGB 四滑条 + hex 大字(前景黑/白)
日期 Tab 月份表头 + 翻月箭头、7 列日格(前导空位/禁用/选中)、年份模式
时间 Tab HH:MM 大字 + AM/PM、表盘锚点(12h 单圈/24h 双圈)、分钟锚点、nearest 查询
关于 Tab 版本握手、库简介、上游↔内核↔桥↔ArkTS 分层、AtomGit 链接
日志区 底部固定 LogPanel:每次桥调用 →/← 留痕(环形缓冲 50 条,失败帧含 error)
页面启动时先 ping 握手,再为各引擎会话发 create;aboutToDisappear 对全部 owned 会话发 dispose。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
与 KStore 不同,本库没有任何第三方制品线可依赖——内核必须零依赖。验证结论有三:其一,eazytec 社区 Maven 提供的 HarmonyOS Kotlin 2.2.21-1.0.0(ohosArm64() / ohosX64() 工厂函数、ohosMain 源集都来自它)是唯一外部依赖;其二,Kotlin/Native 发行版经 kotlin.native.distribution.downloadFromMaven=true 从 Maven 拉取(本机 ~/.konan 缓存 cpf-llvm 工具链);其三,libc++_shared.so 从 ~/.konan/dependencies/<triple> 定位(aarch64-linux-ohos / x86_64-linux-ohos),由 copy 任务与 KN so 一同回填 HAP 工程。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
内核零依赖意味着不需要像 KStore 那样被 coroutines 的 stdlib 元数据反推到 2.4.20 线——社区此前批准的钉法 2.2.21-1.0.0(qrose / sheets / FlexibleBottomSheet 同款)即可。代价是 Gradle 必须从上游的 7.5 升到 8.14.1:这条 Kotlin 线的 KMP 插件要求 Gradle 8.x,且新版版本目录与 RepositoriesMode.PREFER_PROJECT 特性在 7.5 上不可用。两条工具链线的经验不能互相套用:2.4.20 线可以沿用上游 Gradle 9.x,2.2.21 线必须停在 8.14.1。
settings.gradle.kts 把 eazytec 仓库写在最前(顺序错误则 ohosArm64() 直接 unresolved):
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_compose_material_dialogs"
include(":core")
include(":example:nativeApp")
另一个必须的隔离动作:buildSrc → buildSrc.upstream 改名。Gradle 会自动编译名为 buildSrc 的目录,而它注册的 common-library 插件引用 AGP——鸿蒙工具链解析不了,配置期直接卡死。改名后 Gradle 不再识别,上游文件原样留盘供查证。
2.3 编译内核引擎
core/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证目标:commonMain 被 jvmTest 钉死
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖,仅 stdlib
jvmTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游 Android 源码(src/main/java、src/androidTest)不是任何已注册 target 的 source set,“留盘不编译”。实测:
./gradlew :core:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.4 测试基建的处置:上游测试全部失效
上游测试是 Android instrumented + Shot 截图测试(需要设备、AGP、Compose 测试工具链),零个可复用。处置是两条纪律的组合:交叉验证测试全部新写在 core/src/jvmTest(仅 kotlin.test),ohos 侧只承担编译与链接,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
新写测试的有效性来自"对上游语义逐分支复刻 + 与 JDK 权威实现对账"(见 6.2),而不是"上游原测试零改动"——这是与 KStore 交叉验证的本质差异。
第 3 阶段:内核提取(技术难点)
3.1 提取原则
每条进入内核的语义都必须能在上游源文件里指出对应行,每个内核文件头部 KDoc 冻结映射表。六个引擎与上游的对照:
| 上游源文件 | 内核引擎 | 提取的精确语义(示例) |
|---|---|---|
| MaterialDialog.kt + MaterialDialogButtons.kt | DialogEngine | positiveButtonEnabled.values.all { it }(空 map=true);正键 onClick 先隐藏后触发回调、reset 在下一帧才清表(桥先捕获再 hide);autoDismiss × disableDismiss 四组合;负键仅 autoDismiss 隐藏;中性键永不隐藏 |
| MaterialDialogInput.kt | InputEngine | valid = isTextValid(text) 每键重算;waitForPositiveButton 决定 onInput 时机;isError + errorMessage 恰在 !valid 出现 |
| MaterialDialogLists.kt | ListEngine | 多选 toggle 保持插入顺序(上游重建 mutable set);单选未选中时正键禁用;disabled 索引无操作;plain 点击先 hide 再回调 |
| ColorPicker.kt + ColorPalette.kt | ColorEngine + ColorPalette | 289 色板 Color→Long 转译;主色同索引重复点击不重置选中但照样进子页;hex 标签 padStart(8,'0') 大写、无 alpha 选择器去前两位;foreground() 逐行复刻 ColorUtils 复合+亮度 |
| DatePicker.kt(java.time) | CivilDate + CalendarEngine | 分页 page=(year-first)*12+month-1;前导空位 (dow(1号)-firstDayOfWeek) mod 7;选中高亮只在视图月;翻月边界 no-op;年份跳页 (year-viewYear)*12 |
| TimePicker.kt + TimePickerState.kt | ClockEngine | 锚点角 (2π/N)(x-15);12h hour = isAM ? a : a+12;24h adjustAnchor 0↔12 交换(外圈顶 12、内圈顶 00);minuteRange 上游怪异分支(61…0 空区间)原样保留;isPMEnabled = end.hour >= 0 恒真的 quirk 不修;AM/PM 点击 coerceIn 钳制 |
3.2 java.time 的替代:CivilDate 随机对账
CivilDate 用 java.time 同源算法自实现外推公历(epochDay ↔ 年月日、闰年、月长、ISO 星期、plusMonths 月末钳制),约 150 行。正确性不靠目测,靠 JVM 上与 java.time.LocalDate 对账:
repeat(500) { // 正向:LocalDate -> CivilDate
val d = dates.random(rng)
assertEquals(d.toEpochDay(), CivilDate.of(d.year.toLong(), d.monthValue, d.dayOfMonth).epochDay)
}
repeat(500) { // 反向:epochDay -> y/m/d/dayOfWeek/leap
val day = rng.nextLong(startEpoch, endEpoch)
val ref = LocalDate.ofEpochDay(day)
val c = CivilDate(day)
assertEquals(ref.year.toLong(), c.year); assertEquals(ref.dayOfWeek.value, c.dayOfWeek)
...
}
开发中被这套对账抓出一个真实 bug:of() 漏了 Mar-based 算法的 +60 回退调整,1970-01-01 被算成 epochDay=-60——没有对账测试,这类错误会安静地进真机。
3.3 内核 Bug 被测试钉住的第二个案例
ClockEngine.nearestAnchor(触点最近锚点吸附)最初把 24h 表盘当成单一 24 锚环 (2π/24)(i-15) 排布;上游 ClockLayout 实际是两个各自独立的 12 锚环(外圈顶=anchor 0 “12”、内圈顶=anchor 12 “00”)。几何测试(触点正上方应吸附 anchor 0)原实现返回 anchor 9,修复按环取角度后通过——这条语义随后被 52 项 ClockEngineTest 钉死。
第 4 阶段:桥协议建模(技术难点)
4.1 JSON 契约
core/src/commonMain(6 引擎,纯 Kotlin)
↓ DialogEngine + InputEngine/ListEngine/ColorEngine/CalendarEngine/ClockEngine
Bridge.kt(@CName × 1,会话制,25 op,扁平 JSON 进出)
↓ UTF-8 JSON
N-API cmdBridge(request)(libcmd_napi.so)
↓
ArkUI Index.ets(单页 6 Tab + 桥日志)
请求 : {"id":"i0","op":"input","text":"vanpra"}
{"id":"d0","op":"date.grid","year":2026,"month":10}
{"id":"t0","op":"time.anchor","screen":"hour","anchor":9}
响应 : {"ok":true,"op":"input","id":"i0","text":"vanpra","valid":true,
"isError":false,"positiveEnabled":true,"liveEvent":"-"}
{"ok":false,"op":"...","id":"...","error":"unknown op '...'"}
两个对称设计:create 的 config 是全协议唯一嵌套对象(其 items 是唯一数组),只在请求侧出现;响应永远扁平——标量 + 逗号串(cells 为 "day:enabled:selected" 逗号编码、多选集合 "0,2"、通道值 "161,33,150,243"),因为 ArkTS typed 上下文对嵌套 JSON.parse 结果的类型收窄会失败。
4.2 会话模型
有状态会话制:HashMap<String, Session>(UI 单线程同步调用,无锁——KN 无 synchronized)。每个 Session = DialogEngine(生命周期)+ 一个 body 引擎;同 id create 整体替换;op 打到错误引擎类型直接拒绝(op 'input' on engine 'date')。
上游"对话框隐藏 → 内容离开组合 → 重显时组件重挂载"的生命周期在桥上模型化为:hide 触发 reset()(清回调表与门控表并回滚计数器),show 时 body 重新 attach——与上游 DisposableEffect 驱动的 reset/重挂载逐位对应。
4.3 错误通道与响应自校验
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("CmdBridge")
fun cmdBridge(request: String): String = try {
val response = dispatch(op, id, req).done()
try { MiniJson.parse(response) } // 自校验:响应必须能反解
catch (t: Throwable) {
throw IllegalStateException("response self-check failed: ${t.message} :: $response")
}
response
} catch (t: Throwable) {
"""{"ok":false,"op":"...","error":"..."}"""
}
自校验是真机踩坑换来的防线(见 8.1 第 10 条):拼接 bug 在本机三层验证(编译/链接/打包)里全部隐形,到真机上表现为 ArkTS 端一串无法定位的 SyntaxError;加了自校验后,任何非法帧都会以带原文的 ok:false 错误帧暴露。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcmd_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscmd.so)
│ CmdBridge
▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
@CName("CmdBridge")
fun cmdBridge(request: String): String // 25 op 一口进,JSON 进 / JSON 出
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
CmdBridge | cmdBridge(req) | 全部 6 引擎 25 op + 会话生命周期 + 版本握手 |
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"cmdBridge", nullptr, NapiCmdBridge, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string(两段式读串,规避 API 26 头文件把 buffer 标 non-const 的坑)、OH_LOG_INFO 打字节数、调 CmdBridge(request.c_str()) 并包装返回。KN 返回的字符串必须经 libohoscmd_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscmd_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
5.4 CMake、copyKnSo 和类型声明
// example/nativeApp/build.gradle.kts
ohosArm64 { binaries.sharedLib { baseName = "ohoscmd" } }
ohosX64 { binaries.sharedLib { baseName = "ohoscmd" } }
tasks.register<Copy>("copyKnSoArm64") { // linkDebugShared* finalizedBy 自动回填
from(layout.buildDirectory.dir("bin/ohosArm64/debugShared")) { include("libohoscmd.so") }
from(cxxShared("aarch64-linux-ohos")) // libc++_shared.so 同车
into(rootProject.dir("example/harmonyApp/entry/libs/arm64-v8a"))
}
add_library(ohoscmd SHARED IMPORTED)
set_target_properties(ohoscmd PROPERTIES
IMPORTED_LOCATION ${KN_SO} # entry/libs/${OHOS_ARCH}/libohoscmd.so
IMPORTED_NO_SONAME TRUE)
add_library(cmd_napi SHARED napi_init.cpp)
target_link_options(cmd_napi PRIVATE "-Wl,--no-undefined" "-Wl,-rpath,\$ORIGIN")
target_link_libraries(cmd_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohoscmd)
OHOS_ARCH 为空或 libohoscmd.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const cmdBridge: (request: string) => string;
第 6 阶段:ArkUI 宿主
6.1 注入状态和触发操作
aboutToAppear(): void {
this.doPing(); // 版本握手:engine 0.9.0-ohos.1 · kotlin 2.2.21-1.0.0
this.buildDialog(); this.buildPlain(); this.buildSingle();
this.buildMulti(); this.buildColor(); this.buildDate(); this.buildTime();
}
private send(req: BridgeReq): BridgeResp {
const raw = cmdBridge(JSON.stringify(req)); // 同步 NAPI 调用
const resp = JSON.parse(raw) as BridgeResp;
if (!resp.ok) { hilog.error(0x0000, 'CmdDemo', 'bridge error: %{public}s', resp.error ?? ''); }
this.pushLog(req, resp); // →/← 双行留痕,环形缓冲 50 条
return resp;
}
页面只做两件事:把引擎答案画出来、把用户意图喂回去——约 50 个扁平 @State 字段镜像各引擎状态,任何交互都经 send() 往返。
6.2 页面交互
单页纵向滚动,顶部版本握手行 + 横滚 TabChip;中部为当前 Tab 的组件化页面(DialogTab/ListTab/ColorTab/DateTab/TimeTab/AboutTab,均以扁平 @Prop + 回调通信);底部固定 LogPanel 桥日志(→ 请求 / ← 响应,最新在前,失败帧带 error 原文)。
6.3 对比度和错误边界
正键深蓝、负键灰、中性键浅底;错误统一走 ErrBanner + 日志双通道;ok:false 原文可见。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。
6.4 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscmd)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/cmdbridge/
│ └── Bridge.kt @CName × 1 + MiniJson + 会话 + 响应自校验
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.composematerialdialogs、versionName 0.9.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/libohoscmd_api.h
├── src/main/cpp/types/libcmd_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/ pages/Index.ets + components/×9 + model/BridgeTypes.ets
6.5 构建、签名和安装
./gradlew :core:jvmTest # 196 项交叉验证
./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 回填带 material 的完整配置)。
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.composematerialdialogs -m entry
四、完整代码对照
4.1 整体架构
com.vanpra.composematerialdialogs.dialog/**(提取的内核引擎)
│ DialogEngine(生命周期/按钮/回调) + InputEngine + ListEngine
com.vanpra.composematerialdialogs.color/**
│ ColorEngine + ColorPalette(289 色)+ ColorUtils 语义复刻
com.vanpra.composematerialdialogs.datetime/**
│ CivilDate(java.time 同算法) + CalendarEngine + ClockEngine
example/cmdbridge/Bridge.kt + MiniJson
│ CmdBridge(25 op 会话制)
libcmd_napi.so
│ cmdBridge(request) → string
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(单页 6 Tab + 桥日志)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_compose_material_dialogs、上游模块留盘 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 + KMP 插件别名 |
build.gradle.kts(根) | 版本 0.9.0-ohos.1、全局禁用 Ohos*Test* 任务 |
core/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,上游源码目录不注册 |
core/src/commonMain(7 文件) | 6 引擎 + 色板 + CivilDate,每文件 KDoc 冻结上游映射 |
core/src/jvmTest(7 文件,196 项) | 交叉验证:语义复刻 + java.time/公式级对账 |
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 验收页面(会话状态 + send() + 日志) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(20 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | DialogEngine.press(POSITIVE, disableDismiss) | 复刻正键 onClick:先捕获回调表、按 autoDismiss 隐藏、再触发回调 |
| 引擎 | InputEngine.onTextChange(text) | 每键校验;pushValid(dialog) 同步正键门控 |
| 引擎 | ListEngine.click(index, dialog) | 三模式统一入口;disabled 拒绝;plain 触发 hide 副作用 |
| 引擎 | ColorEngine.foreground() | ColorUtils 复合+亮度 → 黑/白前景 |
| 引擎 | CivilDate.of(y,m,d) / monthLength / plusMonths | java.time 同算法公历 |
| 引擎 | CalendarEngine.monthGrid(y,m) | 前导空位 + 日格(enabled/selected) |
| 引擎 | ClockEngine.nearestAnchor(screen,x,y,R) | 双环几何最近锚点吸附 |
| Native | CmdBridge | C ABI JSON 导出(25 op) |
| N-API | cmdBridge(request) | 向 ArkTS 暴露能力 |
| ArkTS | send(req) | 触发操作、镜像 @State、日志留痕 |
4.4 ArkTS 与 Kotlin 的边界
const created = this.send({
op: 'create', id: 'i0', engine: 'input', title: 'INPUT DIALOG',
config: { label: 'Username', validation: 'required', errorMessage: 'Cannot be empty' }
});
const r = this.send({ op: 'input', id: 'i0', text: 'vanpra' }); // r.valid === true
const p = this.send({ op: 'press', id: 'i0', button: 'positive' }); // p.fired === 'input("vanpra")'
is Body.Input -> {
val valid = b.engine.onTextChange(text) // 每键:校验 + 门控同步
b.engine.pushValid(session.dialog)
}
"press" -> {
val fired = session.dialog.press(button, disableDismiss)
r.str("fired", firedPayload(session)) // input("vanpra") / list(0,2) / color(#FF2196F3) / date(2026-10-19) / time(09:30)
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;集合值(cells/selected/channels)由 ArkTS 侧 split(',') 解码。
五、关键决策说明
决策 1:内核提取而不是真编译整库
上游是纯 Compose UI 库,鸿蒙侧既无 Compose 制品、也无其依赖的任何 ohos 变体——“真编译整库"的前提(KStore 式制品线齐全)不存在。对话框的价值恰好高度集中在可提取的决策逻辑上(按钮门控、校验、选择集合、日历数学、锚点几何、颜色科学),UI 壳反而是最薄的一层。提取的纪律是"每条语义指认上游行”,验证的纪律是 jvmTest 逐分支钉死 + 权威实现对账。
决策 2:工具链钉 2.2.21-1.0.0,Gradle 升 8.14.1
内核零依赖,不需要追 2.4.20 线;但上游 wrapper 7.5 太老,这条 Kotlin 线必须配 Gradle 8.x。与 KStore(2.4.20 线沿用 Gradle 9.6.1)互为印证:Gradle 版本跟着工具链线走,不跟上游走,也不跟习惯走。
决策 3:java.time 用同算法自实现,不引 kotlinx-datetime
上游日期逻辑全在 LocalDate/Month/WeekFields 上;kotlinx-datetime 在该目标无制品且远重于需求。CivilDate 约 150 行、采用 java.time 的 OpenJDK 同源算法(era/yoe/doe 分解),JVM 上与 java.time.LocalDate 随机 2×500 次双向对账——权威实现就在测试可达的 JVM 上,对账比手写日历表更有说服力。
决策 4:单入口 JSON 会话制 + 扁平响应
上游 API 是"一个对话框对象挂多个内容组件",天然有状态且组件间共享正键门控——映射为会话制最省桥面。25 个 op 全走 CmdBridge 一个符号;响应永远扁平是 ArkTS typed 上下文的硬约束(sheets 适配实测)。任意 lambda(allowedDateValidator / isTextValid)无法跨 FFI,协议提供 min/max 日期与六种输入校验形态作为等价面。
决策 5:响应自校验作为桥的最后一道闸
真机首跑暴露:响应拼接模板错误("op" 键重复 + 缺逗号)产出非法 JSON,本机编译/链接/打包三层验证全部隐形,真机上表现为一串无法定位的 SyntaxError: Unexpected Object in JSON。修复之外加了结构性防线——每帧生成后 MiniJson.parse 反解自检,非法帧折叠为带原文的 ok:false。桥的 JSON 生成路径没有本地运行时验证机会(ohos 无测试 runner),自校验把这类 bug 的爆炸半径从"全场 SyntaxError"压到"单帧可读错误"。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Adoptium 21.0.12)、Gradle 8.14.1、DevEco Studio 26(内置 hvigor + API 26 SDK)、OpenHarmony 命令行工具(hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :core:jvmTest
196 项用例全部通过(全部新写;含 --rerun-tasks 复跑验证):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
CivilDateTest | 23 | 与 java.time.LocalDate 固定种子随机 2×500 双向对账(1600–2400)、锚点(1970-01-01=0/周四、2000-02-29=11016)、闰年规则、plusMonths 月末钳制 |
DialogEngineTest | 21 | 生命周期、autoDismiss×disableDismiss 四组合、回调注册顺序、all-语义(空 map=true)、reset 清表回滚计数 |
InputEngineTest | 16 | 六种 Validation 通过/拒绝/边界、liveEvent 两种 waitForPositiveButton 时机 |
ListEngineTest | 21 | 三 mode、disabled 拒绝且状态不变、multi 顺序(2→0→取消 2 得 “0”)、single 正键门控、plain 的 hide 副作用 |
ColorEngineTest | 35 | 色板规模 19/19×10/16/16×4、compositeColors/compositeAlpha/calculateLuminance 公式级对账、foreground 黑白判定、hexLabel 补零大写去 alpha、子页导航、四通道滑条 |
CalendarEngineTest | 28 | page 往返、pageCount=2412、navMonth 边界 no-op、leadingBlanks 与 java.time 12 个月对账、月格 selected 只在选中月、min/max 端点、selectYear 跳页 |
ClockEngineTest | 52 | simpleHour/isAM/toAM/toPM、上游怪异 minuteRange 分支(含 61…60 空 AM 区间)、12h/24h 锚点门控与 0↔12 交换、钳制与拒绝、nearestAnchor 几何(顶=0、右=3、24h 内圈=12、暴力枚举对账)、amEnabled quirk、selectPeriod 钳制 |
交叉验证在开发期抓到两个真实内核 bug:CivilDate.of 的 Mar-based +60 调整缺失、nearestAnchor 的 24h 双环几何错误——都在进真机前被钉住。
6.3 HAP 验证
:core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:core:jvmTest: 196 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填)
llvm-nm -D: CmdBridge / libohoscmd_symbols 均为全局 T 符号
default@CompileArkTS: finished
BuildNativeWithCmake / BuildNativeWithNinja: finished(双 ABI 编出 libcmd_napi.so)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 内含双 ABI 各三件套(libcmd_napi.so / libohoscmd.so / libc++_shared.so):KN so arm64 约 2.16MB、x86_64 约 2.11MB(debugShared,含 Kotlin/Native 运行时),NAPI 包装层约 64KB,libc++ 约 1.33MB。
6.4 功能用例
- 启动后版本握手显示
engine 0.9.0-ohos.1 · kotlin 2.2.21-1.0.0(来自真实 Native 调用,非静态文字); - 对话框 Tab:SHOW 后输入空文本——错误消息出现、POSITIVE 状态镜像为禁用;输入
vanpra后门控即时解除;POSITIVE 按下fired显示input("vanpra")、showing 翻 false、callbacks 清空(reset);autoDismiss 开关重创会话后行为随之改变; - 列表 Tab:plain 会话点击行即隐藏;single 未选中时正键禁用、选中即解禁;multi 依次勾选 2→0→取消 2,集合串按
"0"顺序显示;disabled 行点击被拒(accepted=false); - 颜色 Tab:主色选中打勾(勾的颜色按 foreground 黑/白自动切换);进入子色页再返回;ARGB 页四滑条实时改
#FF2196F3,轻透明黑叠白底后前景切黑; - 日期 Tab:2026-10 月格前导 4 个空位(firstDayOfWeek=7 周日);切换月份后选中高亮消失(跨月不显示);翻到 1900-01 与 2100-12 边界箭头 no-op;min/max 外日期灰显且
date.select返回 accepted=false; - 时间 Tab:12h 表盘顶部锚点 “12”(AM→0 点、PM→12 点);切 24h 出现内圈 00/13…23(12 外顶、00 内顶);range 9…17 时范围外锚点禁用;AM 10:30 切 PM 被钳到 17:00(clamped=true);小时选中后 screen 自动变 minute;nearest 查询示例返回吸附锚点与 enabled;
- 每步操作的桥日志按时间戳滚动留痕(→ 请求 / ← 响应),修复后的真机会话无任何
ok:false帧。
6.5 验证结论
内核引擎、自实现公历、颜色科学复刻、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的对话框会话:版本握手、输入校验门控、三模式列表、两级色板与 ARGB、月格导航与范围禁用、双圈表盘与钳制闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-material-dialogs.git
cd compose-material-dialogs
export JAVA_HOME="/path/to/jdk-21"
./gradlew :core:compileKotlinOhosArm64 :core:compileKotlinOhosX64 # klib 编译
./gradlew :core:jvmTest # 196 项交叉验证
./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.composematerialdialogs -m entry
hdc shell hilog | grep CmdNapi
八、遗留问题与改进方向
8.1 踩坑复盘
settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;buildSrc必须改名(本仓 →buildSrc.upstream):Gradle 自动编译 buildSrc,其 AGP 引用让鸿蒙工具链配置期卡死——这与"上游模块留盘不 include"是两种不同处置;- 上游 Gradle 7.5 配不了 Kotlin 2.2.21 线:wrapper 升 8.14.1 后版本目录 alias、RepositoriesMode 全部就位;
Math.powJVM 编译通过、compileKotlinOhosArm64失败(java.lang.Math 在 KN common 不可见)——统一kotlin.math.pow扩展;internal句柄跨 Gradle 模块不可见(nativeApp 摸 core 的 internal 成员报错)——引擎补 publicattach()/pushValid(),桥不碰句柄;- Kotlin 属性 + 同名显式函数在 JVM 上平台签名冲突(
setYearPickerShowing等),删函数保留属性; CivilDate.of漏 Mar-based+60调整——1970-01-01 算出 -60,被 java.time 对账测试当场抓住;- ArkTS 组件成员名撞基类通用属性:
@Prop size/@Prop enabled与CustomComponent的size()/enabled()签名冲突(真机编译报10505001),改名swatchSize/dayEnabled; - struct 内
@State字段与私有方法重名(timeScreen)报Duplicate identifier,还会连带炸出"缺返回类型""String has no call signatures"两个假错误——方法改名即可; - 真机 JSON 拼接 bug:响应模板把
"op":写了两遍且缺逗号,产出{"ok":true,"op":"op":...}非法帧——本机编译/链接/打包全绿,真机上一律SyntaxError: Unexpected Object in JSON、握手失败。修复之外给桥加了响应自校验(每帧反解,非法折叠为带原文的 ok:false);教训是桥的 JSON 生成路径在本地没有运行时验证机会,自检成本一行、收益全场; Scroll({ horizontal: true })在 API 26 编译器被拒,改Scroll()+.scrollable(ScrollDirection.Horizontal);- 命令行 hvigor 的
PackageHap报spawn java ENOENT——打包工具是 Java 程序,把 DevEco 自带 JBR 的 bin 加进 PATH 即可产出未签名 HAP; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可。
8.2 已知问题
- 像素级 UI 不保真:表盘 Canvas、Pager 翻页、swipeable 双页、透明棋盘格由 ArkTS 原生控件近似重绘;决策逻辑与上游逐位一致;
Locale/WeekFields本地化收窄为英文(java.time en 形态)+ 显式firstDayOfWeek参数,ArkTS 可按引擎返回的索引做本地化;- 任意 lambda 验证器不跨桥:
allowedDateValidator/isTextValid以 min/max 日期与六种校验形态等价替代; - 一个会话挂一个 body 引擎(上游可叠多个内容块);Demo 与绝大多数用法一致;
- android 目标与发布链(maven-publish/dokka/spotless/Shot)剥离,本 fork 不发 Maven Central;上游
.githubworkflows 指向 GitHub 生态,随根构建替换一并失效; - 交付形态是源码 + 动态库 + Demo 工程(社区 cmp_qrose 同款),非
.har包。
8.3 未来优化方向
- 桥加"多 body 组合"(一个对话框叠 title+input+buttons,同上游多内容块);
- 月名/星期名 ArkTS 侧本地化(内核已给索引);
- release shared 构建(
linkReleaseSharedOhos*)与符号裁剪,压缩 HAP 内 KN so 体积; hvigorfile挂前置任务,DevEco 构建前自动执行linkDebugShared*,免手动两段构建;- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补真机(x86_64)验收矩阵与自动化截图。
九、总结
9.1 核心难点回顾
上游 core/color/datetime(纯 Compose UI,~4000 行)
→ 依赖度普查(纯逻辑 20~30%)+ 查重
→ 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 7.5→8.14.1 / buildSrc 改名隔离
→ 内核提取:6 引擎 + 289 色板 + CivilDate(每条语义指认上游行)
→ 196 项 jvmTest 钉语义(java.time 2×500 对账抓 1 个内核 bug)
→ C ABI(@CName × 1)+ 25 op 会话制 + 扁平响应 + 自校验
→ C++ N-API(DisposeString 纪律)+ CMake IMPORTED/$ORIGIN
→ ArkTS 6 Tab + 桥日志
→ ArkUI 真机单页验收(握手/校验门控/三列表/色板/月格/表盘钳制)
9.2 封装层次
- 引擎层:
DialogEngine生命周期与按钮门控、InputEngine校验、ListEngine选择集合、ColorEngine颜色科学、CalendarEngine/CivilDate公历、ClockEngine锚点几何与范围; - Native 层:双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:参数检查、字符串转换与 Native 内存释放;
- ArkTS 层:会话状态镜像、JSON 分发、日志与错误文本;
- DevEco 层:CMake、HAP、签名、安装与真机运行。
9.3 三条经验
- 路线选择由当天的制品可达性决定:KStore 适配时 kotlinx ohos 制品线齐全,正确答案是"真编译整库 + 8 行 actual";本库依赖的 Compose 生态在鸿蒙上没有制品,正确答案是"内核提取 + 语义指认"。两条路线不是偏好之争,是事实之分——先验证制品,再定路线;
- 权威实现对账是新写测试的骨架:上游没有可复用的纯逻辑测试,196 项的有效性来自两类锚点——上游源码逐分支复刻(连 quirk 都不修),以及 JVM 可达的权威实现(
java.time.LocalDate、ColorUtils 公式)随机对账。对账测试在开发期抓住两个真实内核 bug,值回全部编写成本; - 桥的运行时路径需要自己的防线:本机三层验证(编译/链接/打包)对"JSON 拼接正确性"完全盲区,真机首跑才暴露。响应自校验一行代码把这类 bug 的爆炸半径从"全场 SyntaxError 无法定位"压到"单帧可读错误"——凡是没有本地运行时验证机会的路径,都要在入口处自证。
9.4 适配成果
当前适配已完成:
- compose-material-dialogs
0.9.0决策内核的双 ABI Kotlin/Native 编译(6 引擎约 1500 行纯 Kotlin,零第三方依赖;上游源码零改动留盘); java.time的同算法自实现(CivilDate)与androidx ColorUtils的逐行复刻,全部经 JVM 权威对账;- 1 个 C ABI 符号(25 op 会话制 JSON 桥,含响应自校验)和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 单页验收页面(6 Tab 全 op 覆盖、版本握手、桥日志面板);
- 196 项 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)