开源鸿蒙平台 CMP 三方库「Persian-Date-picker」适配全流程
本文记录
Persian-Date-picker-kotlin-multiplatform接入 OpenHarmony 的完整过程,覆盖纯 UI 库的适配边界判定、外部依赖内核的重写决策、波斯历天文历算法选型与锚点验证、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
Persian-Date-picker-kotlin-multiplatform是基于 Compose Multiplatform 的 Material 3 波斯(Jalali / شمسی)历日期选择器:单日选择、日期范围选择、Dialog 与 Popup 形态、RTL 界面、可定制配色,支持 Android / iOS / JVM / WASM。本次适配与社区此前两条路线都不同:qrose / haze / PreCompose / voyager 走"零依赖内核提取",KStore 走"社区 kotlinx 制品线补齐后真编译整库"——而本库的特殊性在于决策内核根本不在仓库里:闰年、公历↔Jalali 转换、月名、星期名全部在外部依赖io.github.faridsolgi:persianDateTime:0.2.2(另一个仓库,还拖着没有 ohos 制品的kotlinx-datetime)。因此本 fork 的正确动作是依据上游调用点契约,把选择器赖以决策的波斯历数学以零依赖纯 Kotlin 重写在commonMain(历法转换、闰年、月网格、翻月边界、输入校验、可选日期窗口),编译成 OpenHarmony 双 ABI 动态库,ArkUI 页面复刻 Material 3 选择器交互作为验收宿主——每一条决策语义都与上游实现逐一对照,并由 27 项 JVM 交叉验证测试(含java.time全范围对照与已知天文历锚点)钉死没有漂移。中文主文档见仓库
README.OpenHarmony_CN.md,本文是完整过程记录。
项目地址: AtomGit/oh-tpc/Persian-Date-picker-kotlin-multiplatform.git
上游仓库: faridsolgi/Persian-Date-picker-kotlin-multiplatform
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
波斯历(Jalali)是伊朗、阿富汗的官方历法:一年 12 个月,前六个月每月 31 天、中间五个月每月 30 天、末月 Esfand 平年 29 天闰年 30 天;星期从周六(شنبه)开始,周五(جمعه)是主麻日;新年 Nowruz 定在春分——春分发生在德黑兰视太阳时正午之前,当天即新年,否则顺延一天。它与公历的对应不是固定公式,而是随春分漂移的天文历,这也是本适配最容易踩错的地方。
上游选择器把这些复杂性全部封装在 Compose UI 背后:用户看到 Material 3 网格,背后是 persianDateTime 依赖完成历法转换、PersianCalendarUtils 完成月长与闰年、state 扩展完成翻月边界、PersianDateEnterSection 完成输入校验。OpenHarmony 应用同样需要这套能力,但上游构建(Kotlin 2.2.20 / AGP 8.12.3 / Compose 1.9 / wasm 目标 / dokka / Maven Central 发布链)不会生成 OpenHarmony 变体,其 Compose 依赖栈也进不了鸿蒙工具链。
如果只把示例页面改成 ArkTS,页面可以画出按钮,却无法证明波斯历决策内核在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证历法引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| UI 即全部 | 上游 library 模块 100% 是 commonMain 共享 Compose 代码,没有可直接复用的 nativeMain。 |
| 核心算法在外部依赖 | 闰年、转换、月名、星期名在 io.github.faridsolgi:persianDateTime:0.2.2,该依赖还带 kotlinx-datetime——两者都没有 ohos 制品,"换线不换版"在 这里无从谈起。 |
| 工具链不一致 | 上游 Kotlin 2.2.20 / AGP 8.12.3 / Compose 1.9 / vanniktech 发布插件与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 历法正确性风险 | 闰年若用朴素 33 年周期会与官方天文历漂移(Nowruz 1404 实际在 2025-03-21 而非 03-20),选错算法整个选择器都是错的。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:波斯历决策内核的纯 Kotlin 重写与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。历法正确性由 JVM 侧 27 项交叉验证测试背书,ArkTS 只负责调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的波斯历选择器会话(波斯语 RTL 界面,三个 Tab),对应桥接的全部入口:
- 日历(تقویم / Picker 模式):Material 3 风格月历卡片——标题头以波斯数字显示选中日期(如
یکشنبه ۲۶ مهر ۱۴۰۵)、月份导航行(مهر ۱۴۰۵+ 前后箭头,1300…1499 边界自动禁用)、周六为首的星期表头(ش ی د س چ پ ج)、6×7 日格(波斯数字、今日描边、选中填充)、滑动手势翻月(复刻上游 0.0.14 特性)、年份下拉、今日按钮、选中日详情卡(公历对照、星期名、年内第几天); - 输入与转换(ورودی و تبدیل / Input 模式):输入框复刻上游
DateVisualTransformation(YYYYMMDD输入实时显示为YYYY/MM/DD),满 8 位触发 Native 校验;双向转换卡(شمسی↔میلادی);闰年检查卡(是否闰年、Esfand 天数、年长、前后闰年); - 关于(درباره):
PersianDatePickerVersion()原生握手返回0.0.14-ohos.1、引擎与年份范围信息、调用链说明。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文(含波斯语构造器消息)。 |
op | 回显本次操作名。 |
year / month / day / gregorian[] / jalali[] | Jalali 与公历双侧日期。 |
weekday / weekdayName / monthName / title | 星期编号(1=شنبه…7=جمعه)与波斯语名称。 |
formatSlash / formatMedium / formatLong | ۱۴۰۵/۰۷/۲۶、۲۶ مهر ۱۴۰۵、یکشنبه ۲۶ مهر ۱۴۰۵ 三级格式化。 |
emptyDaysBefore / days[] | 月网格前置空格数与逐日单元格(含 isToday、label、weekIndex、公历与 epoch)。 |
引擎侧完整能力(月长、闰年、双向转换、星期推导、网格布局、翻月边界、输入校验、可选日期窗口、波斯数字格式化)全部在重写的 commonMain 里随编译进入 .so,JVM 侧保留 27 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的历法逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 Compose 源码 git mv 至 composeMain(0 行改动)不进构建;新增引擎三文件(JalaliConverter / PersianDateEngine / PersianDateNames)为全仓唯一新 Kotlin 逻辑,语义逐一对应上游实现。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 :samples(Android Compose 应用)留盘不进构建。 |
| 历法正确性 | breaks 表天文历算法(jalaali-js 同源);JVM 测试钉死 1402–1405 已知 Nowruz 公历日期、1300…1499 全范围与 java.time 往返一致、每年年长符合闰年规则。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| UI 完整 | 三 Tab 覆盖全部桥接入口:日历网格、翻月与滑动、年份下拉、输入校验、双向转换、闰年检查和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的渲染栈(Compose 1.9 / Material 3 Compose)留在
composeMain不参与鸿蒙编译;ArkTS Demo 复刻的是选择器的交互与决策,所有日期数学都来自 Native 调用而非页面内重算。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点源码与依赖边界、确认核心算法在外部依赖、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、composeMain 隔离
第 3 阶段:引擎重写 ── JalaliConverter(breaks 表)+ PersianDateEngine(网格/导航/校验/窗口)+ 名称表
第 4 阶段:交叉验证 ── 27 项 JVM 测试(java.time 对照 + 天文锚点 + 全范围往返)全绿
第 5 阶段:桥协议与原生桥接 ── @CName C ABI、MiniJson、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主与验收 ── 三 Tab RTL Demo、HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
library/src/commonMain/ 新增:波斯历引擎(JalaliConverter / PersianDateEngine / PersianDateNames,零依赖纯 Kotlin)
library/src/commonTest/ 新增:27 项 JVM 交叉验证
library/src/composeMain/ 上游 Compose 源码 + composeResources(git mv,0 行改动,不进构建)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
samples/ 上游 Android 示例(依赖 Compose,留盘不进 settings)
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_persian_date_picker"。上游的 androidLibrary / compose / composeCompiler / dokka / vanniktech 发布 / signing 插件与 en-mirror.ir 镜像全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Windows,org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 钉版,见 8.1 踩坑第 2 条) |
| 版本号 | 0.0.14-ohos.1(gradle.properties 提供) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.persiandatepicker |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点(本适配的成败手)
| 模块 / 依赖 | 内容 | 依赖形态 | 处置 |
|---|---|---|---|
library(上游) | 全部 Compose UI(19 个 kt + 资源) | compose.material3 / ui / foundation / materialIconsExtended | git mv 至 composeMain,不进构建 |
io.github.faridsolgi:persianDateTime:0.2.2 | 历法转换、闰年、月名、星期名、格式化 DSL | 外部 Maven 依赖,带 kotlinx-datetime 0.7.1 | 无 ohos 制品,按调用点契约重写 |
kotlinx-datetime | Instant / TimeZone / LocalDate | 外部 Maven 依赖 | 无 ohos 制品;引擎改为零依赖 + 调用方注入 epoch |
samples(上游) | Android Jetpack Compose 示例 | com.android.application | 留盘不进构建 |
盘点方法是把上游全部 import 与调用点反推成一张 API 面清单:PersianDateTime(year, month, day, hour, minute, second) 构造器(非法值抛波斯语 IllegalArgumentException)、parse(epochMillis) / now()、year/month/day 属性、copy、isLeapYear() / monthLength() / persianDayOfWeek() / toEpochMilliseconds() / toLocalDate() / plusDays / minusDays、PersianWeekday.entries(number、displayName)与格式化 DSL——引擎按这张清单逐条给出等价实现,而不是自由发挥。
1.4 页面边界
标题区 انتخاب تاریخ(选中日期波斯数字大字,如 یکشنبه ۲۶ مهر ۱۴۰۵)
Tab 切换 تقویم / ورودی و تبدیل / درباره
日历卡 月份导航行(前后箭头 + مهر ۱۴۰۵ + 闰年角标)、星期表头(ش ی د س چ پ ج)
6×7 日格(波斯数字、今日描边、选中实心)、PanGesture 横滑翻月
操作区 امروز(回今天)+ 年份 Select 下拉(1300–1499 波斯数字)
详情卡 شمسی / میلادی 对照、روز هفته、روز سال
输入卡 YYYYMMDD→YYYY/MM/DD 视觉转换、8 位触发校验、错误文案(波斯语)
转换卡 شمسی→میلادی / میلادی→شمسی 三输入 + 结果行
闰年卡 年份输入 + 是否闰年 / Esfand 天数 / 年长 / 前后闰年
关于区 版本握手、引擎信息、调用链说明、仓库与社区链接
页面启动时先调 persianDatePickerVersion() 做握手,再用 Date.now() 与 -getTimezoneOffset()*60 注入当前时刻,加载本月网格并选中今天。
第 2 阶段:目标与依赖打通
2.1 为什么是重写内核而不是引依赖
KStore 的前提是社区 Maven 上存在与上游同版本的 kotlinx ohos 制品线(换线不换版)。本库核对后结论相反:persianDateTime 只发布在 Maven Central 且无 ohos 变体;它依赖的 kotlinx-datetime 同样没有 ohos 制品;上游源码又 100% 站在 Compose 上。三条路里只剩一条——把"选择器赖以决策的数学"从 Compose 文件与外部依赖里搬出来,以零依赖纯 Kotlin 重写在 commonMain,用 JVM 测试对齐上游语义。重写范围刻意收窄在决策面(转换 / 闰年 / 网格 / 导航 / 校验 / 窗口 / 名称 / 格式化),渲染与交互不在其内。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
引擎零依赖意味着不需要迁就任何 kotlinx 制品线的 stdlib 元数据,工具链回到社区标准钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze / kmp-wheel-picker 用的同一条线),Gradle 8.14.1。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_persian_date_picker"
include(":library")
include(":example:nativeApp")
gradle.properties 四件套:version=0.0.14-ohos.1、kotlin.native.ignoreDisabledTargets=true、kotlin.native.distribution.downloadFromMaven=true、JDK 21 的 org.gradle.java.home。
2.3 编译引擎(composeMain 隔离)
library/build.gradle.kts 收敛为纯 KMP 模块,commonMain 依赖表清空(Compose 剥离的自然结果):
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游源码的处置是 git mv library/src/commonMain/{kotlin,composeResources} library/src/composeMain/——移动后 composeMain 不是任何 target 的 sourceSet,也没有 compose 插件,这些文件自然不参与任何编译;git 以 100% rename 记录,上游历史完整保留。实测:
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64 # BUILD SUCCESSFUL
根构建保留两条纪律:allprojects 统一 group(io.github.faridsolgi)与版本(providers.gradleProperty("version")),subprojects 禁用所有名字含 Ohos + Test 的任务(ohos 目标没有测试运行器,测试统一在 JVM 跑)。
第 3 阶段:引擎重写(技术难点)
3.1 上游决策面逐一对照
| 上游来源 | 语义 | 引擎落点 |
|---|---|---|
PersianCalenderUtil.getDaysInMonth | 1–6 月 31 天、7–11 月 30 天、12 月闰 30 平 29 | PersianDateEngine.daysInMonth |
PersianCalenderUtil.isLeapYear | 委托 persianDateTime | isLeapYear(天文历,见 3.2) |
PersianDatePickerCalender 网格推导 | firstDayOfWeek.copy(day=1) → firstDayWeekday.number − 1 个前置空格;表头 entries.filterNot{UNKNOWN}.sortedBy{number}.map{displayName.take(1)} | monthGrid(emptyDaysBefore、周六为首的 weekdayHeaders) |
PersianDatePickerStateExtensions | 1 月回上年 12 月、12 月进下年 1 月;年界外 no-op;canNavigate* 边界 | navigate(越界保持不变)与 canNavigate |
PersianDatePicker.kt 输入段 | 仅数字、满 8 位触发、年份范围检查、构造器异常转错误文案 | parseInput(文案见 3.3) |
SelectableDates | isSelectableDate 接缝 | isSelectable(本地午夜粒度闭区间窗口) |
PersianDatePickerDefaults | YearRange = 1300..1499、输入模式年份 3 列 | DEFAULT_MIN_YEAR/MAX_YEAR、yearList |
persianDateTime 转换 / 星期 / 名称 | toLocalDate、persianDayOfWeek().number、月名星期名 | JalaliConverter、dayOfWeek、PersianDateNames |
星期编号沿用上游约定 1 = شنبه(周六)… 7 = جمعه(周五),emptyDaysBefore = number − 1 使周六落在网格第一列(RTL 布局的最右列)。星期由儒略日数直接推出:JDN ≡ 5 (mod 7) 为周六,重排后 dayOfWeek = ((jdn % 7) + 2) % 7 + 1。
3.2 历法算法选型:breaks 表天文历
波斯历的闰年是天文规则,朴素 33 年周期在 1400 年代会漂移——最著名的反例正是本适配必须钉死的 Nowruz 1404 = 2025-03-21:春分 2025-03-20 09:01 UTC 即德黑兰 12:31,落在视太阳时正午之后,新年顺延一天;33 年周期会给出 03-20。引擎采用 jalaali-js / moment-jalaali 一族的 breaks 表算法:20 个断点把 1178–1633 年分段,段内 33 年周期、段间累积修正,jalCal 返回该年元旦对应的公历 3 月日期(march)与闰年指示,jalaliToJdn / jdnToJalali 经 JDN 互转。对本选择器的 1300…1499 全范围与官方天文历一致。算法落在 internal object JalaliConverter,全部整型运算、零依赖、约 100 行。
锚点测试直接写进 commonTest:Nowruz 1402→2023-03-21、1403→2024-03-20(闰年)、1404→2025-03-21、1405→2026-03-21;2026-10-18 = 1405-07-26(یکشنبه / 周日,dayOfWeek = 2);1405/1/1 是 شنبه(dayOfWeek = 1)。
3.3 错误文案复刻
| 场景 | 文案 | 来源 |
|---|---|---|
| 格式(非 8 位) | تاریخ باید به صورت سال ۴ رقمی، سپس ماه ۲ رقمی و سپس روز ۲ رقمی وارد شود.(附示例 ۱۴۰۴/۱۲/۳۰) | 上游 strings.xml 逐字复刻 |
| 年份越界 | سال باید بین ۱۳۰۰ تا ۱۴۹۹ باشد.(数字波斯化,区间参数化) | 上游 strings.xml 逐字复刻 |
| 非法月 / 日 | ماه باید بین ۱ و ۱۲ باشد. / روز باید بین ۱ و <月长> باشد. | 等价复刻(上游原文案在外部依赖构造器内,不进 ohos 构建) |
3.4 时间语义:时钟注入与本地午夜
引擎不读系统时钟(Kotlin/Native 取时钟要平台 API,且不利于测试):now 类操作由 ArkTS 传入 epochMillis 与 tzOffsetSeconds(Demo 默认回落 Asia/Tehran +03:30)。日界用「本地午夜」对齐(dayStartOf 以 floorDiv/floorMod 实现,该 Kotlin 分支的 common stdlib 没有 kotlin.math.floorDiv,见 8.1 第 4 条),保证可选窗口判断不受时分秒影响。epoch 毫秒在 JSON 里以整型收发(< 2^53,双精度安全)。
第 4 阶段:桥协议与原生桥接
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libpersiandatepicker_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohospersiandatepicker.so)
│ PersianDatePickerDispatch / PersianDatePickerVersion
▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
@CName("PersianDatePickerDispatch")
fun persianDatePickerDispatch(request: String): String // 10 个 op 一口进
@CName("PersianDatePickerVersion")
fun persianDatePickerVersion(): String = "0.0.14-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
PersianDatePickerDispatch | persianDatePickerDispatch(req) | version / now / monthInfo / navigate / years / convert / parse / leap / dayDetails / selectable 十个操作 |
PersianDatePickerVersion | persianDatePickerVersion() | fork 版本握手(关于页版本行的数据来源) |
单一分发入口的意义是 ABI 面积恒为 2:新增能力加 op 字段即可,不改符号表。序列化用手写 MiniJson(递归下降解析 + 转义安全的序列化,含 \uXXXX 与控制字符处理),不引 kotlinx.serialization,Native 产物小且无多余运行时符号。每个导出整体 try/catch,Kotlin 异常永远不跨 FFI,全部走 ok:false 通道。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"persianDatePickerDispatch", nullptr, NapiPersianDatePickerDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"persianDatePickerVersion", nullptr, NapiPersianDatePickerVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohospersiandatepicker_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohospersiandatepicker_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号)。
4.4 CMake 和类型声明
add_library(ohospersiandatepicker SHARED IMPORTED)
set_target_properties(ohospersiandatepicker PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(persiandatepicker_napi SHARED napi_init.cpp)
target_link_libraries(persiandatepicker_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohospersiandatepicker)
OHOS_ARCH 为空或 libohospersiandatepicker.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const persianDatePickerDispatch: (request: string) => string;
export const persianDatePickerVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 启动握手与状态注入
aboutToAppear(): void {
this.nativeVersion = persianDatePickerVersion(); // 版本握手:真实 Native 调用
this.today = JSON.parse(send(new NowRequest())) as NowResult;
this.years = (JSON.parse(send(new YearsRequest())) as YearsResult).years;
this.loadMonth(this.today.year, this.today.month);
this.selectDay(this.today.year, this.today.month, this.today.day);
}
所有请求是显式 class 实例(ArkTS 严格模式禁止无类型对象字面量),send() 一行完成 JSON.stringify → dispatch → string。
5.2 页面交互
日历 Tab 自上而下:标题头(选中日期波斯数字大字)、月份导航行(RTL 下第一个子元素在最右——前置箭头›在右、后置箭头‹在左)、星期表头、6×7 日格、امروز 按钮与年份下拉、详情卡。日格支持点击选中(dayDetails op 刷新详情)与 PanGesture 横滑翻月(offsetX > 60 上月 / < −60 下月,先判 canNavigate*)。输入 Tab 的 TextInput 过滤非数字取 8 位,展示层实时插入 /(复刻 DateVisualTransformation),满 8 位调用 parse:错误红字显示引擎返回的波斯语文案,有效则展示 formatLong 与星期。关于 Tab 展示版本握手行、调用链 ArkTS → libpersiandatepicker_napi.so → C ABI → Kotlin/Native 与仓库 / 社区链接。
5.3 错误边界与一个真实崩溃的复盘
ok:false 时错误原文(含波斯语构造器消息)直接进结果区红字;Native 调用本身抛错同样被 try/catch 收进 UI。真机首验时在选中日详情卡崩过一次:
Error message: Cannot load property of null or undefined
at Index.ets:524 → this.selectedDetails.gregorian[0]
根因是桥的 dayDetails 响应漏了 gregorian 字段(当时只有 convert op 单独塞了),而 ArkTS 的 JSON.parse 返回普通对象——类里声明的 gregorian: number[] = [] 默认值在运行时不存在,undefined[0] 直接崩。修复双管齐下:桥的 datePayload 统一输出 gregorian;ArkTS 侧新增 gregorianText() 防御格式化(undefined / 长度不足显示 —),所有数组字段先守卫再下标。这条经验对所有走 JSON 桥的 ArkTS 页面通用:类默认值只是编译期类型标注,响应字段必须由发送方保证或接收方防御。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohospersiandatepicker)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName × 2 + 10 op 分发(继承自 cmp_qrose 审核模式)
│ └── MiniJson.kt 手写 JSON 解析 / 序列化
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── build-hap.cmd 命令行 HAP 构建脚本(DevEco 环境变量注入)
├── AppScope/app.json5 bundleName org.terminator.ohos.persiandatepicker、versionName 0.0.14-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/libohospersiandatepicker_api.h
├── src/main/cpp/types/libpersiandatepicker_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 三 Tab 验收页面(RTL)
6.2 构建、签名和安装
.\gradlew.bat :library:jvmTest # 27 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64
cd example\harmonyApp
.\build-hap.cmd # hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
hdc list targets -v
hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.persiandatepicker -m entry
四、完整代码对照
4.1 整体架构
io.github.faridsolgi.persiancal/**(重写的决策内核,零依赖)
│ JalaliConverter(breaks 表) / PersianDateEngine(网格/导航/校验/窗口) / PersianDateNames
Bridge.kt + MiniJson
│ PersianDatePickerDispatch / PersianDatePickerVersion
libpersiandatepicker_napi.so
│ persianDatePickerDispatch / persianDatePickerVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(三 Tab RTL 滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_persian_date_picker、samples 移出 |
gradle/libs.versions.toml | 只留 Kotlin 2.2.21-1.0.0 + multiplatform 插件别名 |
build.gradle.kts(根) | 版本 0.0.14-ohos.1、全局禁用 Ohos*Test* 任务 |
library/build.gradle.kts | 纯 KMP:jvm() + ohosArm64 + ohosX64,commonMain 依赖清空 |
library/src/commonMain/.../JalaliConverter.kt | breaks 表天文历:jalCal / jalaliToJdn / jdnToJalali / 公历互转 |
library/src/commonMain/.../PersianDateEngine.kt | 月长 / 闰年 / 星期 / 网格 / 导航 / 输入校验 / 可选窗口 / 格式化 |
library/src/commonMain/.../PersianDateNames.kt | 月名、星期名、表头字母、波斯数字 |
library/src/commonTest/.../PersianDateEngineTest.kt | 27 项 JVM 交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、10 op 分发、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 三 Tab 验收页面(约 900 行,严格模式显式类型) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档 |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | JalaliConverter.isLeapYear | 天文历闰年(breaks 表) |
| 引擎 | JalaliConverter.jalaliToJdn / jdnToJalali | Jalali ↔ JDN ↔ 公历互转 |
| 引擎 | PersianDateEngine.daysInMonth | 月长(对应上游 getDaysInMonth) |
| 引擎 | PersianDateEngine.dayOfWeek | 1=شنبه…7=جمعه(对应 persianDayOfWeek().number) |
| 引擎 | PersianDateEngine.monthGrid | 月网格:emptyDaysBefore、逐日单元格(星期 / 公历 / epoch / label) |
| 引擎 | PersianDateEngine.navigate / canNavigate | 翻月与边界(对应 state 扩展,越界 no-op) |
| 引擎 | PersianDateEngine.parseInput | 8 位输入校验(对应 PersianDateEnterSection,文案复刻) |
| 引擎 | PersianDateEngine.isSelectable | 可选日期窗口(对应 SelectableDates) |
| Native | PersianDatePickerDispatch / PersianDatePickerVersion | C ABI JSON 导出 |
| N-API | persianDatePickerDispatch(req) / persianDatePickerVersion() | 向 ArkTS 暴露能力 |
| ArkTS | send(request) | 触发操作并 JSON.parse 刷新 @State |
4.4 ArkTS 与 Kotlin 的边界
const req: MonthInfoRequest = new MonthInfoRequest();
req.year = 1405; req.month = 7;
const grid: MonthInfoResult = JSON.parse(send(req)) as MonthInfoResult;
// grid.emptyDaysBefore = 4、grid.days[25] = {day:26, weekday:2, isToday:true, gregorian:[2026,10,18], ...}
val grid = PersianDateEngine.monthGrid(1405, 7, tz)
Bridge 响应:
{"ok":true,"op":"monthInfo","title":"مهر ۱۴۰۵","emptyDaysBefore":4,
"days":[{"day":1,"weekday":5,...},{"day":26,"weekday":2,"isToday":true,
"gregorian":[2026,10,18],"epochMillis":1792269000000,...}],"canNavigateToNextMonth":true,...}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:重写决策内核而不是引入外部依赖
persianDateTime 与 kotlinx-datetime 都没有 ohos 制品,"换线不换版"不可用;把两个依赖源码整体拷进来则要连 kotlinx-datetime 一起维护。重写收窄在决策面(约 300 行纯 Kotlin),渲染与交互不碰——上游 UI 一行不改地留在 composeMain,历法语义由 27 项测试对齐。这与 KStore 的"真编译整库"不矛盾:路线由当天的制品可达性决定,本库的可达性为零。
决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1
引擎零依赖,不迁就任何制品线的 stdlib 元数据,回到社区标准钉法(qrose / kmp-wheel-picker 同线)。Gradle 8.14.1 与该 Kotlin 线配套(KStore 的 2.4.20 线才支持 Gradle 9.x,两条线的经验不能互相套用)。
决策 3:根构建替换(cmp_qrose 模式),composeMain 隔离
上游根构建(AGP / compose / dokka / vanniktech / signing / 伊朗镜像)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。上游源码用 git mv 进 composeMain——不是删除而是隔离:git 100% rename 保留历史,将来 Compose 鸿蒙线追平上游版本时可以整体搬回。
决策 4:breaks 表天文历而不是 33 年周期
选择器的正确性底线是历法正确。33 年周期在 1404 这类年份直接错一天;breaks 表算法(jalaali-js 同源)对 1300…1499 与官方天文历一致,成本只是约 40 行断点修正。测试用已知 Nowruz 公历日期钉死,防止未来改动回退。
决策 5:时钟注入而不是引擎读时钟
引擎保持纯函数:now 类操作由调用方传 epochMillis + tzOffsetSeconds。好处有三——Kotlin/Native 不碰平台时钟 API、JVM 测试可以测任意时刻、时区语义显式(Demo 用设备时区,默认回落 Asia/Tehran)。日界以本地午夜对齐,可选窗口不受时分秒影响。
决策 6:jvmTest 交叉验证 + 单一分发入口
引擎逻辑在 commonMain,JVM 与 ohos 编译同一份代码;27 项 jvmTest 是"内核没漂"的闸门。桥只导出 2 个符号、10 个 op 走 JSON 分发——新增能力不改 ABI,ArkTS 升级不依赖 so 重编(除引擎本身变更外)。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Eclipse Adoptium)、DevEco Studio 5.1.0 Release(E:\DE\DevEco Studio)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙模拟器(x86_64)。
6.2 JVM 交叉验证
./gradlew :library:jvmTest
27 项用例全部通过:
| 测试类方法 | 用例数 | 钉住什么 |
|---|---|---|
jdnAnchors / jdnGregorianRoundTrip | 2 | JDN 锚点(1970-01-01=2440588、2000-01-01=2451545)+ 9 万天往返 |
knownNowruzDates / knownLeapYears | 2 | 天文历锚点:Nowruz 1402–1405 公历日期、1399/1403/1408 闰 |
todayGregorianToJalali / farvardinFirstOf1405IsSaturday | 2 | 2026-10-18=1405-07-26(یکشنبه)、1405/1/1=شنبه |
jalaliRangeRoundTripAgainstJavaTime | 1 | 1300…1499 每月首/中/末日与 java.time 往返一致、相邻日连续 |
yearLengthMatchesLeapRule / daysInMonthMatchesUpstreamRule / invalidMonthThrows | 3 | 年长 365/366、月长 31/30/29-30、非法月日抛异常 |
names / persianDigits / formatting | 3 | 月名星期名表头、波斯数字、三级格式化(۱۴۰۵/۰۷/۲۶ 等) |
epochRoundTrip / jalaliEpochMatchesGregorianEpochDay / dayStartOfUsesLocalMidnight | 3 | epoch 互转、与 LocalDate.toEpochDay 对齐、本地午夜吸附 |
monthGridMehr1405 / monthGridAlignsWeekdayColumns | 2 | مهر ۱۴۰۵ 网格(emptyDaysBefore=4、day26 公历对照)、13 年 × 12 月列对齐 |
navigationWithinRange / navigationClampsAtYearRange / yearListCoversPickerRange | 3 | 翻月换年、1300/1499 边界 no-op、年份清单 200 项 |
parseInputAcceptsValidDates / parseInputRejectsBadPattern / parseInputRejectsYearOutsideRange / parseInputRejectsInvalidMonthAndDay | 4 | 输入校验全分支(含 1403 闰日合法、1404 Esfand 30 非法) |
selectableWindow / dayOfYearMath | 2 | 可选窗口闭区间、dayOfYear(366/365 边界) |
其中 jalaliRangeRoundTripAgainstJavaTime 是分量最重的一条:它以 java.time.LocalDate 为独立参照系,把整个 yearRange 的转换一致性一次性验完。
6.3 HAP 验证
:library:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 自动回填)
llvm-nm --defined-only: PersianDatePickerDispatch / PersianDatePickerVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libpersiandatepicker_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 7.0 MB,内含双 ABI 各三件套(libpersiandatepicker_napi.so / libohospersiandatepicker.so / libc++_shared.so);KN so arm64 约 3.8MB、x86_64 约 3.5MB(debugShared 未 strip,含 Kotlin/Native 运行时)。
6.4 功能用例
- 启动后关于页显示
persianDatePickerVersion() → 0.0.14-ohos.1(来自真实 Native 调用,非静态文字); - 日历网格周六在第一列(RTL 最右列),
مهر ۱۴۰۵的 26 日今日描边高亮; - 点击任意日格,详情卡显示
شمسی: ۱۴۰۵/۰۷/۲۶、میلادی: 2026/10/18、星期与年内第几天(修复gregorian缺字段崩溃后的回归项); - 前后箭头翻月跨年正确(1405/1→1404/12、1404/12→1405/1),滑动手势同样生效;
- 翻到 1300/1 与 1499/12 时对应方向箭头禁用(no-op 语义);
- 年份下拉列出 1300–1499 波斯数字,选择后网格切换;
- 输入
14031230合法(闰年 Esfand 30),14041230报روز باید بین ۱ و ۲۹ باشد.,12990101报年份范围错误,非 8 位报格式错误; - 转换卡 1405/7/26 → 2026/10/18,反向 2026/10/18 → 1405/7/26 一致;
- 闰年卡 1403 → 闰(年长 366、Esfand 30)、1404 → 平(365、29);
- 关于页三 Tab 波斯语 RTL 布局正常,中英文说明与 AtomGit 链接可读。
6.5 验证结论
波斯历引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙设备(含 x86_64 模拟器)上成功安装并进入前台,页面展示了真实的波斯历会话:版本握手、月历网格、翻月与滑动、年份选择、输入校验、双向转换、闰年检查(见下节效果图)。
七、运行效果
7.1 运行截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/Persian-Date-picker-kotlin-multiplatform.git
cd Persian-Date-picker-kotlin-multiplatform
export JAVA_HOME="/path/to/jdk-21" # Windows: set JAVA_HOME=...
./gradlew :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64 # klib
./gradlew :library:jvmTest # 27 项交叉验证
./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.persiandatepicker -m entry
hdc shell hilog | grep PersianDatePicker
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让决策内核真正编译成
ohosArm64/ohosX64动态库——本库的内核还要先从外部依赖的调用点契约里"考古"出来; - 上游 wrapper 是 8.14.3,本机 dist 不完整且网络超时:
gradle-wrapper.properties钉回社区标准 8.14.1(本机已有完整缓存),与参考仓库工具链一致; - HarmonyOS Kotlin 分支的 common stdlib 没有
kotlin.math.floorDiv/floorMod,Math.floorDiv又是 JVM 专属——epoch 日界运算要自带私有 floor 实现; String.format同样是 JVM 专属,MiniJson 的\uXXXX转义要手写十六进制展开;@CName桥的 inline 高阶参数要crossinline,否则"可能包含非局部返回"直接拒绝编译;- Kotlin 的 Map 取值返回可空,JSON 访问器必须声明为可空接收者的顶层扩展(成员函数调不上
MiniJson?); - 源码里写字面控制字符(换页符)会形成非法字符字面量,统一改
'\u000C'; - ArkTS 严格模式:请求/响应全部显式 class(禁止无类型对象字面量)、泛型
as T不可靠——改为每处JSON.parse(raw) as 具体类型; JSON.parse出来的普通对象没有类默认值:dayDetails响应漏了gregorian字段,undefined[0]在真机崩Cannot load property of null or undefined——发送方保证字段、接收方防御下标,双端都要做;- ArkTS 的 Scroll 组件没有
scrollBarBarState,正确属性是scrollBar(BarState.Off)(编译期才报错); - RTL 布局下 Row 第一个子元素在最右——"上一月›在右、下一月‹在左"的顺序要与 LTR 直觉反着写;
entry/libs/、oh_modules/、.hvigor/、entry/build/、.cxx与*.so要追加 gitignore(根构建的/build/盖不住嵌套目录),so 由 Gradle Copy 任务回填,clone 后先跑 link 任务;build-profile.json5的signingConfigs仓库内留空数组,签名由 DevEco 自动生成回填,证书绝不进入仓库;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可(DevEco 模拟器是 x86_64); - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free()。
8.2 已知问题
- Compose UI(
PersianDatePicker/PersianDateRangePicker/ Dialog / Popup)不参与鸿蒙编译,ArkTS Demo 复刻选择器交互与决策而非 Compose 渲染;HarmonyOS CMP 社区线(Compose 1.9.2)与上游工具链无法共编; - 构造器错误文案为等价复刻(上游原文案在外部依赖内);年份范围与格式错误文案逐字复刻自上游
strings.xml; - Range Picker 未做独立双面板 UI:起止窗口语义已通过
selectableop(本地午夜闭区间)暴露; - 引擎不读设备时钟,
now类操作需要 ArkTS 传epochMillis(纯函数化的代价); - android / ios / wasm 目标剥离(源文件留在
composeMain树里),发布、dokka 一并剥离,本 fork 不发 Maven Central; - HAP 安装必须使用与 bundle name
org.terminator.ohos.persiandatepicker匹配的签名。
8.3 未来优化方向
- 补 Range Picker 双面板 ArkTS UI(起止日高亮连区间),把
selectable窗口语义画满; - 桥增加
rangeop 返回起止区间逐日状态,减少 ArkTS 循环调用; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(arm64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 library(100% Compose commonMain)+ 外部 persianDateTime / kotlinx-datetime(无 ohos 制品)
→ 适配边界判定:重写决策内核,UI 隔离(composeMain git mv,0 行改动)
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1)
→ 根构建替换(cmp_qrose 模式)
→ JalaliConverter(breaks 表天文历)+ PersianDateEngine + PersianDateNames(零依赖纯 Kotlin)
→ 27 项 jvmTest(java.time 全范围对照 + Nowruz 锚点)全绿
→ C ABI(@CName × 2,10 op JSON 分发)+ MiniJson
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析(严格模式显式 class + 防御性字段访问)
→ ArkUI 三 Tab RTL 真机验收(日历 / 输入转换 / 闰年 / 版本握手)
9.2 封装层次
- 引擎层:breaks 表
JalaliConverter、PersianDateEngine(月长 / 闰年 / 星期 / 网格 / 导航 / 校验 / 窗口)、PersianDateNames(名称与波斯数字); - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、请求封装、JSON 解析、防御性字段访问与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配路线由当天的制品可达性决定:KStore 有 kotlinx ohos 制品线所以真编译整库;本库的
persianDateTime/kotlinx-datetime可达性为零,正确动作是按调用点契约重写决策面并用独立参照系(java.time+ 天文历锚点)钉死正确性——重写不可怕,没有对照系的重写才可怕; - UI 库的适配边界是"决策"而不是"渲染":把网格推导、翻月边界、输入校验这些 Compose 文件里的数学搬进纯 Kotlin,渲染留给 ArkUI 复刻;上游源码用
git mv隔离而不是删除,历史与升级路径都保留; - JSON 桥两侧都要为"缺字段"负责:发送方保证响应字段完整(
datePayload统一输出),接收方防御下标(gregorianText先守卫再取值)——ArkTS 的类默认值在JSON.parse结果上不存在,这条崩溃在真机首验当天就教育了我们。
9.4 适配成果
当前适配已完成:
- 波斯历决策内核的双 ABI Kotlin/Native 编译(重写引擎三文件约 600 行,零依赖;上游 Compose 源码
git mv隔离,0 行改动); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号),10 个 op 的 JSON 分发协议; - DevEco Stage ArkUI 三 Tab 验收页面(RTL、波斯数字、月历网格、滑动翻月、输入校验、双向转换、闰年检查、版本握手);
- 27 项 JVM 交叉验证全绿(
java.time全范围对照 + 天文历锚点)+ HAP 构建(双 ABI 六件套 so,约 7MB)、签名、设备安装和真机 / 模拟器验证; - 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)