本文记录
kmp-onboarding接入 OpenHarmony 的完整过程,覆盖引导页 UI 库盘点、Compose 依赖隔离决策、语义镜像核心编写、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 引导页真机页面、HAP 构建和真机验收。
kmp-onboarding是 JetBrains Compose Multiplatform 的 App 引导页(onboarding)UI 库:IntroductionScreen用HorizontalPager承载多页引导(官方 Demo 三页),底部一行控件(左槽 Skip/Back、中间 WormDots 虫形指示器、右槽 Next/Done),每页标题/正文/图标配视差动画,页面背景支持渐变与纯色,IntroState提供程序化翻页。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"零依赖内核提取")有本质区别,与 KStore 的"真编译整库引擎"也不同:这是一个纯 UI 库 ,100% 源码(连PageViewModel数据类都持有@Composable槽位与Brush/TextStyle/Dp)粘在androidx.compose上,鸿蒙侧没有可用的 Compose 渲染层,也没有可提取的"零依赖内核"。因此本仓走 "语义镜像"路线 (SpotlightOnboarding / ktoast 同款):把上游的翻页状态机、按钮槽位可见性规则、指示器插值几何、背景解析优先级与视差增益逐条对齐地 重写为纯 Kotlin 核心并原样编译进 OpenHarmony 动态库,Compose API 原样停放到src/composeMain等工具链对齐后接回;ArkTS 演示页通过 C ABI 驱动这份核心,Swiper 按快照翻页、指示器按"库核心计算的几何"绘制------真机上跑的每一条翻页规则、每一个指示器尺寸、每一个视差系数都来自 Kotlin 侧的库代码而不是页面手写。项目地址: AtomGit/oh-tpc/kmp-onboarding.git
上游仓库: yskuem/kmp-onboarding
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
kmp-onboarding 为 Compose Multiplatform 应用提供"App 首启引导"这一件事的完整答案:IntroductionScreen(pages) 一个入口包住整屏引导,PageViewModel(title, body, image, decoration) 建模每一页,IntroState 封装 PagerState 提供带动画语义的程序化翻页(next()/previous() 用 spring(stiffness = 500f),skipToEnd()/animateScrollTo() 用 tween(350)),ControlsRow 的两个 when 分支决定左槽(非末页优先 Skip、否则翻过页后 Back)与右槽(末页 Done 否则 Next),WormDots 按拖动进度对点宽/高/透明度做插值产生"虫形拉伸",PageView 用三段 graphicsLayer 给标题/正文/图标加视差。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要这套引导能力,但上游构建(Compose Multiplatform 1.9.1 / Kotlin 2.2.20 / AGP 8.11.2)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以滑动可以点按钮,却无法证明引导状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态机语义、指示器几何、槽位规则、Native 动态库、C ABI、N-API、ArkUI 引导页和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| Compose 全面依赖 | 上游 8 个源文件 100% 依赖 androidx.compose:状态层用 PagerState/animateScrollToPage/derivedStateOf,几何用 Dp/DpSize/Brush/TextStyle/Shape,绘制用 HorizontalPager/Crossfade/AnimatedVisibility/graphicsLayer,槽位用 @Composable lambda。没有任何"纯 Kotlin 内核"可直接抽取。 |
| 工具链不一致 | 社区鸿蒙线是 HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)+ CMP 1.9.2 制品线;上游 Compose 源集基于 Kotlin 2.2.20 + CMP 1.9.1 的 Jetpack 谱系,ohos 侧编译不了 composeMain 源码集。 |
| AGP 不可共存 | 上游用新版 com.android.kotlin.multiplatform.library 插件(AGP 8.11.2),与鸿蒙工具链无法共存于一个 Gradle 构建,根构建必须整体替换。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,@Composable 槽位(image/titleWidget/footer)是编译期 lambda,跨不过 C ABI。 |
| UI 语义跨不了桥 | IntroState 的 suspend 回调、PagerState 的实时 offsetFraction、WormDots 的逐帧插值都是 Compose 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:上游 UI 语义(状态机 + 几何 + 规则)的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。像素级渲染属于宿主,ArkTS Demo 用 ArkUI 复刻官方三页引导,但每一个行为决策都来自 Kotlin 侧的库核心。
1.2 库提供的能力
Demo 复刻上游官方示例(Welcome / Auto-generated Quizzes / Daily Progress 三页),对应桥接的全部入口:
- 会话配置(configure) :三页内容(第一页
#FFFFF5F0 → #FFFFE2D6垂直渐变、后两页纯色#FFEDEBFA/#FFE4F5F1)与四个按钮开关(showSkip/showNext/showBack/showDone),镜像IntroductionScreen(...)的参数语义; - 程序化翻页(next/prev/skip/goto) :镜像
IntroState的四个方法,边界 clamp 与动画规格(spring500/tween350)随响应下发; - 手势翻页落定(settle) :镜像
PagerState.settledPage,Swiper 手势/动画结束后提交页码; - 拖动进度(progress) :镜像
PagerState.currentPageOffsetFraction,驱动指示器逐帧插值(本机 SDK 降级实录见 3.6 / 8.1); - 槽位可见性(leftSlot/rightSlot) :镜像
ControlsRow的两个when------左槽非末页优先 Skip、currentPage > 0时 Back、否则 64vp 占位;右槽末页 Done 否则 Next; - 指示器几何(dots/dotsWidth) :镜像
WormDots的插值数学,每个点的宽/高/透明度/颜色/圆角由库核心按当前进度计算,ArkUI 只画"库给的几何"; - 背景解析(bg) :镜像
IntroductionScreen的BGSkin优先级------页渐变 > 页纯色 > 全局背景 > 透明; - 页面视差(fx) :镜像
PageView的三段graphicsLayer增益(标题1-|off|、正文1-1.2|off|、图标-40*off平移与0.2下限透明度); - 版本握手(version):返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应是统一的全量快照:
| 字段 | 作用 |
|---|---|
ok / op |
操作是否成功(失败时 error 带原文)与本次操作名。 |
page / pageCount / isFirst / isLast |
当前会话的页位置。 |
move |
仅转移类 op 返回:`{target, anim:"spring500" |
leftSlot / rightSlot |
`"skip" |
bg |
{color:"#AARRGGBB", gradient?:[...]},gradient 非空时为渐变。 |
dots / dotsWidth |
每个点的 {i,w,h,alpha,color,radius,active} 与指示器总宽。 |
fx |
仅 op=fx 返回:该页视差的七个数值。 |
version |
1.0.3-ohos.1。 |
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 语义复用 | 翻页状态机、槽位规则、指示器几何、背景解析、视差增益五组语义逐条对齐上游 Compose 实现,重写为纯 Kotlin 核心;渲染不属于核心,归宿主。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 composeApp / iosApp 示例与 composeMain 停放区留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON 快照,不传递 Kotlin 对象地址;每次 op 返回全量快照,宿主无状态渲染;异常永远不跨 FFI。 |
| UI 完整 | 三页 Swiper + 自绘 WormDots + 四按钮控制行 + 渐变/纯色背景 + 视差 + 版本握手 + 关于面板 + 错误状态行。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
kmp-onboarding的价值在引导语义(翻页规则、指示器数学、槽位逻辑),这些语义可以原样跨平台;像素渲染天然属于宿主,ArkTS Demo 只是把桥暴露的快照画出来。
二、实现路线图
text
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认"纯 UI 库无内核可抽"、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、禁用 ohos 测试
第 3 阶段:核心提炼 ── composeMain 隔离 8 个 Compose 文件、commonMain 逐条镜像五组语义
第 4 阶段:桥协议建模 ── 9-op JSON 契约、全量快照、move 动画规格、错误通道
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、DisposeString 释放、CMake IMPORTED
第 6 阶段:ArkUI 宿主 ── 三页 Swiper、WormDots 绘制、控制行、视差应用、onContentOffset 降级实录
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
text
kmp-onboarding/ 库模块(commonMain 语义核心 + composeMain 停放区 + commonTest 交叉验证)
kmp-onboarding/src/composeMain/ 上游 8 个 Compose 文件原样停放(不注册、不编译、不删除)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 引导页
composeApp/ iosApp/ 上游 Android/iOS 示例,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
doc/ 本文章与效果图
构建拓扑走 cmp_qrose 模式 (根构建即 ohos 构建):settings.gradle.kts 只 include :kmp-onboarding 与 :example:nativeApp,rootProject.name = "ohos_kmp_onboarding"。上游的 android/compose/dokka/vanniktech 发布插件、composeApp、iosApp 全部随根构建替换剥离------本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Temurin jdk-21.0.12) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 与 eazytec 分发对齐) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.onboarding |
| 版本 | 1.0.3-ohos.1(上游 1.0.3 + ohos 后缀) |
bash
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
上游库模块的 8 个源文件(全部在 commonMain,逐文件盘点):
| 文件 | 依赖形态 | 处置 |
|---|---|---|
api/IntroductionScreen.kt |
HorizontalPager/Surface/WindowInsets/semantics,BGSkin 背景解析 |
停放 composeMain,背景解析规则镜像进核心 |
api/Types.kt |
PageViewModel 持有 @Composable 槽位,DotsDecorator/IntroButtonStyle 全是 Dp/Color/Shape |
停放;可序列化子集进 OnboardingPageSpec,默认值进 OnboardingDefaults |
state/IntroState.kt |
封装 PagerState,suspend animateScrollToPage + spring/tween |
停放;翻页规则与动画规格镜像进 OnboardingCore |
ui/ControlsRow.kt |
TextButton/Button/FilledTonalButton + 两个 when 槽位分支 |
停放;槽位规则镜像进 OnboardingCore.leftSlot/rightSlot |
ui/Indicators.kt |
WormDots:animateDpAsState/animateFloatAsState 插值 |
停放;插值数学镜像进 WormDotsGeometry |
ui/PageView.kt |
Crossfade/AnimatedVisibility/graphicsLayer 视差 |
停放;视差增益镜像进 OnboardingCore.pageFx |
internal/Animations.kt |
lerpDp/lerpFloat(Dp 运算) |
停放;浮点版进 WormDotsGeometry 私有函数 |
internal/BackGround.kt |
BGSkin(Color + Brush) |
停放;ARGB 版进 BgSpec |
expect/actual 声明全库为零,androidMain/iosMain 源集为空壳------整个上游库就是一层 Compose 皮,这正是"语义镜像"路线的判定依据。
1.4 页面边界
text
页头小字 kmp-onboarding · <version> · ohosArm64(版本握手,真实 Native 调用)
Swiper 区 三页:中央 emoji 图标(🎓/📝/⭐,160vp)+ 标题(26fp bold)+ 正文(16fp)
背景层 bg.gradient 非空 → 180° 线性渐变;否则 bg.color 纯色,350ms 平滑过渡
控制行 左槽(Skip/Back/64vp 占位)| 居中 WormDots(快照几何逐点绘制)| 右槽(Next/Get Started/占位)
状态行 最近一次操作结果与错误原文
关于面板 半模态:库简介、架构分层、仓库与社区链接、时间戳事件日志
第 2 阶段:目标与依赖打通
2.1 制品验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上 HarmonyOS Kotlin 发行版的可达性:org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21-1.0.0 及其 ohosArm64()/ohosX64() target 支持------qrose / SpotlightOnboarding 等已过审仓库用的正是这套钉法。本库除 Compose 外零第三方依赖 (无 kotlinx、无 serialization),不需要验证任何制品线,工具链钉版直接沿用社区标准:Kotlin 2.2.21-1.0.0、Gradle 8.14.1。上游源码基于 Kotlin 2.2.20,与 2.2.21 同代,核心重写源码零降级成本。
同时确认了一个反方向结论:eazytec 上虽然出现了 org.jetbrains.compose 的 1.9.2-OH.0.1.2-* 制品线,但已过审仓库无一采用(qrose 上游 CMP 1.12 编不过、SpotlightOnboarding 上游 CMP 1.7.3 编不过);本库上游恰好是 CMP 1.9.1、与 1.9.2 只差一个 patch,但 Compose 渲染层在 HAP 内的入口、skiko 与设备能力对齐均无已过审先例------按"路线选择必须基于当天的已验证组合"的社区纪律,本仓同样不启用该制品线,UI 语义走镜像、渲染归宿主。
2.2 根构建替换(cmp_qrose 模式)
settings.gradle.kts 把 eazytec 仓库写在最前:
kotlin
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_kmp_onboarding"
include(":kmp-onboarding")
include(":example:nativeApp")
gradle.properties 钉住 JDK 与三个 Kotlin/Native 开关(ignoreDisabledTargets / distribution.downloadFromMaven / allowUsingOpenSourceCompiler),version=1.0.3-ohos.1;根 build.gradle.kts 全局禁用 ohos 测试任务(HarmonyOS Kotlin 的 ohos 测试目标不可用):
kotlin
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
gradle/libs.versions.toml 收敛为 kotlin = "2.2.21-1.0.0" 单插件。上游 gradle.properties 里的 org.gradle.configuration-cache、android.* 等条目随重写清除。
2.3 编译语义核心
kmp-onboarding/build.gradle.kts 收敛为纯 KMP 模块:
kotlin
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies { implementation(kotlin("test")) }
}
}
实测:
bash
./gradlew :kmp-onboarding:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :kmp-onboarding:compileKotlinOhosArm64 :kmp-onboarding:compileKotlinOhosX64 # BUILD SUCCESSFUL
第 3 阶段:核心提炼(语义镜像)
3.1 composeMain 隔离
8 个上游 Compose 文件 git mv 到 kmp-onboarding/src/composeMain/(包路径原样),不注册进任何 target:社区 Compose 工具链(CMP 1.9.2 / Kotlin 2.2.21)编不了上游(CMP 1.9.1 / Kotlin 2.2.20)源集;保留不删,等制品线与上游对齐后原样接回。库模块 build.gradle.kts 尾注释明确"不要加回来、不要 sync"。
3.2 commonMain 四文件逐条镜像
核心包 io.github.yskuem.onboarding.core,每个 KDoc 注明规则的上游出处:
OnboardingCore ------ 五组语义中的四组:
kotlin
fun next(): PageMove { // IntroState.next(): (page+1).coerceAtMost(last) + spring(500f)
val target = (currentPage + 1).coerceAtMost(_pages.size - 1)
return moveTo(target, spring = true)
}
fun leftSlot(page: Int = currentPage): ControlSlot { // ControlsRow 左槽 when
val isLast = page == _pages.size - 1
return when {
!isLast && showSkipButton && skipContent -> ControlSlot.SKIP
showBackButton && page > 0 && backContent -> ControlSlot.BACK
else -> ControlSlot.NONE // 上游渲染 Spacer(64.dp)
}
}
fun background(page: Int = currentPage): BgSpec { // IntroductionScreen BGSkin 优先级
return when {
spec.gradientArgb.isNotEmpty() -> BgSpec(TRANSPARENT, spec.gradientArgb)
spec.pageColorArgb != null -> BgSpec(spec.pageColorArgb)
global != null -> BgSpec(global)
else -> BgSpec() // 透明
}
}
fun pageFx(page: Int, fraction: Float = currentOffsetFraction): PageFx {
val offset = (currentPage - page) + fraction // PageView: (currentPage - index) + offsetFraction
val d = abs(offset)
return PageFx(
titleAlpha = (1f - d).coerceIn(0f, 1f), // 标题淡入
bodyAlpha = (1f - 1.2f * d).coerceIn(0f, 1f), // 正文更快淡出(1.2 增益)
imageTranslateX = -40f * offset, // 图标反向平移(视差)
imageAlpha = (1f - 0.5f * d).coerceIn(0.2f, 1f), // 图标半速淡出,下限 0.2
imageScale = 1f - 0.05f * d, // 轻微缩小
titleTranslateY = 8f * d, bodyTranslateY = 10f * d, // 垂直漂移
)
}
WormDotsGeometry ------ 指示器插值数学(上游 Indicators.kt 逐行对应):
kotlin
val dir = if (progress >= 0f) 1 else -1 // 拖动方向
val target = (current + dir).coerceIn(0, count - 1) // 行进方向的相邻页
val frac = abs(progress).coerceIn(0f, 1f)
val width = when {
isCurrent -> lerp(m.activeWidth, m.size, frac) // 当前点从激活宽度缩回
isTarget -> lerp(m.size, m.activeWidth, frac) // 目标点从基础宽度拉伸
else -> m.size
}
val alpha = when {
isCurrent -> lerp(1f, m.inactiveAlpha, frac) // 1.0 → 0.55
isTarget -> lerp(m.inactiveAlpha, 1f, frac) // 0.55 → 1.0
else -> m.inactiveAlpha
}
// color = (active ? activeColor : color).copy(alpha = alpha)
// radius: 激活态 min(RoundedCornerShape(12), height/2),CircleShape = size/2
"虫形拉伸"完全由这些数值产生,宿主不做任何动画计算。OnboardingDefaults 把 DotsDecorator/DotsContainerStyle/IntroButtonStyle/PageDecoration 的全部默认值 ARGB 化(点 8vp、激活 22×8vp、间距 8vp、0x33000000/0xFF6666FF、基线透明度 0.55、按钮圆角 16vp、槽位占位 64vp、标题 26fp、正文 16fp)。OnboardingPageSpec 是 PageViewModel + PageDecoration 的可序列化子集(@Composable 槽位无桥接等价物,由宿主自绘)。
第 4 阶段:桥协议建模
4.1 JSON 契约
text
io.github.yskuem.onboarding.core/**(语义核心,逐条镜像上游)
↓ OnboardingCore / WormDotsGeometry / OnboardingDefaults
Bridge.kt(@CName × 2,9-op JSON 进出)
↓ UTF-8 JSON 全量快照
N-API onboardingVersion() / onboardingDispatch()
↓
ArkUI Index.ets(三页 Swiper 引导)
请求 op 一览(缺省 snapshot):
| op | 载荷 | 镜像的上游 API |
|---|---|---|
configure |
pages 对象数组 + show* 四开关 + globalBackground | IntroductionScreen(...) 参数 |
next / prev |
--- | IntroState.next()/previous()(spring500) |
skip / goto |
index | skipToEnd()/animateScrollTo()(tween350) |
settle |
index | PagerState.settledPage 提交 |
progress |
value(可负) | PagerState.currentPageOffsetFraction |
fx |
index | PageView 的 graphicsLayer 视差 |
snapshot |
--- | 全量状态查询 |
三个设计要点:其一,每次 op 返回全量快照 (页位置 + move + 槽位 + 背景 + 指示器 + 总宽),宿主拿到快照即可整页重绘,不维护第二份状态------UI 库桥的正确形态是"库给几何与决策,宿主只画";其二,move 携带动画规格 (spring500/tween350),宿主把上游 spring(stiffness = 500f) 与 tween(350) 的语义映射到 Swiper 动画时长;其三,错误统一通道 ,configure 的对象数组解析、非法 op、越界索引全部走 ok:false。
4.2 MiniJson 的数组扩展
qrose/SpotlightOnboarding 的 MiniJson 是扁平 map;本协议 configure 需要 "pages":[{...},{...}] 一层对象数组。处置是在手写状态机风格内扩展:顶层按括号深度 0 切分键值对,新增 objArray(对象数组)与 strArray(gradient 字符串数组)两个访问器,仍然零依赖------不引入 kotlinx.serialization,避免桥模块多拉一条制品线。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
text
ArkTS
│ JSON string
▼
C++ N-API entry(libonboarding_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosonboarding.so)
│ OnboardingDispatch / OnboardingVersion
▼
UTF-8 JSON + DisposeString
@Composable 槽位与 PagerState 对象都跨不过 C 签名,只传扁平 JSON。
5.2 Kotlin/Native 导出函数
kotlin
@CName("OnboardingDispatch")
fun onboardingDispatch(request: String): String // 9 op 一口进,全量快照一口出
@CName("OnboardingVersion")
fun onboardingVersion(): String = "1.0.3-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖语义 |
|---|---|---|
OnboardingDispatch |
onboardingDispatch(req) |
会话配置 + 翻页 + 槽位 + 指示器几何 + 背景 + 视差 |
OnboardingVersion |
onboardingVersion() |
fork 版本握手(真机版本行的数据来源) |
5.3 C++ N-API 与 CMake
napi_init.cpp 注册 nm_modname = "onboarding_napi",导出 onboardingDispatch/onboardingVersion;字符串读取先量长再 std::vector 拷贝(规避新版头文件把缓冲区标非 const);KN 返回串必须经 libohosonboarding_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosonboarding_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号)。
cmake
set(KN_SO ${KN_SO_DIR}/libohosonboarding.so)
if(NOT EXISTS ${KN_SO})
message(FATAL_ERROR "Missing ${KN_SO}. Run :example:nativeApp:linkDebugSharedOhosArm64 / OhosX64 first.")
endif()
add_library(ohosonboarding SHARED IMPORTED)
set_target_properties(ohosonboarding PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE) # 防 Windows 绝对路径进 RUNPATH
add_library(onboarding_napi SHARED napi_init.cpp)
target_link_libraries(onboarding_napi PUBLIC
libace_napi.z.so libhilog_ndk.z.so ohosonboarding)
$ORIGIN rpath + POST_BUILD 三件套拷贝(NAPI so、KN so、libc++)保证 dlopen 互相可见。example/nativeApp/build.gradle.kts 双 ABI binaries.sharedLib { baseName = "ohosonboarding" },link 任务 finalizedBy Copy 任务把 so 与 ~/.konan 对应 triple 的 libc++_shared.so 灌进 harmonyApp/entry/libs/{arm64-v8a,x86_64}。
typescript
export const onboardingDispatch: (request: string) => string;
export const onboardingVersion: () => string;
第 6 阶段:ArkUI 宿主
6.1 会话初始化
typescript
aboutToAppear(): void {
this.version = onboardingVersion(); // 版本握手:真实 Native 调用
this.opConfigure(); // 三页内容 + 四开关,逐字复刻上游官方 Demo
}
6.2 页面交互
- Swiper 复刻 HorizontalPager :
onChange → settle提交落定页;move响应驱动swiperController.changeIndex(target),tween350→ 350ms、spring500→ 500ms 等时长近似;程序驱动期间抑制手势上报并给起止两页预取fx; - WormDots 自绘 :Row 内逐点
width/height/radius/color(#AARRGGBB)渲染,间距 8vp,总宽来自dotsWidth居中;点击指示器可goto对应页; - 控制行三槽 :左槽按
leftSlot渲染 Skip(TextButton 风格)/Back/64vp 占位;右槽按rightSlot渲染 Get Started(#FF6666FF底白字)或 Next(FilledTonal 近似:#3366666FF底#FF6666FF字);行 padding 20vp 对齐上游 Demo 的DotsContainerStyle(contentPadding = 20.dp); - 视差逐字段应用 :标题
opacity=titleAlpha + translateY、正文同理、中央图标translateX + opacity + scale,对应上游三段graphicsLayer; - 背景跟随快照:gradient 非空 → 180° 线性渐变,否则纯色,350ms 补间过渡;
- 图标替代 :Material Icons(
Icons.Outlined.School/Quiz/AutoAwesome)在 ArkTS 无矢量资源等价物,以 emoji 🎓📝⭐ 替代,160vp 图标区。
6.3 onContentOffset 降级实录
开发期用 Swiper 的 onContentOffset 把拖动偏移换算成 currentPageOffsetFraction 实时上报 progress(带 1%/16ms 双重节流),指示器逐帧插值。真机编译报 ArkTS ERROR 10505001: Property 'onContentOffset' does not exist on type 'SwiperAttribute'------该接口需 API 15+,本机 SDK 未达。处置按预留注释降级:删除该事件与换算方法,settle-only 更新(手势拖动中指示器不逐帧插值,落定后快照一次到位),按钮驱动路径不受影响。
四、完整代码对照
4.1 整体架构
text
io.github.yskuem.onboarding.core/**(语义核心:逐条镜像上游 Compose 实现)
│ OnboardingCore(翻页/槽位/背景/视差) + WormDotsGeometry + OnboardingDefaults
example/nativeApp Bridge.kt + MiniJson
│ OnboardingDispatch / OnboardingVersion
libonboarding_napi.so
│ onboardingDispatch / onboardingVersion
Index.ets
│ JSON parse + @State 快照应用
▼
ArkUI 真机引导页(三页 Swiper + WormDots + 控制行)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts |
极简 ohos 构建、eazytec Maven 置前、ohos_kmp_onboarding、composeApp/iosApp 移出 |
gradle.properties |
JDK 21、version=1.0.3-ohos.1、三个 Kotlin/Native 开关 |
gradle/libs.versions.toml |
Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根) |
版本后缀、全局禁用 Ohos*Test* 任务 |
kmp-onboarding/build.gradle.kts |
纯 KMP:jvm() + ohosArm64() + ohosX64(),无 sharedLib |
kmp-onboarding/src/composeMain/** |
上游 8 个 Compose 文件原样停放(不编译) |
kmp-onboarding/src/commonMain/**/core/* |
语义核心四文件(全新,逐条镜像) |
kmp-onboarding/src/commonTest/** |
15 项 JVM 交叉验证(全新,钉住镜像规则) |
example/nativeApp/build.gradle.kts |
双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
example/nativeApp/.../Bridge.kt |
2 个 C ABI、9-op 分发、MiniJson(含数组扩展)、错误通道 |
example/harmonyApp/.../cpp/napi_init.cpp |
N-API 导出、参数检查、DisposeString |
example/harmonyApp/.../cpp/CMakeLists.txt |
IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
example/harmonyApp/.../ets/pages/Index.ets |
ArkUI 引导页(约 770 行,含降级实录) |
README.OpenHarmony_CN.md |
鸿蒙适配主文档(15 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 核心 | OnboardingCore.next()/previous() |
边界 clamp + spring(stiffness=500f) 语义 |
| 核心 | OnboardingCore.skipToEnd()/scrollTo() |
tween(350) 语义,非法索引 clamp |
| 核心 | OnboardingCore.leftSlot()/rightSlot() |
ControlsRow 两个 when 的镜像 |
| 核心 | OnboardingCore.background() |
BGSkin 优先级:页渐变 > 页纯色 > 全局 > 透明 |
| 核心 | OnboardingCore.pageFx() |
PageView 三段 graphicsLayer 增益 |
| 核心 | WormDotsGeometry.dots()/totalWidth() |
WormDots 插值数学与指示器总宽 |
| 核心 | OnboardingDefaults |
DotsDecorator 等默认值 ARGB 化 |
| Native | OnboardingDispatch / OnboardingVersion |
C ABI JSON 导出 |
| N-API | onboardingDispatch(req) / onboardingVersion() |
向 ArkTS 暴露能力 |
| ArkTS | call(req) |
触发操作并应用全量快照 |
4.4 ArkTS 与 Kotlin 的边界
typescript
const s = JSON.parse(onboardingDispatch(JSON.stringify({ op: 'next' })));
// s.ok === true; s.move = {target: 1, anim: 'spring500'}
// s.leftSlot = 'skip'; s.rightSlot = 'next'; s.dots = [...]; s.bg = {...}
this.applyMove(s.move); // swiperController.changeIndex(1), 500ms
this.applySnapshot(s); // @State: 槽位/指示器/背景/状态行
kotlin
"next" -> move = state.next() // OnboardingCore: clamp + spring 语义
"prev" -> move = state.previous()
...
snapshot(op, move, fx) // 全量快照 JSON 拼装
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象、@Composable 槽位或未校验的动态结构。
五、关键决策说明
决策 1:语义镜像而不是等 Compose 工具链,也不是整库重写
上游是纯 UI 库:价值全在 Compose 渲染 + 行为语义。eazytec 上虽出现 CMP 1.9.2-OH 制品线,但无已过审的"HAP 内跑 Compose"先例,渲染层入口/skiko/设备能力对齐都是未知数;反过来把整库 ArkTS 重写则等于抛弃上游------状态机与几何会随上游演进失联。正确切面是语义:翻页规则、槽位逻辑、指示器数学、背景优先级、视差增益,这些是库的" constitution",跨平台无损;渲染天然属于宿主。核心每个函数的 KDoc 标注上游出处,上游升级时可逐条核对。
决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1(社区标准线)
本库零第三方依赖,无 kotlinx 制品线牵制,直接沿用 qrose / SpotlightOnboarding 已过审的钉法;上游 Kotlin 2.2.20 与 2.2.21 同代,核心源码零降级。与 KStore 的教训互为印证:工具链版本跟着依赖走------KStore 因 coroutines 制品线被迫升 2.4.20 线留 Gradle 9.x,本库无此约束,选最保守的已验证组合。
决策 3:composeMain 停放不删除
8 个 Compose 文件原样 git mv 进 src/composeMain/(git 识别为 100% rename),不注册任何 target。删除会丢失"上游原件"的对照基准,镜像核心的每条规则都要能对回上游源码;保留的成本为零(不参与编译),等社区 CMP ohos 制品线与上游谱系对齐后原样接回。
决策 4:sharedLib 只在 example,库模块只出 KLIB
库模块声明 target 但不开 binaries.sharedLib:否则每个模块各带一份 Kotlin/Native 运行时,符号与体积失控(qrose 审核模式明文规则)。libohosonboarding.so 只由 example/nativeApp 链接产出,桥是唯一的 so 出口。
决策 5:全量快照桥 + move 携带动画规格
UI 库的桥和存储库不同:KStore 每次只回操作结果,本库每次回整页渲染所需的全部决策 (位置/槽位/指示器/背景/动画规格)。宿主因此完全无状态------拿快照画页面,丢快照丢状态,不存在两侧状态漂移;move.anim 把上游动画语义(spring500/tween350)显式下发给宿主执行,而不是让宿主猜。
决策 6:交叉验证测试全新编写(与 KStore 沿用上游测试相反)
KStore 真编译上游引擎,跑的是上游原测试文件;本库的核心是新写的镜像 ,没有现成测试可跑。处置是 15 项新测试逐条钉住镜像规则------每个断言注释对应上游哪个文件哪个行为(next() 的 clamp、左槽 Skip 优先于 Back、背景四级优先级、WormDots 插值端点、0x8C/0xC5 alpha 通道编码、视差增益与 0.2 下限),JVM 与 ohos 跑同一份 commonMain,jvmTest 全绿即"镜像没漂"。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin jdk-21.0.12)、DevEco Studio 5.1.0 Release、鸿蒙真机(arm64)。
6.2 JVM 交叉验证
bash
./gradlew :kmp-onboarding:jvmTest
15 项用例全部通过:
| 测试组 | 用例数 | 钉住什么 |
|---|---|---|
| 翻页状态机 | 5 | next/previous 边界 clamp、spring/tween 语义标记、scrollTo 非法索引、settle 同步与分数清零 |
| 槽位规则 | 2 | 左槽 Skip 优先 → Back(currentPage > 0)→ 占位;右槽末页 Done ↔ Next 切换、开关关闭退化 |
| 背景解析 | 1 | 页渐变 > 页纯色 > 全局 > 透明四级优先级 |
| 指示器几何 | 4 | frac 0/1 插值端点交接、负进度反向目标、alpha 通道编码(0xC5 = 0.775×255)、总宽含间距 |
| 视差 | 3 | 居中页全显、上游增益逐字段(1.2/40/0.5/0.05/8/10)、图标透明度 0.2 下限 |
6.3 链路与 HAP 验证
text
:kmp-onboarding:compileKotlinJvm / compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:kmp-onboarding:jvmTest: 15 tests completed, 0 failed
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64 + copyKnSo*: BUILD SUCCESSFUL
符号层: libohosonboarding.so 动态符号含 OnboardingDispatch / OnboardingVersion / libohosonboarding_symbols
CompileArkTS: finished(onContentOffset 降级后)
PackageHap / 签名 / hdc install: 真机安装启动成功
产物(debugShared 未 strip):libohosonboarding.so arm64 约 3.7MB、x86_64 约 3.4MB(含 Kotlin/Native 运行时与语义核心),随 libc++_shared.so(1.85MB/1.73MB)双 ABI 回填 entry/libs/。
6.4 功能用例
- 启动后页头出现
kmp-onboarding · 1.0.3-ohos.1 · ohosArm64(来自onboardingVersion()真实 Native 调用); - 三页 Swiper 手势滑动,落定后
settle快照更新页码/槽位/指示器/背景; - 第一页背景为
#FFFFF5F0 → #FFFFE2D6垂直渐变,第二/三页纯色#FFEDEBFA/#FFE4F5F1,翻页时 350ms 过渡; - 非末页右槽为 Next(FilledTonal 风格),末页切换为 Get Started(
#FF6666FF底白字); - 第一页左槽为 Skip,点按直达末页(tween350 语义);第二页起 Skip 让位给 Back;
- 指示器三 dots 按快照几何渲染,当前页激活(22×8vp 胶囊
#FF6666FF),其余 8vp 圆点基线透明度 0.55;点击指示器goto对应页; - 按钮驱动翻页(Next/Back/Skip)带 move 动画规格,Swiper 按目标页动画滚动;
- 视差随翻页应用:离中心页标题/正文透明度下降、图标反向平移并轻微缩小;
- 末页点 Get Started 弹出完成对话框(对应上游
onDone → onFinished()),状态行留痕; - 关于面板展示架构分层、仓库与社区链接、时间戳事件日志。
6.5 验证结论
语义核心(翻页状态机、槽位规则、指示器几何、背景解析、视差增益)、Native 动态库、C ABI、N-API、ArkUI 引导页和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装进入前台,页面展示了真实的引导会话:版本握手、三页翻页、槽位切换、WormDots 几何与背景过渡全部由 Kotlin 侧库核心驱动(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
bash
git clone https://atomgit.com/oh-tpc/kmp-onboarding.git
cd kmp-onboarding
export JAVA_HOME="/path/to/jdk-21"
./gradlew :kmp-onboarding:compileKotlinOhosArm64 :kmp-onboarding:compileKotlinOhosX64 # klib
./gradlew :kmp-onboarding:jvmTest # 15 项交叉验证
./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.onboarding -m entry
hdc shell hilog | grep OnboardingDemo
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:kmp-onboarding真正编译成ohosArm64/ohosX64,且语义核心镜像自上游而不是页面手写; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 可变属性的 smart cast :
background()里globalBackgroundArgb != null -> BgSpec(colorArgb = globalBackgroundArgb)编译失败------var构造属性判空后不能智能转换,先val global = globalBackgroundArgb再判空使用; - gradlew 与 wrapper jar 必须配套更换 :本仓上游
gradlew被改成java -jar启动方式,与标准 wrapper jar(无 Main-Class manifest)不兼容,报"没有主清单属性"------只换 jar 不换脚本必然失败; - IEEE 负零陷阱 :视差
imageTranslateX = -40f * offset在 offset=0 时产生-0.0,assertEquals(0f, -0.0f)失败(Float.equals对负零判不等)------浮点断言要么用abs()归一,要么用带容差的重载;上游 Compose 同样产生 -0.0,渲染无差异; - 测试断言必须按上游语义核对而不是直觉:
previous()从第 1 页的目标是第 0 页(不是停在 1),pageFx对非当前页的 offset 是(currentPage - page) + fraction; onContentOffset是 API 15+ 接口 :真机编译报10505001 Property 'onContentOffset' does not exist on type 'SwiperAttribute'------开发期就应在代码里预留降级注释(本仓按注释一键降级 settle-only),SDK 相关 API 先查 compatibleSdkVersion 的可用矩阵;- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - CMake 引入 KN so 必须
IMPORTED_NO_SONAME TRUE+$ORIGINrpath,否则 Windows 下链接会把绝对路径写进 RUNPATH,装机即dlopen失败; - 根构建禁用
Ohos*Test*任务 +kotlin.native.ignoreDisabledTargets=true,否则 ohos 测试目标配置期报错; - 签名由 DevEco 自动生成写回
build-profile.json5,提交前保护(git update-index --skip-worktree或还原该文件),证书绝不进入 AtomGit;仓库内signingConfigs保持占位。
8.2 已知问题
- 上游 Compose UI(
IntroductionScreen/PageViewModel等)不在鸿蒙编译范围,src/composeMain停放待社区 CMP ohos 制品线与上游谱系对齐后接回; - Material Icons 以 emoji 替代(ArkTS 无 Compose 矢量图标资源);
spring(stiffness = 500f)以 500ms 等时长近似(Swiper 不支持自定义弹簧曲线);渐变背景切换不做逐色插值(仅纯色路径补间); onContentOffset降级后手势拖动中指示器不逐帧插值、视差不实时跟随,落定后快照一次到位(升级 SDK 后可恢复);rawPages/globalHeader/globalFooter/ 容器外观(DotsContainerStyle的 shadowElevation 等)是纯 Compose 槽位,无桥接等价物,Demo 未覆盖;- 上游
onSkip/onDone的 suspend 回调语义 = ArkTS 按钮回调,不跨协程;桥内会话为单例(Demo 只持有一个引导会话,同上游示例只持有一个rememberIntroState); - ohos 测试目标禁用(工具链不支持),验证走 JVM
jvmTest+ 真机 Demo; - android / ios 目标剥离(源文件留在树里),发布、dokka 一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
- SDK 升级后恢复
onContentOffset实时上报,指示器/视差逐帧插值(字段与节流逻辑已保留); - 跟踪社区 CMP ohos 制品线(
1.9.2-OH.*),与上游 CMP 谱系对齐后评估把composeMain原样接回,桥层退役; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查与 HAP 构建;
- 补充 uitest 自动化截图与多设备验收矩阵。
九、总结
9.1 核心难点回顾
text
上游 kmp-onboarding(8 文件 100% Compose,无内核可抽)
→ 判定路线:语义镜像(SpotlightOnboarding / ktoast 同款),渲染归宿主
→ 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 / Gradle 8.14.1)
→ composeMain 停放 8 文件 + commonMain 镜像五组语义(翻页/槽位/几何/背景/视差)
→ ohosArm64 / ohosX64 KLIB + jvm() 交叉验证
→ C ABI(@CName × 2)+ 9-op JSON 全量快照(move 携带动画规格)
→ C++ N-API(DisposeString 纪律)+ CMake IMPORTED 双 ABI
→ ArkTS 三页引导 + WormDots 自绘 + onContentOffset 降级实录
→ 15 项 jvmTest 全绿 + HAP 签名真机验收
9.2 封装层次
- 核心层:
OnboardingCore(翻页状态机/槽位规则/背景解析/视差增益)+WormDotsGeometry(指示器插值)+OnboardingDefaults(上游默认值 ARGB 化); - Native 层:双 ABI 动态库并通过 2 个 C ABI 输出 JSON 快照;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:会话配置、快照应用、Swiper/指示器/控制行渲染、错误状态行与日志;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 库的适配面是语义不是像素:翻页规则、槽位逻辑、指示器数学、背景优先级、视差增益是引导库的"constitution",可以逐条镜像跨平台;像素渲染天然属于宿主。切面切对了,ArkTS Demo 只需 770 行就能完整复刻上游行为,且每条行为可追溯到 Kotlin 侧库代码;
- 全量快照是 UI 库桥的正确形态 :每次 op 返回渲染所需的全部决策(含动画规格),宿主无状态化,从根上消灭两侧状态漂移;
move.anim把上游动画语义显式下发,而不是让宿主自行猜时长; - 交叉验证的形态跟着路线走 :真编译路线(KStore)跑上游原测试零改动;镜像路线(本仓)必须新写测试钉住镜像规则------断言注释逐一对应上游文件与行为,测试即镜像契约的文档。
9.4 适配成果
当前适配已完成:
- kmp-onboarding
1.0.3引导语义的五组规则(翻页状态机、槽位可见性、WormDots 插值几何、背景解析、视差增益)逐条镜像为纯 Kotlin 核心并完成ohosArm64/ohosX64双 ABI Kotlin/Native 编译(上游 Compose 源码零改动停放composeMain); - 2 个 C ABI 符号和 C++ N-API 包装层(符号层验证通过);
- DevEco Stage ArkUI 三页引导验收页面(版本握手、三页翻页、槽位切换、指示器几何、背景过渡、视差、关于面板、错误状态行);
- 15 项 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。