本文记录 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% 是 @Composable UI(依赖 androidx.compose 1.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 大字与黑/白前景自动切换(androidx ColorUtils 复合+亮度语义复刻);
  • 日期 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home 钉死 Adoptium 21.0.12)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 wrapper 7.5 必须升级):KMP 2.2.21 线要求 8.x,且 RepositoriesMode/版本目录 alias 特性 7.5 不可用
内核依赖仅 Kotlin stdlib(无 kotlinx、无 Compose)
DevEco product5.1.0(18) 兼容 / API 26
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.composematerialdialogs
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度普查

逐文件标注"纯逻辑 / Compose UI / Android 平台",结论决定每个模块的处置:

上游模块文件纯逻辑占比处置
:core MaterialDialog.kt / Buttons / Input / Lists6 文件 ≈1220 行~30%:状态机、按钮门控、校验、选择集合提取 3 引擎;UI 留盘
:color ColorPicker.kt / ColorPalette.kt2 文件 ≈770 行~25%:289 色板、ARGB/hex、亮度前景提取引擎+色板转译;AndroidView 棋盘格留盘
:datetime date/ 9 文件 time/9 文件 ≈1980 行~20%:日历数学(java.time)、锚点/范围/AM-PM提取 2 引擎 + CivilDate;Pager/Canvas 留盘
:app / :test-utilsdemo / 测试基建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.ktDialogEnginepositiveButtonEnabled.values.all { it }(空 map=true);正键 onClick 先隐藏后触发回调、reset 在下一帧才清表(桥先捕获再 hide);autoDismiss × disableDismiss 四组合;负键仅 autoDismiss 隐藏;中性键永不隐藏
MaterialDialogInput.ktInputEnginevalid = isTextValid(text) 每键重算;waitForPositiveButton 决定 onInput 时机;isError + errorMessage 恰在 !valid 出现
MaterialDialogLists.ktListEngine多选 toggle 保持插入顺序(上游重建 mutable set);单选未选中时正键禁用;disabled 索引无操作;plain 点击先 hide 再回调
ColorPicker.kt + ColorPalette.ktColorEngine + ColorPalette289 色板 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.ktClockEngine锚点角 (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 方法覆盖内核
CmdBridgecmdBridge(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.tomlKotlin 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.kt1 个 C ABI、会话制、MiniJson、错误通道、响应自校验
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(会话状态 + 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 / plusMonthsjava.time 同算法公历
引擎CalendarEngine.monthGrid(y,m)前导空位 + 日格(enabled/selected)
引擎ClockEngine.nearestAnchor(screen,x,y,R)双环几何最近锚点吸附
NativeCmdBridgeC ABI JSON 导出(25 op)
N-APIcmdBridge(request)向 ArkTS 暴露能力
ArkTSsend(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 复跑验证):

测试类用例数钉住什么
CivilDateTest23与 java.time.LocalDate 固定种子随机 2×500 双向对账(1600–2400)、锚点(1970-01-01=0/周四、2000-02-29=11016)、闰年规则、plusMonths 月末钳制
DialogEngineTest21生命周期、autoDismiss×disableDismiss 四组合、回调注册顺序、all-语义(空 map=true)、reset 清表回滚计数
InputEngineTest16六种 Validation 通过/拒绝/边界、liveEvent 两种 waitForPositiveButton 时机
ListEngineTest21三 mode、disabled 拒绝且状态不变、multi 顺序(2→0→取消 2 得 “0”)、single 正键门控、plain 的 hide 副作用
ColorEngineTest35色板规模 19/19×10/16/16×4、compositeColors/compositeAlpha/calculateLuminance 公式级对账、foreground 黑白判定、hexLabel 补零大写去 alpha、子页导航、四通道滑条
CalendarEngineTest28page 往返、pageCount=2412、navMonth 边界 no-op、leadingBlanks 与 java.time 12 个月对账、月格 selected 只在选中月、min/max 端点、selectYear 跳页
ClockEngineTest52simpleHour/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 功能用例

  1. 启动后版本握手显示 engine 0.9.0-ohos.1 · kotlin 2.2.21-1.0.0(来自真实 Native 调用,非静态文字);
  2. 对话框 Tab:SHOW 后输入空文本——错误消息出现、POSITIVE 状态镜像为禁用;输入 vanpra 后门控即时解除;POSITIVE 按下 fired 显示 input("vanpra")、showing 翻 false、callbacks 清空(reset);autoDismiss 开关重创会话后行为随之改变;
  3. 列表 Tab:plain 会话点击行即隐藏;single 未选中时正键禁用、选中即解禁;multi 依次勾选 2→0→取消 2,集合串按 "0" 顺序显示;disabled 行点击被拒(accepted=false);
  4. 颜色 Tab:主色选中打勾(勾的颜色按 foreground 黑/白自动切换);进入子色页再返回;ARGB 页四滑条实时改 #FF2196F3,轻透明黑叠白底后前景切黑;
  5. 日期 Tab:2026-10 月格前导 4 个空位(firstDayOfWeek=7 周日);切换月份后选中高亮消失(跨月不显示);翻到 1900-01 与 2100-12 边界箭头 no-op;min/max 外日期灰显且 date.select 返回 accepted=false;
  6. 时间 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;
  7. 每步操作的桥日志按时间戳滚动留痕(→ 请求 / ← 响应),修复后的真机会话无任何 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 踩坑复盘

  1. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  2. buildSrc 必须改名(本仓 → buildSrc.upstream):Gradle 自动编译 buildSrc,其 AGP 引用让鸿蒙工具链配置期卡死——这与"上游模块留盘不 include"是两种不同处置;
  3. 上游 Gradle 7.5 配不了 Kotlin 2.2.21 线:wrapper 升 8.14.1 后版本目录 alias、RepositoriesMode 全部就位;
  4. Math.pow JVM 编译通过、compileKotlinOhosArm64 失败(java.lang.Math 在 KN common 不可见)——统一 kotlin.math.pow 扩展;
  5. internal 句柄跨 Gradle 模块不可见(nativeApp 摸 core 的 internal 成员报错)——引擎补 public attach()/pushValid(),桥不碰句柄;
  6. Kotlin 属性 + 同名显式函数在 JVM 上平台签名冲突(setYearPickerShowing 等),删函数保留属性;
  7. CivilDate.of 漏 Mar-based +60 调整——1970-01-01 算出 -60,被 java.time 对账测试当场抓住;
  8. ArkTS 组件成员名撞基类通用属性:@Prop size / @Prop enabled 与 CustomComponent 的 size()/enabled() 签名冲突(真机编译报 10505001),改名 swatchSize / dayEnabled;
  9. struct 内 @State 字段与私有方法重名(timeScreen)报 Duplicate identifier,还会连带炸出"缺返回类型""String has no call signatures"两个假错误——方法改名即可;
  10. 真机 JSON 拼接 bug:响应模板把 "op": 写了两遍且缺逗号,产出 {"ok":true,"op":"op":...} 非法帧——本机编译/链接/打包全绿,真机上一律 SyntaxError: Unexpected Object in JSON、握手失败。修复之外给桥加了响应自校验(每帧反解,非法折叠为带原文的 ok:false);教训是桥的 JSON 生成路径在本地没有运行时验证机会,自检成本一行、收益全场;
  11. Scroll({ horizontal: true }) 在 API 26 编译器被拒,改 Scroll() + .scrollable(ScrollDirection.Horizontal);
  12. 命令行 hvigor 的 PackageHap 报 spawn java ENOENT——打包工具是 Java 程序,把 DevEco 自带 JBR 的 bin 加进 PATH 即可产出未签名 HAP;
  13. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  14. 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;上游 .github workflows 指向 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 三条经验

  1. 路线选择由当天的制品可达性决定:KStore 适配时 kotlinx ohos 制品线齐全,正确答案是"真编译整库 + 8 行 actual";本库依赖的 Compose 生态在鸿蒙上没有制品,正确答案是"内核提取 + 语义指认"。两条路线不是偏好之争,是事实之分——先验证制品,再定路线;
  2. 权威实现对账是新写测试的骨架:上游没有可复用的纯逻辑测试,196 项的有效性来自两类锚点——上游源码逐分支复刻(连 quirk 都不修),以及 JVM 可达的权威实现(java.time.LocalDate、ColorUtils 公式)随机对账。对账测试在开发期抓住两个真实内核 bug,值回全部编写成本;
  3. 桥的运行时路径需要自己的防线:本机三层验证(编译/链接/打包)对"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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐