本文记录 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Windows,org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 钉版,见 8.1 踩坑第 2 条)
版本号0.0.14-ohos.1(gradle.properties 提供)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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 / materialIconsExtendedgit mv 至 composeMain,不进构建
io.github.faridsolgi:persianDateTime:0.2.2历法转换、闰年、月名、星期名、格式化 DSL外部 Maven 依赖,带 kotlinx-datetime 0.7.1无 ohos 制品,按调用点契约重写
kotlinx-datetimeInstant / 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.getDaysInMonth1–6 月 31 天、7–11 月 30 天、12 月闰 30 平 29PersianDateEngine.daysInMonth
PersianCalenderUtil.isLeapYear委托 persianDateTimeisLeapYear(天文历,见 3.2)
PersianDatePickerCalender 网格推导firstDayOfWeek.copy(day=1) → firstDayWeekday.number − 1 个前置空格;表头 entries.filterNot{UNKNOWN}.sortedBy{number}.map{displayName.take(1)}monthGrid(emptyDaysBefore、周六为首的 weekdayHeaders)
PersianDatePickerStateExtensions1 月回上年 12 月、12 月进下年 1 月;年界外 no-op;canNavigate* 边界navigate(越界保持不变)与 canNavigate
PersianDatePicker.kt 输入段仅数字、满 8 位触发、年份范围检查、构造器异常转错误文案parseInput(文案见 3.3)
SelectableDatesisSelectableDate 接缝isSelectable(本地午夜粒度闭区间窗口)
PersianDatePickerDefaultsYearRange = 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 方法覆盖内核
PersianDatePickerDispatchpersianDatePickerDispatch(req)version / now / monthInfo / navigate / years / convert / parse / leap / dayDetails / selectable 十个操作
PersianDatePickerVersionpersianDatePickerVersion()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.ktbreaks 表天文历:jalCal / jalaliToJdn / jdnToJalali / 公历互转
library/src/commonMain/.../PersianDateEngine.kt月长 / 闰年 / 星期 / 网格 / 导航 / 输入校验 / 可选窗口 / 格式化
library/src/commonMain/.../PersianDateNames.kt月名、星期名、表头字母、波斯数字
library/src/commonTest/.../PersianDateEngineTest.kt27 项 JVM 交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、10 op 分发、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 三 Tab 验收页面(约 900 行,严格模式显式类型)
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
引擎JalaliConverter.isLeapYear天文历闰年(breaks 表)
引擎JalaliConverter.jalaliToJdn / jdnToJalaliJalali ↔ JDN ↔ 公历互转
引擎PersianDateEngine.daysInMonth月长(对应上游 getDaysInMonth)
引擎PersianDateEngine.dayOfWeek1=شنبه…7=جمعه(对应 persianDayOfWeek().number)
引擎PersianDateEngine.monthGrid月网格:emptyDaysBefore、逐日单元格(星期 / 公历 / epoch / label)
引擎PersianDateEngine.navigate / canNavigate翻月与边界(对应 state 扩展,越界 no-op)
引擎PersianDateEngine.parseInput8 位输入校验(对应 PersianDateEnterSection,文案复刻)
引擎PersianDateEngine.isSelectable可选日期窗口(对应 SelectableDates)
NativePersianDatePickerDispatch / PersianDatePickerVersionC ABI JSON 导出
N-APIpersianDatePickerDispatch(req) / persianDatePickerVersion()向 ArkTS 暴露能力
ArkTSsend(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 / jdnGregorianRoundTrip2JDN 锚点(1970-01-01=2440588、2000-01-01=2451545)+ 9 万天往返
knownNowruzDates / knownLeapYears2天文历锚点:Nowruz 1402–1405 公历日期、1399/1403/1408 闰
todayGregorianToJalali / farvardinFirstOf1405IsSaturday22026-10-18=1405-07-26(یکشنبه)、1405/1/1=شنبه
jalaliRangeRoundTripAgainstJavaTime11300…1499 每月首/中/末日与 java.time 往返一致、相邻日连续
yearLengthMatchesLeapRule / daysInMonthMatchesUpstreamRule / invalidMonthThrows3年长 365/366、月长 31/30/29-30、非法月日抛异常
names / persianDigits / formatting3月名星期名表头、波斯数字、三级格式化(۱۴۰۵/۰۷/۲۶ 等)
epochRoundTrip / jalaliEpochMatchesGregorianEpochDay / dayStartOfUsesLocalMidnight3epoch 互转、与 LocalDate.toEpochDay 对齐、本地午夜吸附
monthGridMehr1405 / monthGridAlignsWeekdayColumns2مهر ۱۴۰۵ 网格(emptyDaysBefore=4、day26 公历对照)、13 年 × 12 月列对齐
navigationWithinRange / navigationClampsAtYearRange / yearListCoversPickerRange3翻月换年、1300/1499 边界 no-op、年份清单 200 项
parseInputAcceptsValidDates / parseInputRejectsBadPattern / parseInputRejectsYearOutsideRange / parseInputRejectsInvalidMonthAndDay4输入校验全分支(含 1403 闰日合法、1404 Esfand 30 非法)
selectableWindow / dayOfYearMath2可选窗口闭区间、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 功能用例

  1. 启动后关于页显示 persianDatePickerVersion() → 0.0.14-ohos.1(来自真实 Native 调用,非静态文字);
  2. 日历网格周六在第一列(RTL 最右列),مهر ۱۴۰۵ 的 26 日今日描边高亮;
  3. 点击任意日格,详情卡显示 شمسی: ۱۴۰۵/۰۷/۲۶、میلادی: 2026/10/18、星期与年内第几天(修复 gregorian 缺字段崩溃后的回归项);
  4. 前后箭头翻月跨年正确(1405/1→1404/12、1404/12→1405/1),滑动手势同样生效;
  5. 翻到 1300/1 与 1499/12 时对应方向箭头禁用(no-op 语义);
  6. 年份下拉列出 1300–1499 波斯数字,选择后网格切换;
  7. 输入 14031230 合法(闰年 Esfand 30),14041230 报 روز باید بین ۱ و ۲۹ باشد.,12990101 报年份范围错误,非 8 位报格式错误;
  8. 转换卡 1405/7/26 → 2026/10/18,反向 2026/10/18 → 1405/7/26 一致;
  9. 闰年卡 1403 → 闰(年长 366、Esfand 30)、1404 → 平(365、29);
  10. 关于页三 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让决策内核真正编译成 ohosArm64 / ohosX64 动态库——本库的内核还要先从外部依赖的调用点契约里"考古"出来;
  2. 上游 wrapper 是 8.14.3,本机 dist 不完整且网络超时:gradle-wrapper.properties 钉回社区标准 8.14.1(本机已有完整缓存),与参考仓库工具链一致;
  3. HarmonyOS Kotlin 分支的 common stdlib 没有 kotlin.math.floorDiv/floorMod,Math.floorDiv 又是 JVM 专属——epoch 日界运算要自带私有 floor 实现;
  4. String.format 同样是 JVM 专属,MiniJson 的 \uXXXX 转义要手写十六进制展开;
  5. @CName 桥的 inline 高阶参数要 crossinline,否则"可能包含非局部返回"直接拒绝编译;
  6. Kotlin 的 Map 取值返回可空,JSON 访问器必须声明为可空接收者的顶层扩展(成员函数调不上 MiniJson?);
  7. 源码里写字面控制字符(换页符)会形成非法字符字面量,统一改 '\u000C';
  8. ArkTS 严格模式:请求/响应全部显式 class(禁止无类型对象字面量)、泛型 as T 不可靠——改为每处 JSON.parse(raw) as 具体类型;
  9. JSON.parse 出来的普通对象没有类默认值:dayDetails 响应漏了 gregorian 字段,undefined[0] 在真机崩 Cannot load property of null or undefined——发送方保证字段、接收方防御下标,双端都要做;
  10. ArkTS 的 Scroll 组件没有 scrollBarBarState,正确属性是 scrollBar(BarState.Off)(编译期才报错);
  11. RTL 布局下 Row 第一个子元素在最右——"上一月›在右、下一月‹在左"的顺序要与 LTR 直觉反着写;
  12. entry/libs/、oh_modules/、.hvigor/、entry/build/、.cxx 与 *.so 要追加 gitignore(根构建的 /build/ 盖不住嵌套目录),so 由 Gradle Copy 任务回填,clone 后先跑 link 任务;
  13. build-profile.json5 的 signingConfigs 仓库内留空数组,签名由 DevEco 自动生成回填,证书绝不进入仓库;
  14. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可(DevEco 模拟器是 x86_64);
  15. 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:起止窗口语义已通过 selectable op(本地午夜闭区间)暴露;
  • 引擎不读设备时钟,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 窗口语义画满;
  • 桥增加 range op 返回起止区间逐日状态,减少 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 三条经验

  1. 适配路线由当天的制品可达性决定:KStore 有 kotlinx ohos 制品线所以真编译整库;本库的 persianDateTime / kotlinx-datetime 可达性为零,正确动作是按调用点契约重写决策面并用独立参照系(java.time + 天文历锚点)钉死正确性——重写不可怕,没有对照系的重写才可怕;
  2. UI 库的适配边界是"决策"而不是"渲染":把网格推导、翻月边界、输入校验这些 Compose 文件里的数学搬进纯 Kotlin,渲染留给 ArkUI 复刻;上游源码用 git mv 隔离而不是删除,历史与升级路径都保留;
  3. 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。

参考文档

Logo

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

更多推荐