开源鸿蒙平台 CMP 三方库「RowKalendar」适配全流程
本文记录
RowKalendar接入 OpenHarmony 的完整过程,覆盖 Compose UI 库盘点、kotlinx-datetime鸿蒙制品线选型、真实日历引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
RowKalendar是 chouaibMo 开发的 Compose Multiplatform 横向滚动日历库:一个RowKalendarcomposable 包住LazyRow,内部RowKalendarViewModel以今天为中心维护日期窗口(初始[-30,+30]),滚动触边时分页加载(每次 30 天、每侧封顶maxDays),每个日期格由DateCell按 selected / past / future 三态渲染。本次适配与存储类库(KStore 的"真编译整库")有本质区别:RowKalendar是纯 UI 库,六个源文件里五个锚定 Compose / AndroidX 生命周期,无法整体编进鸿蒙——因此沿用社区已过审的 cmp_qrose 路线,日历引擎(上游 ViewModel 的日期窗口状态机)1:1 去 Compose 化移植进commonMain原样编译成 OpenHarmony KLIB,Compose UI 物理隔离到不编译的composeMain源集,一个带触边分页、选中态、边界预算的完整日历引擎运行在真机上,ArkUI 页面按 JSON 快照重新渲染横向日历行。
项目地址: AtomGit/oh-tpc/RowKalendar.git
上游仓库: chouaibMo/RowKalendar
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
RowKalendar 为 Compose Multiplatform 应用提供"横向滚动选日期"这一件事的完整答案:RowKalendar(isBounded, maxDays, content) 一个 composable 开箱即用,内置 DateCell 日期格支持 shape / colors / border / elevation 四维定制(selected / past / future 三态各一套),也可以换任意自定义 composable;ViewModel 负责日期窗口的全部生命周期——冷启动以今天为中心加载前后各 30 天、LazyRow 滚到窗口首尾时自动再加载 30 天、每侧封顶 maxDays(默认 365)、点击切换选中日期。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要这套日历能力,但上游构建(Compose 1.7.0-alpha03 / Kotlin 2.0.20 / AGP 8.5.2)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示日期格子,却无法证明公共日历引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证日期引擎、kotlinx-datetime 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Compose 1.7.0-alpha03 / Kotlin 2.0.20 / AGP 8.5.2 与鸿蒙工具链(HarmonyOS Kotlin 2.2.21-1.0.0)无法共存于一个 Gradle 构建;上游 wrapper 8.7 也需对齐。 |
| 依赖矩阵 | 引擎依赖 kotlinx-datetime 0.6.1(无 ohos 变体)与 lifecycle-viewmodel-compose 2.8.0(无 ohos 变体且属 AndroidX 生命周期);Compose 六件套全部无法进鸿蒙线。 |
| 纯 UI 库的拆分难题 | 引擎逻辑(ViewModel 状态机)与 Compose 类型(MutableState)编在同一批文件里,不能像 KStore 那样整库直编,必须做源码手术。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交互语义跨不了桥 | LazyRow 触边加载是 Compose 组合期的副作用,桥是同步一来一回的请求-响应。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:日历引擎的去 Compose 化移植与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。日期序列演化完全由移植后的引擎负责(算法与上游逐位一致),ArkTS 只负责调用 Native、解析 JSON 和渲染结果。
1.2 库提供的能力
Demo 提供一个真实的日历会话,对应桥接的全部入口:
- 初始窗口(calendar):以今天为中心加载
[-30, +30]共 61 天,默认选中今天,响应携带每个日期的完整状态; - 触边分页(more):请求过去/未来方向再加载 30 天,双侧各自计数、封顶
maxDays——预算耗尽后再触边不增长; - 选择(select):切换选中日期,全量快照返回;
- 配置(configure):运行时调整
maxDays(1…365)与isBounded,镜像上游setMaxDaysToLoad/setIsBounded; - 重置(reset):回到今天的初始窗口,支持传入
today覆盖用于确定性测试; - 版本握手:
rowKalendarVersion()返回 fork 版本号与 Kotlin 版本,来自真实 Native 调用而非静态文字。
每个日期(days 数组元素)包含:
| 字段 | 作用 |
|---|---|
date | ISO YYYY-MM-DD。 |
dayOfMonth / month / year | 数值拆解,ArkTS 直接摆组件。 |
isoWeekday / weekDayShort | 1=Mon…7=Sun 与三字母短标签(Mon/Tue/…,镜像上游 DateCell 的显示逻辑)。 |
isWeekend / isToday / isPast / isSelected | 三态渲染所需的全部布尔。isPast 严格小于今天(与上游 isBefore 一致,今天属于 future 分支)。 |
引擎侧完整能力(初始窗口、触边分页、每侧预算、选中态、isBounded 存储)全部来自上游 RowKalendarViewModel 的逐行移植,与上游语义逐位一致;JVM 侧同一份引擎源码编译出 class 文件作交叉验证目标,真机功能用例覆盖全部五个 op。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游日期扩展 LocalDateExtensions.kt 原样编译(零改动);RowKalendarViewModel 的状态机逐行移植为 RowKalendarEngine(结构映射见 3.1,日期算法零改动);Compose UI 六文件物理隔离不编译。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游构建文件改名 .upstream. 保留;convention-plugins(模板遗留、未被引用)移出 includeBuild。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 引擎依赖收敛为唯一一个:kotlinx-datetime 换 0.7.1-OH-001 社区线(换线升小版本,API 完全兼容);Compose 与 lifecycle 依赖随 UI 隔离移出构建。 |
| UI 完整 | 单页覆盖全部桥接入口:版本握手、横向日历行、触边自动分页 + 显式按钮、三档 maxDays、isBounded 开关、重置、操作日志。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
RowKalendar的渲染层(RowKalendarcomposable、DateCell)留在composeMain等社区 CMP 鸿蒙线成熟后接回;ArkTS Demo 按 JSON 快照用 ArkUIList重新渲染同一套日期语义。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 datetime ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:源码手术 ── commonMain→composeMain 隔离、引擎 1:1 复刻、日期扩展抽回
第 4 阶段:原生桥接 ── @CName C ABI × 2、MiniJson、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 版本握手、横向日期行、触边分页、配置区、操作日志
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
rowKalendar/ 上游唯一库模块(进 ohos 构建:jvm + ohosArm64 + ohosX64)
rowKalendar/src/commonMain/ 重建:引擎(RowKalendarEngine,移植自上游 ViewModel)+ 日期扩展
rowKalendar/src/composeMain/ 上游 6 个源文件整体迁移(不注册源集,不编译,git mv 历史连续)
convention-plugins/ 上游模板遗留(includeBuild 已移除,留盘不进构建)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
.github/workflows/publish.yml 上游 Maven Central 发布流水线(留盘,本 fork 不发布)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :rowKalendar 与 :example:nativeApp,rootProject.name = "ohos_rowkalendar"。上游的 vanniktech 发布插件、Android library 配置、iOS framework 声明、includeBuild("convention-plugins") 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Homebrew openjdk@21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(上游 8.7 对齐社区钉法;2.2.21 线不支持 Gradle 9.x) |
| kotlinx | kotlinx-datetime 0.7.1-OH-001(ohos 制品线) |
| DevEco product | 6.0.0(20) 兼容(构建机 SDK 为 HarmonyOS 6.0.1 / API 21) |
| DevEco Studio | 6.0.1 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.rowkalendar |
export JAVA_HOME="/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home"
java -version
1.3 源码依赖度盘点
上游 rowKalendar 模块只有 commonMain 一个源集、6 个 Kotlin 文件、零 expect/actual 声明:
| 文件 | 依赖形态 | 处置 |
|---|---|---|
RowKalendar.kt | Composable + LazyRow + viewModel {} | 隔离→ composeMain |
RowKalendarViewModel.kt | AndroidX ViewModel / viewModelScope + Compose MutableState + coroutines | 移植→ 引擎(见 3.1) |
RowKalendarUiModel.kt | 纯数据类 + LocalDate.now() 扩展 | 随 ViewModel 归档→ composeMain(引擎自带快照类型) |
DateCell.kt | Material3 Card + Compose 绘制 | 隔离→ composeMain |
DateCellDefaults.kt | Compose Color / Shape / Material3 主题 | 隔离→ composeMain |
extensions/LocalDateExtensions.kt | 纯 kotlinx-datetime(isBefore / isAfter / now()) | 原样编译(抽回 commonMain) |
expect 声明全库为零——平台差异全部被 kotlinx-datetime 吸收,这是适配面最小的前提。唯一的"手术"发生在 ViewModel:它的三样 Compose 依赖(ViewModel 基类、MutableState 可观察状态、viewModelScope.launch 协程壳)都要去掉,而日期算法一行不能动。
1.4 页面边界
标题区 ROWKALENDAR · OPENHARMONY / 版本握手行(RowKalendar 0.0.3 → 0.0.3-ohos.1 · kotlin 2.2.21-1.0.0)
状态卡 今天 / 选中 / 窗口天数 / 双侧已加载计数 / 上限 / isBounded
日历行 横向 List:weekDayShort + dayOfMonth 日期格,三态配色 + 今天描边 + 周末红字
触边区 触顶/触底自动分页(onReachStart / onReachEnd)+ 显式「加载过去 / 加载未来」按钮
配置区 maxDays 30/90/365 三档 + isBounded Toggle + 重置窗口
日志区 带时间戳的操作日志(最新在前,上限 8 条,失败红字显示 error)
页面启动时先调 rowKalendarVersion() 做握手,再 {"op":"calendar"} 拉初始窗口并滚动定位到今天(镜像上游 LazyRow 的 initialFirstVisibleItemIndex = dates.size / 2 - 1)。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
引擎的日期算力全部站在 kotlinx-datetime 上(LocalDate / DatePeriod / Clock.System.now() / TimeZone.currentSystemDefault()),先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 变体。用 .module 元数据验证:
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-OH-001
variants: ohosArm64Api-published / ohosX64Api-published
/ jvmApi-published / jvmRuntime-published / iosArm64… / androidNative…
三个关键结论:其一,普通 0.6.1 与 0.7.1 在 Maven Central 上都没有 ohos 变体,必须钉 0.7.1-OH-001 后缀才解析得到;其二,OH 制品同时发布 jvm 变体,jvm() 交叉验证可以用同一个版本号,不需要双线隔离;其三,0.6→0.7 对本库用到的 API 零破坏,唯一变化是 kotlinx.datetime.Clock 被标为 deprecated(让位于 kotlin.time.Clock)——日期扩展源码保持与上游一致,文件级 @Suppress("DEPRECATION") 吸收告警。
另一个依赖就没有这么幸运:org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0 没有任何 ohos 制品,它是 ViewModel 文件的根依赖——这正是引擎必须去 Compose 化移植、而不能整库直编的根本原因。
2.2 工具链钉版(为什么是 2.2.21-1.0.0,而不是 KStore 的 2.4.20 线)
社区两条工具链线的选择完全由依赖反推:KStore 的 coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20,必须钉 Kotlin 2.4.20-0.1.0-07;RowKalendar 的 kotlinx-datetime 0.7.1-OH-001 元数据在 2.2 线内,沿用社区已过审的 HarmonyOS Kotlin 2.2.21-1.0.0(cmp_qrose 同款)即可。代价是 Gradle 必须从上游 8.7 升到 8.14.1 对齐社区钉法——2.2.21 线不支持 Gradle 9.x,而上游源码(Kotlin 2.0.20 语法)对 2.2.21 完全向后兼容,源码零改动。
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_rowkalendar"
include(":rowKalendar")
include(":example:nativeApp")
dependencyResolutionManagement 同款双仓置顶 + RepositoriesMode.PREFER_PROJECT。
2.3 编译真实引擎
rowKalendar/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证线:同一份引擎源码产出 JVM class
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api(libs.kotlinx.datetime) // 0.7.1-OH-001,引擎唯一依赖
}
}
}
实测:
./gradlew :rowKalendar:compileKotlinJvm :rowKalendar:compileKotlinOhosArm64 :rowKalendar:compileKotlinOhosX64
# BUILD SUCCESSFUL(三 target 全绿,仅 datetime deprecation 告警,已抑制)
库模块不开 binaries.sharedLib——否则每个库模块各吐一份带 Kotlin/Native 运行时的 so;动态库只在 example/nativeApp 出一次,这是 cmp_qrose 确立的纪律。
2.4 根构建替换与模板遗留处置
上游 convention-plugins 是 Kotlin 官方 multiplatform-library-template 的遗留物(两个 precompiled script plugin,全仓没有任何地方 apply,实际发布走的是模块内 vanniktech 插件)。处置是把它从 includeBuild 移除、目录留盘——它的存在方式只有这一种。上游 .github/workflows/publish.yml(Maven Central 发布)同样留盘不删:本 fork 不发布,保留仅为与上游对照。所有上游构建文件改名保留(settings.upstream.gradle.kts、build.upstream.gradle.kts、gradle/libs.upstream.versions.toml、rowKalendar/build.upstream.gradle.kts),diff 一眼可辨。
第 3 阶段:源码手术(技术难点)
3.1 引擎复刻:ViewModel → Engine 的结构映射
RowKalendarViewModel 的三样 Compose 依赖去掉后,状态机逐行搬进 RowKalendarEngine.kt(commonMain,object 单例——镜像上游"每个 composable 一个 ViewModel 实例"的 ArkTS 主线程单线程调用约定):
| 上游(ViewModel) | 引擎(Engine) | 说明 |
|---|---|---|
uiState: MutableState<RowKalendarUiModel> | dates / selectedDate 私有字段 + state(): RowKalendarState 快照 | 可观察性服务 Compose 重组,桥只要不可变快照 |
loadInitialDates() | reset(today) | ((-30)..30).map { today.plus(DatePeriod(days = it)) } 一字不差;today 参数化便于确定性测试 |
loadDates(isPast) | loadPreviousDates() / loadUpcomingDates() | minOf(DAYS_TO_LOAD, maxDaysToLoad - daysLoaded) 预算逻辑原样,双侧独立计数 |
selectDate(date) | selectDate(date) | 原样 |
setMaxDaysToLoad / setIsBounded | configure(maxDays, bounded) | 原样:只存储,下次加载才生效 |
viewModelScope.launch { … } + isLoading 门闩 | 同步执行 | 上游协程壳为 Compose 重组服务;桥单线程调用无重入,日期序列演化一致。快照保留 isLoading(恒 false)对齐上游 UI 模型 |
两处忠实保留的"上游考古发现",是本次适配最值得记录的语义细节:
isBounded在上游实现中只写不读。setIsBounded赋值后,loadDates里的maxDaysToLoad封顶检查是无条件的——isBounded从未参与判断。上游 README 声称"If bounded, the calendar will not load more dates when reaching the maxDays limit",但 0.0.3 的代码并未实现该分支。本 fork 原样保留(引擎行为与上游逐位一致是验收底线),Demo 把isBounded作为可观察字段透出。修上游逻辑不在适配边界内。- "今天"落在 future 分支。
DateCell的三态判断是isSelected → date.isBefore(today) → else,isBefore严格小于,今天不属于 past。桥的isPast字段同样严格<,ArkTS 侧另用isToday做高亮描边。
3.2 日期扩展抽回
LocalDateExtensions.kt(isBefore / isAfter / LocalDate.Companion.now())零 Compose 依赖,从 composeMain 抽回 commonMain 原样编译。唯一的产物差异是文件头 8 行注释 + @file:Suppress("DEPRECATION"):0.7.1-OH-001 线里 kotlinx.datetime.Clock 让位于 kotlin.time.Clock,源码保持与上游字节级一致,只压掉告警。
3.3 Compose UI 隔离
上游 6 个文件中 5 个(含已归档的 UiModel)git mv 到 src/composeMain。composeMain 不是注册的 source set,Kotlin 编译器视而不见——留它有三个用处:上游 API 对照、git blame 连续(rename 追踪 100%)、社区 CMP 鸿蒙线工具链成熟后原样接回。这与 cmp_qrose 对 *Painter.kt 的处理完全一致。
第 4 阶段:桥协议建模与原生桥接
4.1 JSON 契约
io.github.chouaibmo.rowkalendar/**(引擎 + 日期扩展,移植后原样编译)
↓ RowKalendarEngine 状态机 / kotlinx-datetime 日期算力
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON 全量快照
N-API rowKalendarVersion() / rowKalendarDispatch()
↓
ArkUI Index.ets(单页)
请求与响应:
请求 : {"op":"calendar"}
{"op":"more","direction":"past"|"future"}
{"op":"select","date":"2026-10-20"}
{"op":"configure","maxDays":90,"isBounded":true}
{"op":"reset"[,"today":"2026-01-01"]}
响应 : {"ok":true,"op":"...","today":"...","selectedDate":"...",
"isBounded":true,"maxDaysToLoad":365,
"daysLoadedPast":30,"daysLoadedFuture":30,
"count":61,"selectedIndex":30,
"days":[{...日期格状态...}]}
{"ok":false,"error":"..."}
一个核心设计:每个成功响应都是全量快照。上游的 MutableState 让 Compose 自动订阅增量;桥没有订阅通道,干脆每次把窗口、选中、双侧预算、全部日期格一次给齐,ArkTS 无需本地拼状态,@State 一刷到底。触边分页的交互语义(上游 LazyRow 首尾 item 渲染时触发加载)映射为 ArkTS List 的 onReachStart / onReachEnd 事件 + 两个显式按钮兜底。
4.2 Kotlin/Native 导出函数
@CName("RowKalendarVersion")
fun rowKalendarVersion(): String // 版本握手:name / upstreamVersion / version / kotlin / repo
@CName("RowKalendarDispatch")
fun rowKalendarDispatch(request: String): String // 五个 op 一口进,整体 try/catch
| C ABI 符号 | ArkTS 方法 | 覆盖引擎 |
|---|---|---|
RowKalendarDispatch | rowKalendarDispatch(req) | reset / load* / selectDate / configure 全部入口 |
RowKalendarVersion | rowKalendarVersion() | fork 版本握手(真机版本行的数据来源) |
JSON 解析用桥内手写 MiniJson(扁平 key→string,约 60 行,cmp_qrose 审核模式同款),序列化 buildString 手拼——五个扁平字段不值得把 kotlinx-serialization 运行时与编译器插件链拖进 ohos 线。日期入参 LocalDate.parse(ISO)直接用 datetime 的解析;Kotlin 异常统一转 {"ok":false,"error":…},永不跨 FFI。
4.3 C++ N-API 与 CMake
napi_property_descriptor desc[] = {
{"rowKalendarVersion", nullptr, NapiRowKalendarVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"rowKalendarDispatch", nullptr, NapiRowKalendarDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string(Version 无参跳过)、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosrowkalendar_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosrowkalendar_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
CMake 侧 KN so 作 IMPORTED 库(IMPORTED_NO_SONAME TRUE),-Wl,--no-undefined 链接期暴露缺失符号,$ORIGIN rpath + POST_BUILD 把 KN so 与 libc++_shared.so 拷到 NAPI so 旁。KN so 与 libc++ 由 Gradle 侧 copyKnSoArm64 / copyKnSoX64(finalizedBy 挂在 linkDebugSharedOhos* 后)拷进 entry/libs/<abi>/——先跑 Gradle 再跑 hvigor,顺序错了 CMake 直接 FATAL_ERROR 并提示该跑哪个任务。
第 5 阶段:ArkUI 宿主
5.1 握手与初始加载
aboutToAppear(): void {
this.handshake(); // rowKalendarVersion() → 状态行
this.call('{"op":"calendar"}'); // 初始窗口 61 天
this.scroller.scrollToIndex(this.resp.selectedIndex); // 定位今天
}
private call(request: string): void {
const r = JSON.parse(rowKalendarDispatch(request)) as CalendarResp;
if (!r.ok) { /* 失败红字进日志 */ return; }
this.resp = r; // 全量快照一刷到底
this.log(`✓ ${r.op}: ${r.count} days · selected ${r.selectedDate} …`);
}
5.2 日期格渲染(上游 DateCell 的 ArkTS 孪生)
- 布局:横向
List(space: 6,镜像上游Arrangement.spacedBy(6.dp))+ListItem日期格(圆角 8、内边距 h12/v8,镜像DateCellDefaults.shape与 padding); - 字号:
weekDayShort13fp Bold、dayOfMonth24fp Bold(镜像上游24.sp+FontWeight.Black——ArkTS 无 Black 档,取 Bold); - 三态配色:selected 用
#EADDFF/#21005D(Material3 primaryContainer/onPrimaryContainer),past 与 future 共用#E8DEF8/#49454F(secondaryContainer/onSecondaryContainer,与上游默认配色同构);周末文字红(Demo 增强)、今天加#6750A4描边(Demo 增强,数据仍来自引擎的isToday); - 交互:点击日期格调
select;onReachStart/onReachEnd调more(镜像上游 LazyRow 触边加载),另配「← 加载过去」「加载未来 →」两个显式按钮兜底验证。
5.3 配置区与错误边界
maxDays 三档按钮(30/90/365,当前档反色高亮)与 isBounded Toggle 都走 configure op;「重置窗口」走 reset。ok:false 时错误原文进日志区红字,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosrowkalendar)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + MiniJson(继承 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.rowkalendar、versionName 0.0.3-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/libohosrowkalendar_api.h
├── src/main/cpp/types/librowkalendar_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
./gradlew :rowKalendar:compileKotlinJvm :rowKalendar:compileKotlinOhosArm64 :rowKalendar:compileKotlinOhosX64
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 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.rowkalendar -m entry
四、完整代码对照
4.1 整体架构
io.github.chouaibmo.rowkalendar/**(引擎 + 日期扩展,移植后原样编译)
│ RowKalendarEngine 状态机 / kotlinx-datetime 日期算力
example/nativeapp/Bridge.kt + MiniJson
│ RowKalendarDispatch / RowKalendarVersion
librowkalendar_napi.so
│ rowKalendarDispatch / rowKalendarVersion
Index.ets
│ JSON parse + @State 快照刷新
▼
ArkUI 真机页面(单页滚动)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_rowkalendar、convention-plugins 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 + kotlinx-datetime 0.7.1-OH-001 钉版 |
build.gradle.kts(根) | 版本 0.0.3-ohos.1、group 沿用上游 io.github.chouaibmo、全局禁用 Ohos*Test* 任务 |
rowKalendar/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,api(kotlinx-datetime) |
rowKalendar/src/commonMain/.../RowKalendarEngine.kt | 引擎:上游 ViewModel 逐行移植(唯一新增库源文件) |
rowKalendar/src/commonMain/.../extensions/LocalDateExtensions.kt | 上游原样(唯一 diff:deprecation 抑制注释) |
rowKalendar/src/composeMain/** | 上游 6 文件物理隔离,不编译 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、五 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 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(18 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | reset(today) | 初始窗口:[-30,+30] 天,选中今天(镜像 loadInitialDates) |
| 引擎 | loadPreviousDates() / loadUpcomingDates() | 触边分页:每次 30 天,双侧封顶 maxDaysToLoad |
| 引擎 | selectDate(date) | 切换选中(镜像上游同名) |
| 引擎 | configure(maxDays, bounded) | 边界配置,下次加载生效(镜像 setMaxDaysToLoad / setIsBounded) |
| 引擎 | state() | 不可变全量快照(RowKalendarState) |
| 上游 UI | RowKalendar(isBounded, maxDays, content) | Composable,隔离在 composeMain 待接回 |
| 上游 UI | DateCell 三态 colors/border/elevation | JSON isToday/isPast/isSelected/isWeekend + ArkTS 样式承担 |
| Native | RowKalendarDispatch / RowKalendarVersion | C ABI JSON 导出 |
| N-API | rowKalendarDispatch(req) / rowKalendarVersion() | 向 ArkTS 暴露能力 |
| ArkTS | call(request) | 触发操作并快照刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const resp = JSON.parse(rowKalendarDispatch(
JSON.stringify({ op: 'select', date: '2026-10-20' }))) as CalendarResp;
// resp.ok === true;resp.selectedDate === '2026-10-20'
// resp.days[35].isSelected === true,其余格 isSelected === false
"select" -> {
val date = parseDate(json.str("date")) ?: return err("date must be YYYY-MM-DD")
engine.selectDate(date)
stateJson("select", engine.state()) // 全量快照:61 天 × 每格 11 字段
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:source set 级隔离而不是模块级
kalendar 适配能做模块级隔离(kalendar 视图模块留树不编、kalendar-foundation 引擎模块整编),是因为上游本来就分了两个模块。RowKalendar 是单模块、UI 与引擎编在同一批文件里,模块级隔离无从下手——因此取 cmp_qrose 的 source set 级隔离:Compose 文件整体 git mv 到 composeMain,引擎在 commonMain 重建。git rename 追踪 100%,上游 API 对照与未来接回的路径都保留。
决策 2:引擎移植而不是引擎重写
ViewModel 的日期算法(初始窗口、分页预算、双侧计数)是这条库的全部价值,必须与上游逐位一致。做法是结构映射(MutableState→快照、协程壳→同步体),算法行原样搬运,包括两处"上游考古发现"(isBounded 只写不读、今天落在 future 分支)也忠实保留——适配记录不夹带上游 bug 修复,否则交叉验证失去基准。
决策 3:工具链钉 2.2.21 线、Gradle 升 8.14.1,与 KStore 相反
工具链版本跟着依赖走:KStore 的 coroutines 制品线逼它升到 Kotlin 2.4.20 + Gradle 9.6.1;RowKalendar 的 datetime 0.7.1-OH-001 在 2.2 线内,沿用 cmp_qrose 的 HarmonyOS Kotlin 2.2.21-1.0.0 最稳,代价是 Gradle 从 8.7 升到 8.14.1(2.2 线不支持 9.x)。两条线的经验不能互相套用,唯一通用的规则是"先查制品元数据再钉版本"。
决策 4:全量快照响应而不是增量协议
上游靠 Compose 状态订阅做增量;桥没有订阅通道,五个 op 全部返回完整快照(窗口 + 选中 + 双侧预算 + 全部日期格)。代价是单次响应稍大(61 天 × 11 字段 ≈ 数 KB),换来的是 ArkTS 侧零状态拼接逻辑、零同步 bug 面——对一个 Demo 桥而言这是正确的复杂度取舍。
决策 5:datetime 换线不换 API,交叉验证保留 jvm
0.7.1-OH-001 同时带 jvm 与 ohosArm64/ohosX64 变体(.module 实测),所以 jvm() 交叉验证线得以保留——同一份引擎源码同时产出 JVM class 与 ohos KLIB,加上引擎与上游的映射表(3.1),构成"内核没漂"的结构性证明。kalendar 因 Compose OH 制品无 jvm 变体只能删掉 jvm 靠源码零 diff 论证;RowKalendar 引擎零 Compose 依赖,没有这个约束。
六、测试与验证
6.1 测试环境
本次验证使用 macOS(arm64)、JDK 21(Homebrew openjdk@21)、DevEco Studio 6.0.1 Release(自带 SDK HarmonyOS 6.0.1 / API 21 与 hvigor 6.26.4)、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 交叉验证
上游没有 commonTest(6 个源文件全在 main),没有现成测试可用——这与 KStore(51 项上游测试)不同。处置分两层:
- 结构层:引擎与日期扩展在
commonMain,compileKotlinJvm与compileKotlinOhosArm64/X64从同一份源码出双线产物(三 target 全绿),引擎相对上游的唯一 diff 是第 3.1 节的结构映射表——git diff即是"日期算法零改动"的证明; - 行为层:真机功能用例(6.4)覆盖五个 op 的全部语义分支,包括预算封顶与双侧独立计数。
6.3 HAP 验证
:rowKalendar:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
nm -D: RowKalendarVersion / RowKalendarDispatch / libohosrowkalendar_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake / BuildNativeWithNinja: finished(双 ABI 编出 librowkalendar_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 11.5 MB,内含双 ABI 各三件套(librowkalendar_napi.so / libohosrowkalendar.so / libc++_shared.so);HAP 内 strip 后 KN so arm64 约 4.7 MB、x86_64 约 3.9 MB(debugShared 未 strip 时分别约 13.6 MB / 12.4 MB,含 Kotlin/Native 运行时与 kotlinx-datetime)。
6.4 功能用例(真机实测通过)
- 冷启动握手行显示
RowKalendar 0.0.3 → 0.0.3-ohos.1 · kotlin 2.2.21-1.0.0(真实 Native 返回,非静态文字); - 初始窗口 61 天(
count=61),今天自动选中(selectedIndex=30),日历行滚动定位到今天; - 日期格三态正确:今天紫底深紫字选中态、过去格灰绿、未来格正常,周末星期缩写红字;
- 触底自动追加 30 天(日志
✓ more: 91 days,daysLoadedFuture30→60),触顶同理且daysLoadedPast独立计数; maxDays=30档位下触边不再增长(预算封顶,daysLoaded*停在 30);- 点击任意日期:选中态移动、状态卡同步、日志回执
✓ select: 61 days · selected 2026-10-xx; isBoundedToggle 与reset正常回显,reset 后回到今天窗口并重新定位;- 每步操作日志按时间戳滚动留痕。
6.5 验证结论
日历引擎、kotlinx-datetime 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的日历会话:版本握手、初始窗口、触边分页、预算封顶、选择与重置闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/RowKalendar.git
cd RowKalendar
export JAVA_HOME="/path/to/jdk-21"
./gradlew :rowKalendar:compileKotlinJvm :rowKalendar:compileKotlinOhosArm64 :rowKalendar:compileKotlinOhosX64 # klib + 交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.rowkalendar -m entry
hdc shell hilog | grep RowKalendar
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:rowKalendar真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;kotlinx-datetime在 Maven Central 上的 0.6.1 / 0.7.1 都没有 ohos 变体,必须钉0.7.1-OH-001——resolve 失败时没有更多提示,动工前先 curl.module元数据看变体清单;lifecycle-viewmodel-compose无 ohos 制品是纯 UI 库整库直编的硬边界——引擎必须先去 Compose 化移植,这一刀的验收基准是"日期算法与上游逐位一致";- command-line-tools 的 hvigor 6.21.2 有 fullVersion 解析缺陷:其内置
hos-sdkmanager-common的transferVersionIntoHosVersion()不识别6.0.0(20)这类带括号的形态(实测返回undefined→ 报SDK component missing);改用 DevEco Studio 自带 hvigor 6.26.4(/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw)后正常; DEVECO_SDK_HOME必须指向含default的 SDK 根(.../Contents/sdk),多拼一层/default或指向 shim 目录都会组件扫描失败;compatibleSdkVersion按构建机 SDK 实情选:qrose 模板的5.1.0(18)在 6.0.1 SDK 上触发组件缺失,本机实测6.0.0(20)可建;- ArkTS 的
FontWeight无Black档(上游 ComposeFontWeight.Black映射为Bold);List的子组件必须是ListItem,ForEach不能直接挂裸容器——两条都是编译期报错,好修; - 上游
.gitignore忽略了gradle.properties(CI 动态生成),ohos 构建必须把它变成提交文件(JDK 路径、版本号、konan 开关都在里面); kotlin.native.distribution.downloadFromMaven=true必须写进gradle.properties,受限网络下 konan 预构建与 ohos sysroot 全靠它;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free()。
8.2 已知问题
composeMain里的 Compose UI 未编译(上游RowKalendarcomposable、DateCell定制体系),等社区 CMP 鸿蒙线工具链成熟后接回;接回前 ArkTS 侧的三态配色是 Material3 baseline 的手工映射;isBounded忠实保留上游"只存不判"的现状(见 3.1);若上游后续修复,引擎同步一行即可;- 星期短标签为上游英文三字母(
Mon/Tue/…),中文化可在 ArkTS 侧按isoWeekday映射,协议无需变更; - 选中日期与窗口为进程内状态,杀进程重置(与上游 ViewModel 行为一致),无持久化语义;
- android / ios 目标剥离(源文件留在 composeMain 与 build.upstream 里),发布体系一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
- ohosTest + uitest 自动化截图,把冷启动握手行与 61 天窗口纳入 CI 断言;
- 把
weekDayShort协议扩展 locale 字段(引擎侧出数字、ArkTS 侧查表),替代英文硬标签; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵。
九、总结
9.1 核心难点回顾
上游 rowKalendar(6 文件,5 个锚定 Compose)
→ 验证 datetime ohos 制品线(0.7.1-OH-001,jvm+ohos 双线齐全)
→ 工具链对齐 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1(依赖反推,与 KStore 的 2.4.20 线相反)
→ 根构建替换(cmp_qrose 模式,上游构建文件改名保留)
→ 源码手术:composeMain 隔离 + 引擎 1:1 复刻 + 日期扩展抽回
→ ohosArm64 / ohosX64 KLIB + jvm 交叉验证线
→ C ABI(@CName × 2)+ 全量快照 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + ArkUI List 三态渲染
→ ArkUI 真机单页验收(初始窗口 / 触边分页 / 预算封顶 / 选择 / 重置)
9.2 封装层次
- 引擎层:移植后的
RowKalendarEngine——初始窗口、触边分页、双侧预算、选中态、isBounded 存储,算法与上游逐位一致; - 日期层:上游原版
LocalDateExtensions+kotlinx-datetime 0.7.1-OH-001的LocalDate/DatePeriod/Clock算力; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出全量快照 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、JSON 解析、操作分发、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 纯 UI 库的适配答案是"引擎在哪一刀切开":kalendar 有现成的模块缝、KStore 根本没有 UI,
RowKalendar两者都不是——引擎与 Compose 类型编在同一批文件里,必须自己做 source set 级手术。判据只有一个:引擎与上游逐位一致(结构映射表 + git diff 可证明),UI 隔离件等工具链接回; - 工具链版本跟着依赖走,两条线的经验不能互相套用:KStore 被 coroutines 逼升 Kotlin 2.4.20 + Gradle 9.x,
RowKalendar的 datetime 在 2.2 线内就钉 2.2.21 + Gradle 8.14.1——共同规则是动工前先查制品的.module元数据(变体清单 + stdlib 依赖),而不是沿用上一个库的钉法; - 交互语义的桥上翻译要找对等物:上游
LazyRow触边加载、MutableState增量订阅、initialFirstVisibleItemIndex初始定位,分别映射为 ArkTSList的onReachStart/onReachEnd、全量快照响应、scrollToIndex(selectedIndex)——每一个上游交互行为都有可验证的 ArkTS 对等物,Demo 验收清单才算完整。
9.4 适配成果
当前适配已完成:
RowKalendar 0.0.3日历引擎的双 ABI Kotlin/Native 编译(上游日期算法零改动,引擎移植有结构映射表背书);kotlinx-datetime 0.7.1-OH-001社区制品线在日历类库上的完整落地(jvm + ohosArm64 + ohosX64 三变体解析,保留 JVM 交叉验证线);- 2 个 C ABI 符号和 C++ N-API 包装层(
nm -D验证全局 T 符号,双 ABI); - DevEco Stage ArkUI 单页验收页面(版本握手、横向日历行、触边自动分页、maxDays/isBounded 配置、选择、重置、操作日志);
- HAP 构建(双 ABI 六件套 so,约 11.5MB)、签名、设备安装和真机验证(六类功能用例全部通过);
- 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)