开源鸿蒙平台 CMP 三方库「compose-shelf」适配全流程
本文记录
compose-shelf接入 OpenHarmony 的完整过程,覆盖 UI 组件库的"逻辑半边 / 渲染半边"拆分、社区 Compose ohos 制品线选型、整库零源码手术编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
compose-shelf是 Compose Multiplatform 的 Material 3 Side Sheet(侧边抽屉)组件库:Google 只给 Views 体系实现了SideSheetBehavior/SideSheetDialog(Material 1.8.0),Compose 至今没有官方实现,本库补位——ModalSideSheet(模态 + scrim + 滑动关闭)、StandardSideSheet(内联无 scrim)、AdaptiveSheet(≥600dp 侧板 / 窄屏回退底板)三个变体,零 material3 依赖(M3 色值硬编码),状态由SideSheetState(真实AnchoredDraggableState驱动,125.dp 速度阈值 + 半距离位置阈值 + spring settle)承载。本次适配与存储类库(KStore)路线的关键差异在于:这是该系列第一个"交互手势型" UI 组件库——OHOS 尚无 Compose 组合宿主,composables 编译进.so但不能渲染;适配把"逻辑半边"(真实SideSheetState状态机:锚点、弹簧、阈值、拖拽增量)原样编入 Native 动态库,ArkUI 页面按引擎逐帧供出的 offset 摆放面板——偏移与弹性动画全部在.so内计算,ArkTS 只重绘,不重算。
项目地址: AtomGit/oh-tpc/compose-shelf
上游仓库: aldefy/compose-shelf
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-shelf 为 Compose Multiplatform 应用提供 M3 规范的侧边抽屉:28dp 内缘圆角(shapeAppearanceCornerLarge)、Level 1 海拔(1dp)、32% 黑 scrim、可拖拽把手(4dp × 32dp 药丸)、速度感知的滑动关闭、RTL Start/End 边缘语义、无障碍(paneTitle / dismiss)。库本体只有 10 个 Kotlin 文件 785 行,全部在 commonMain,无 expect/actual、无平台源码集——这是最理想的可移植形态。但上游构建(Compose Multiplatform 1.8.0 / Kotlin 2.1.20 / AGP 8.9.0 / Gradle 8.12,android / jvm / wasmJs / iOS 六 target)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面能滑动,却无法证明库的公共 API 在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:真实状态引擎(锚点计算、spring/decay 动画、速度阈值判定)、Compose 依赖线、Native 动态库、C ABI、N-API、ArkUI 手势页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 依赖矩阵 | 库的 commonMain 站在 compose runtime / foundation / animation / ui 上(另经 snapshotFlow 用到 coroutines),每一件都要有 ohos 变体才能解析。 |
| 无组合宿主 | 社区 Compose ohos 线能编 @Composable,但 OHOS 上没有组合宿主——三个 Sheet composable 编译进 .so 后无人调用渲染。 |
| 手势语义跨不了桥 | 拖拽增量、松手 fling 速度是 ArkTS 手势事件;spring settle 是 Kotlin suspend 机器,C ABI 只能同步一来一回。 |
| 废弃 API 兼容性 | SideSheetState 用 foundation 1.8 已废弃的 AnchoredDraggableState 构造器(源码自带 @Suppress("DEPRECATION")),在社区 1.12 线上是 Warning 还是 Error、乃至是否还在,必须实测。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:整库 commonMain 的 ohos 编译(零源码手术)、根构建替换(cmp_qrose 模式)、状态机 headless 桥接层和 ArkUI 手势页面。锚点、动画、阈值完全由上游原版实现负责,ArkTS 只负责手势采样、调用 Native、按帧定位和绘制。
1.2 库提供的能力
Demo 提供一个真实的侧边抽屉会话,对应桥接的全部入口:
- 版本握手:
shelfVersion()返回 fork 版本号1.0.0-alpha01-ohos.1,来自真实 Native 调用而非静态文字; - 打开 / 关闭:
sheet.show/sheet.hide触发真实SideSheetState.expand()/hide()弹簧动画,页面按sheet.frame逐帧摆放面板; - 跟手拖拽:Pan 手势每个增量经
sheet.drag直达真实AnchoredDraggableState.dispatchRawDelta——与上游anchoredDraggablemodifier 同一入口; - 速度感知落位:
sheet.release携带松手速度,引擎用库自身的阈值(125.dp 速度阈值、半距离位置阈值)决定 settle 目标,再跑真实animateTo弹簧; - 边缘切换:End / Start 双物理边缘(RTL 语义同上游
ModalSideSheet的解析规则); - 底板回退:
orientation: "bottom"复刻AdaptiveSheet窄屏回退(垂直锚点、顶部圆角)。
sheet.frame 响应字段:
| 字段 | 作用 |
|---|---|
current / target | 真实状态机当前值 / 目标值(hidden / expanded)。 |
offset | 真实面板偏移(px),页面按契约定位公式摆放。 |
progress | 0…1 展开进度,scrim 透明度 = progress × 32%。 |
animating | 引擎弹簧是否仍在跑。 |
ok / op / error | 操作成败与错误通道(Kotlin 异常永不跨 FFI)。 |
引擎侧完整能力(锚点接线、spring settle、exponentialDecay、confirmValueChange 否决、saver)全部在上游 commonMain 里原样随编译进入 .so;JVM 侧保留同源码对上游 Compose 线(1.8.0)的交叉验证编译,证明 ohos .so 里执行的状态逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | shelf 的全部 commonMain 文件原样编译,不提取、不重写;仅新增 1 个 headless 门面文件(376 行,全 fork 唯一新增的库源码)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sample(KMP demo)与 website(Astro 文档站)留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 版本主体与上游完全一致,只把仓库换到 ohos 制品线(换线不换版);compose 因社区线不发布桌面子制品而按 sourceSet 分线声明。 |
| UI 完整 | 单页覆盖全部桥接入口:收件箱 + 过滤侧板、End/Start 边缘切换、底板回退、tokens 明暗对照、版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 没有组合宿主,composables 编译但休眠;ArkTS Demo 把桥暴露的真实状态机画出来——面板位置、scrim 透明度每一帧都来自
.so内的真实SideSheetState。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 compose ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、compose 双线声明
第 3 阶段:桥协议建模 ── sheet.* JSON 契约、settle 阈值语义、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 收件箱 + 手势侧板 + 底板回退 + About,16ms 帧轮询
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
shelf/ 上游库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
shelf/src/commonMain/ 上游 10 文件 785 行,一行未动(含废弃构造器)
shelf/src/ohosMain/ 新增:ShelfHeadless.ohos.kt(376 行,全 fork 唯一新增库源码)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
example/BRIDGE_CONTRACT.md 桥契约(两侧共同遵守的协议文档)
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份,CN 版 8600+ 字)
README.md / README.upstream.md fork 门面首页 / 上游原文封存
sample/ website/ 上游 KMP demo 与文档站,留盘不进 settings
*.upstream 上游构建 5 件套封存(不参与构建,可审计)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :shelf 与 :example:nativeApp,rootProject.name = "ohos_shelf"。上游的 AGP classpath、maven-publish + GPG 签名、binary-compatibility-validator 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(Adoptium 21.0.12.101,org.gradle.java.home 指向本机安装) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.6.1(上游 8.12 升级;并补齐上游缺失的 gradlew.bat) |
| Compose(ohos 线) | 1.12.0-0.1.0-07 runtime / foundation / animation / ui(klib) |
| Compose(jvm 线) | 1.8.0——正是上游 compose-multiplatform 版本(交叉验证) |
| kotlinx | coroutines 1.11.0-0.1.0-07(ohos 线)/ 1.9.0(jvm 线,同上游 catalog) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.shelf |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
shelf | 10(785 行) | compose runtime/foundation/animation/ui + coroutines flow,全部纯 Kotlin commonMain,零 expect/actual | 整体编译(jvm + 双 ohos) |
sample | 3 + 平台入口 | AGP / wasmJs / iOS 框架 demo app | 留盘不进构建 |
website | — | Astro + Starlight 文档站(Node 项目) | 留盘不进构建 |
上游库的关键结构事实决定了两件事:
其一,渲染半边与逻辑半边天然分离。ModalSideSheet / StandardSideSheet / AdaptiveSheet 三个 composable 是渲染;SideSheetState 是纯逻辑(无 @Composable,构造只需 Density),内部 AnchoredDraggableState 承载锚点、positionalThreshold = distance * 0.5f、velocityThreshold = 125.dp、SpringSpec settle、exponentialDecay fling。OHOS 没有组合宿主,渲染半边编译后休眠;逻辑半边正是值得桥出的真实引擎。
其二,updateAnchors / snapTo / anchoredDraggableState 是 internal。跨模块不可见,所以 headless 门面必须放进库模块自身的 ohosMain(同模块可见)——这是全 fork 唯一新增源文件的落位原因:
// shelf/src/ohosMain/kotlin/io/github/aldefy/sidesheet/ShelfHeadless.ohos.kt
// headless facade over the real SideSheetState machinery
// (cmp_qrose / krogress headless pattern)
private val state = SideSheetState(
initialValue = ...,
density = Density(densityPx, fontScale), // 与 rememberSideSheetState 捕获 LocalDensity 同款
)
init {
if (orientation == "bottom") state.updateVerticalAnchors(sheetPx) // AdaptiveSheet 回退
else state.updateAnchors(sheetPx, isEndEdge) // ModalSideSheet 接线
}
1.4 页面边界
标题区 SHELF · OPENHARMONY / 版本握手行(shelfVersion() → 1.0.0-alpha01-ohos.1)
状态行 绿色单行(如 sheet.create side · sheetPx=1088 · density=3.50 → show)
Tab 切换 Side sheet / Bottom sheet / About
Side sheet 收件箱列表(12 封示例邮件 + 彩色首字母头像)+ ☰ 过滤按钮;
End / Start 边缘 chips;侧板 = Filters 标题 + Labels 四复选 + Format 三 chip
Bottom sheet 同一收件箱 + 底部滑出面板(顶部圆角、垂直锚点)
About 版本握手、真实引擎说明、架构分层、tokens 明暗对照表、仓库链接
页面启动时先调 shelfVersion() 握手并加载 tokens,再按需 sheet.create(density 取 display.getDefaultDisplaySync().densityPixels)。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上 compose ohos 制品线的可达性。用 .module 元数据逐个验证:
org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07
org.jetbrains.compose.animation:animation:1.12.0-0.1.0-07
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07
variants: ohosArm64Api-published / ohosX64Api-published 齐全
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
variants: 同上
两个关键结论:其一,整库 commonMain(@Composable 三件套 + AnchoredDraggableState 状态机)对着社区 1.12 线直接编译,一行不用改——包括上游那个 foundation 1.8 已废弃的 AnchoredDraggableState 构造器,源码自带的 @Suppress("DEPRECATION") 足够(社区线保持 Warning 级);其二,社区线不发布 *-desktop 子制品,jvm 交叉验证 target 不能从这条线解析——这是与 KStore(单线同版本双变体)最大的差异,compose 必须按 sourceSet 分线。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07 / Gradle 9.6.1)
社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose 线,Gradle 8.14.1)。但本批适配(capsule / krogress 先例)统一钉 2.4.20-0.1.0-07:社区 compose 1.12.0-0.1.0-07 线基于 stdlib 2.4.20 构建,旧 Kotlin 读不了 2.4 元数据。Gradle 走 9.6.1——这是与 HarmonyOS Kotlin 2.4.20 线验证过的组合(capsule 从上游 9.8.0 主动降到 9.6.1);compose-shelf 上游是 8.12,随根构建替换升到 9.6.1,并补齐上游根本没有的 gradlew.bat(上游只有 gradlew shell 脚本,Windows 构建起不来)。
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_shelf"
include(":shelf")
include(":example:nativeApp")
2.3 编译真实引擎
shelf/build.gradle.kts 收敛为纯 KMP 模块——与 capsule 的差别是必须保留 compose 编译器插件(库里有 @Composable;capsule 纯几何零 composable 可以省,本库不行):
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.compose.compiler) // 库声明 @Composable,编译器插件必需(krogress 模式)
}
kotlin {
applyDefaultHierarchyTemplate()
jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } // 交叉验证用,同上游 17
ohosArm64()
ohosX64()
sourceSets {
ohosMain.dependencies { // ohos 线(社区 compose klib)
api(libs.compose.runtime)
api(libs.compose.foundation)
api(libs.compose.animation)
api(libs.compose.ui)
implementation(libs.kotlinx.coroutines.core) // 1.11.0-0.1.0-07
}
jvmMain.dependencies { // jvm 线(Maven Central 上游线 1.8.0)
api(libs.compose.runtime.jvm)
api(libs.compose.foundation.jvm)
api(libs.compose.animation.jvm)
api(libs.compose.ui.jvm)
implementation(libs.kotlinx.coroutines.core.jvm) // 1.9.0
}
}
}
“line switch, not a version switch”——坐标是同一个多平台 GA,只是 ohosMain 指社区线、jvmMain 指上游线。实测:
.\gradlew.bat :shelf:compileKotlinJvm # BUILD SUCCESSFUL(上游源码 × 上游 1.8.0 线)
.\gradlew.bat :shelf:compileKotlinOhosArm64 :shelf:compileKotlinOhosX64 # BUILD SUCCESSFUL(零源码手术)
上游 commonMain 一个字没动;唯一的新源码是 ohosMain 的 ShelfHeadless 门面。
2.4 测试依赖的处置:上游本来就没有测试
shelf 上游没有任何测试代码(commonTest 依赖声明了但目录不存在)。处置沿用两条纪律:jvm target 承担交叉验证(同一份源码对上游制品线编译通过即"没漂"的证据),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:桥协议建模
3.1 JSON 契约
io.github.aldefy.sidesheet/**(上游引擎原样编译 + ShelfHeadless 门面)
↓ SideSheetState / AnchoredDraggableState(锚点 · 弹簧 · 阈值)
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API shelfVersion() / shelfDispatch()
↓
ArkUI Index.ets(三页签 · 16ms 帧轮询)
九个 op:
tokens {"op":"tokens","dark":false} → M3 设计令牌(明/暗)
sheet.create{"op":"sheet.create","density":3.5,"sheetPx":1088,
"orientation":"side","edge":"end","rtl":false,
"initial":"hidden"} → 建会话 + 真实锚点接线
sheet.show / sheet.hide → 真实 expand()/hide() 弹簧
sheet.drag {"op":"sheet.drag","delta":-14.5} → 真实 dispatchRawDelta
sheet.release{"op":"sheet.release","velocity":1830.2} → 阈值判定 + 真实 animateTo
sheet.snap {"op":"sheet.snap","value":"hidden"} → 程序化跳转(无动画)
sheet.frame {} → current/target/offset/progress/animating
sheet.dispose {} → 页面卸载
响应 : {"ok":true,"op":"..."} / {"ok":false,"op":"...","error":"..."}
两个不对称设计:请求是扁平对象(值只有字符串 / 数字 / 布尔,手写 MiniJson 就能解析,ohos 二进制上不引 kotlinx.serialization);响应里的 offset / progress 是 4 位定点小数(Kotlin/Native 没有 String.format,裸 Float.toString 可能进科学计数法,固定 4 位 + 去尾零既保证 JSON 合法又保持像素以下精度)。
3.2 settle 语义为什么这样建模
上游松手落位(anchoredDraggable modifier 的 settle)做两件事:用速度阈值和位置阈值决定目标锚点,然后带着速度跑 animateTo。桥把这拆成"判定"与"执行":
- 判定在门面重放:阈值常量逐字取自
SideSheetState构造(velocityThreshold = 125.dp.toPx()按会话 density 换算、positionalThreshold = distance * 0.5f)——速度 ≥ 阈值沿速度方向选锚,否则过半距离者胜; - 执行是真实的:门面调
anchoredDraggableState.animateTo(target),spring / decay / 锚点插值全部是库内原件。
一个实测发现必须记录:社区 1.12 线的 animateTo 没有 (target, velocity) 形态。编译探测出的显式签名是 (velocity, anchoredDragScope, anchors, latestTarget, snapAnimationSpec)——AnchoredDragScope 是组合宿主提供的,headless 拿不到。因此 release 的落位动画用与库自身 expand() / hide() 相同的 animateTo(target)(真实弹簧从真实拖拽 offset 出发),测量速度只参与目标判定。这是本适配唯一一处"上游 settle 语义降级",已写入桥契约与已知限制。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ShelfDispatch")
fun shelfDispatch(request: String): String = try {
/* op 分发 */
} catch (t: Throwable) {
"""{"ok":false,"op":...,"error":${jsonString(t.message ?: t.toString())}}"""
}
非法 JSON、缺 op / delta / velocity、density ≤ 0、不支持的 op、无会话(未 create 先 frame),全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libshelf_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosshelf.so)
│ ShelfDispatch / ShelfVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;让 ArkTS 复算弹簧又会复制状态机语义——所以选择 C ABI + JSON,“Kotlin 算偏移、ArkTS 画”。
4.2 Kotlin/Native 导出函数
@CName("ShelfDispatch")
fun shelfDispatch(request: String): String // 九个 op 一口进
@CName("ShelfVersion")
fun shelfVersion(): String = ShelfHeadless.VERSION // "1.0.0-alpha01-ohos.1"
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ShelfDispatch | shelfDispatch(req) | 会话创建 + 锚点接线 + show/hide/drag/release/snap/frame/tokens/dispose |
ShelfVersion | shelfVersion() | fork 版本握手(真机版本行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"shelfDispatch", nullptr, NapiShelfDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"shelfVersion", nullptr, NapiShelfVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosshelf_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosshelf_api.h 按社区已审仓库同款手写精简头(只声明 ExportedSymbols 结构与两个业务符号,21 行),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohosshelf SHARED IMPORTED)
set_target_properties(ohosshelf PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(shelf_napi SHARED napi_init.cpp)
target_link_libraries(shelf_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosshelf)
OHOS_ARCH 为空或 libohosshelf.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const shelfDispatch: (request: string) => string;
export const shelfVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 会话创建与帧循环
aboutToAppear(): void {
this.version = shelfVersion(); // 版本握手:来自真实 Native 调用
this.loadTokens(false); this.loadTokens(true); // tokens 明暗各一次
}
private openSheet(): void {
const d = display.getDefaultDisplaySync();
this.dispatch({ op: 'sheet.create', density: d.densityPixels,
sheetPx: Math.min(360, this.containerW * 0.85) * d.densityPixels,
orientation: 'side', edge: this.edge, rtl: false, initial: 'hidden' });
this.dispatch({ op: 'sheet.show' });
this.frameTimer = setInterval(() => this.pullFrame(), 16); // 60fps 档轮询
}
private pullFrame(): void {
const f = this.dispatch({ op: 'sheet.frame' });
this.sheetOffset = f.offset; this.progress = f.progress; // @State 驱动面板与 scrim
if (f.current === 'hidden' && !f.animating) { this.closeOverlay(); }
}
面板定位按桥契约:物理右缘 x = containerWidth - sheetPx + offset、左缘 x = offset、底板 y = containerHeight - sheetPx + offset;scrim 透明度 = progress × 32%(token 的真实 ScrimColor)。
5.2 手势链路
PanGesture({ fingers: 1 })
.onActionUpdate((e: GestureEvent) => {
const deltaPx = (e.offsetX - this.lastX) * this.density; // ArkTS 手势值是 vp,先折 px
this.lastX = e.offsetX;
this.dispatch({ op: 'sheet.drag', delta: deltaPx }); // → 真实 dispatchRawDelta
})
.onActionEnd((e: GestureEvent) => {
this.dispatch({ op: 'sheet.release', velocity: e.velocityX * this.density });
})
拖拽期间引擎同步跟手(下一帧 sheet.frame 即反映增量);松手后由真实弹簧接管,页面继续按帧吸收直到 animating:false。底板页签用 offsetY / velocityY 同构。
5.3 对比度和错误边界
面板底色、把手、文字色全部取 tokens op 返回的真实库色值(明暗两套 M3 常量);scrim 点击触发 sheet.hide;ok:false 时错误原文进状态行。aboutToDisappear 清 interval 并 sheet.dispose,不泄漏引擎线程。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── BRIDGE_CONTRACT.md 桥契约(C ABI + 9 op + 定位公式 + 真实性边界)
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosshelf)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ ├── Bridge.kt @CName × 2 + 9 op 分发 + fnum(165 行)
│ └── MiniJson.kt 扁平 JSON 解析器(88 行,cmp_qrose 模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.shelf、versionName 1.0.0-alpha01-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/libohosshelf_api.h
├── src/main/cpp/types/libshelf_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 验收页面(1181 行 · 三页签)
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :shelf:compileKotlinJvm :shelf:compileKotlinOhosArm64 :shelf:compileKotlinOhosX64
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
签名在 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.shelf -m entry
四、完整代码对照
4.1 整体架构
io.github.aldefy.sidesheet/**(上游引擎原样编译)
│ SideSheetState / AnchoredDraggableState / 锚点 / spring / 阈值
│ ModalSideSheet / StandardSideSheet / AdaptiveSheet(编译休眠,等待组合宿主)
ShelfHeadless.ohos.kt(库内 headless 门面,376 行)
│ 会话管理 + HeadlessFrameClock(8ms) + 真实引擎驱动
Bridge.kt + MiniJson
│ ShelfDispatch / ShelfVersion
libshelf_napi.so
│ shelfDispatch / shelfVersion
Index.ets
│ 手势采样 + JSON parse + 16ms 帧轮询
▼
ArkUI 真机页面(三页签 · 真实弹簧驱动的侧板)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_shelf、sample/website 移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + compose 双线(ohos 1.12.0-0.1.0-07 / jvm 1.8.0)钉版 |
build.gradle.kts(根) | 版本 1.0.0-alpha01-ohos.1、全局禁用 Ohos*Test* 任务 |
shelf/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,compose 编译器插件,依赖按线分 sourceSet |
shelf/src/ohosMain/.../ShelfHeadless.ohos.kt | 唯一新增库源文件(376 行 headless 门面) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、9 op 分发、MiniJson、fnum、错误通道 |
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 验收页面(1181 行) |
example/BRIDGE_CONTRACT.md | 桥契约(两侧共同遵守) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(15 章) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | SideSheetState(density, animationSpec) | 真实状态持有者(SpringSpec settle + confirmValueChange) |
| 引擎 | updateAnchors / updateVerticalAnchors | Hidden=±sheetPx / Expanded=0 的锚点接线(侧板 / 底板) |
| 引擎 | anchoredDraggableState.dispatchRawDelta | 拖拽增量直达(与 anchoredDraggable modifier 同入口) |
| 引擎 | expand() / hide() / snapTo() | suspend 弹簧动画 / 程序化跳转 |
| 引擎 | currentValue / targetValue / offset | 帧采样的数据源 |
| 引擎 | SideSheetDefaults | 360dp / 1dp / 32% scrim / 28dp / 4×32 把手 + 明暗 M3 色板 |
| 门面 | ShelfHeadless.create/drag/release/frame/tokens | 会话生命周期与 JSON 化 |
| Native | ShelfDispatch / ShelfVersion | C ABI JSON 导出 |
| N-API | shelfDispatch(req) / shelfVersion() | 向 ArkTS 暴露能力 |
| ArkTS | pullFrame() / Pan 手势 | 帧定位 + 手势采样 |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS:手势增量 → 真实引擎,松手只传速度
const res = JSON.parse(shelfDispatch(JSON.stringify({
op: 'sheet.release', velocity: e.velocityX * this.density
})));
// res.ok === true;res.settleTarget === 'hidden'(阈值判定结果)
// 此后每 16ms 的 sheet.frame 给出真实弹簧的 offset 直到 animating:false
// Kotlin:门面把松手交给真实机器
fun release(velocityPxPerSec: Float) {
val target = settleTarget(state.offset, velocityPxPerSec) // 125.dp + 半距离阈值
scope.launch { state.anchoredDraggableState.animateTo(target) } // 真实弹簧
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译整库而不是提取零依赖内核
社区 compose ohos 制品线(1.12.0-0.1.0-07)补齐后,@Composable 三件套与状态机能原样编译——再抽内核等于把 Sheet 语义手抄一遍,引入漂移风险。真编译让锚点、弹簧、阈值、confirmValueChange、saver 全部是上游原件;渲染半边编译进 .so 休眠,等未来组合宿主就位即可唤醒。
决策 2:headless 门面放库模块内(krogress 模式),而不是旁路重写
SideSheetState 的关键入口(updateAnchors / snapTo / anchoredDraggableState)是 internal,跨模块不可见——门面必须作为库自身的 ohosMain 源码存在。这与 krogress 的 KrogressHeadless 同构:引擎驱动的每一帧都来自真实 Compose 动画机器(本库经 HeadlessFrameClock 每 8ms 一 tick 的 MonotonicFrameClock),ArkTS 永不算动画。
决策 3:compose 双制品线,"换线不换版"但按 sourceSet 分线
与 KStore(kotlinx 单线同版本即可)不同,社区 compose 线不发布 *-desktop 子制品:ohosMain 指社区线 1.12.0-0.1.0-07,jvmMain 指上游线 1.8.0(正是上游构建版本)。交叉验证 target 编译的就是上游同款制品,"没漂"的证明才成立——capsule / krogress 同款分线。
决策 4:Gradle 升 9.6.1 并补 gradlew.bat
上游 wrapper 8.12 + 只有 gradlew(无 bat)。与 HarmonyOS Kotlin 2.4.20 线验证过的组合是 Gradle 9.6.1(capsule 从 9.8.0 主动降级得到),wrapper 随根构建替换升级,gradlew.bat 从同模式仓库引入——Windows 构建链路从零补齐。
决策 5:settle 拆成"判定重放 + 真实执行"
社区线 animateTo 无 velocity 形态(显式签名需要组合宿主的 AnchoredDragScope)。与其造一个 scope 破坏真实性边界,不如:阈值判定在门面重放(常量逐字取自库),落位动画走库自身 expand() / hide() 同款 animateTo(target)——弹簧从真实拖拽 offset 出发,测量速度只决定目标。降级点如实写进桥契约与已知限制。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 10(x64)、JDK 21(Adoptium 21.0.12.101)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :shelf:compileKotlinJvm
上游 compose-shelf 没有测试代码(上游 commonTest 只声明了依赖,无测试文件),交叉验证的落位是"同一份未改动源码对着上游 Compose 1.8.0 线编译通过":jvm target 用上游线产物编上游源码,ohos target 用社区线产物编同一份源码,两边都绿即证明换线没有引入 API 漂移——包括最敏感的废弃 AnchoredDraggableState 构造器在 1.12 线上仍是 Warning 级。jvmTest 任务保留(空跑),CI(.github/workflows/test.yml)在 ubuntu + JDK 21 上执行同样的交叉验证编译。
6.3 HAP 验证
:shelf:compileKotlinJvm: BUILD SUCCESSFUL
:shelf:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(2m 36s)
llvm-nm -D: ShelfDispatch / ShelfVersion / libohosshelf_symbols 均为全局 T 符号
实测产物(debugShared,含调试信息未裁剪):
| ABI | libohosshelf.so | libc++_shared.so |
|---|---|---|
| arm64-v8a | 29,130,840 字节(约 27.8 MiB) | 1,850,184 字节 |
| x86_64 | 27,832,736 字节(约 26.5 MiB) | 1,731,432 字节 |
DevEco Studio 打开 example/harmonyApp 完成自动签名后构建 HAP(双 ABI 三件套 so),真机安装运行成功。
6.4 功能用例
- 启动后 About 页与日志出现
shelfVersion() → 1.0.0-alpha01-ohos.1(真实 Native 调用,非静态文字); - 状态行回显真实会话参数:
sheet.create side · sheetPx=1088 · density=3.50(density 来自真机densityPixels); - 点 ☰ 打开侧板:scrim 以 32% 黑淡入,面板从右缘以真实弹簧滑入(28dp 内缘圆角、4×32 药丸把手、Filters 内容);
- 拖拽面板:offset 与手指同步(
dispatchRawDelta实时生效),scrim 透明度随 progress 变化; - 中途松手:快速外甩 → settle 到 hidden;小幅拖动后松手 → 过半距离者胜回到 expanded——速度阈值与位置阈值路径均可复现;
- End / Start chips 切换后重开会话:面板从另一缘滑入,锚点方向随物理边缘翻转;
- Bottom sheet 页签:面板自底部滑入(顶部圆角、垂直锚点),垂直拖拽 / 松手同样由真实引擎驱动;
- About 页 tokens 表与真机面板配色一致(明暗两套 M3 常量来自库内 SideSheetDefaults)。
6.5 验证结论
状态引擎、Compose 依赖线、Native 动态库、C ABI、N-API、ArkUI 手势页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,侧板打开 / 拖拽 / 速度感知落位 / 边缘切换 / 底板回退全部由 .so 内的真实 SideSheetState 驱动(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-shelf.git
cd compose-shelf
export JAVA_HOME="/path/to/jdk-21" # Windows 用 $env:JAVA_HOME 指向 JDK 21
./gradlew :shelf:compileKotlinJvm :shelf:compileKotlinOhosArm64 :shelf:compileKotlinOhosX64
./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.shelf -m entry
hdc shell hilog | grep Shelf
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:shelf真正编译成ohosArm64/ohosX64动态库——UI 库还要回答"composables 编译了谁来渲染",本仓的答案是 headless 状态机 + ArkTS 重绘; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Windows 下
gradlew.bat启动 wrapper 需要先设JAVA_HOME——gradle.properties的org.gradle.java.home只管 daemon,管不了 wrapper 自己的 bootstrap JVM; - 社区 compose 线不发布
*-desktop子制品:jvm 交叉验证 target 必须单独指 Maven Central 上游线(1.8.0),依赖按 sourceSet 分线声明,commonMain 保持 line-agnostic; - 库里有
@Composable就必须应用 compose 编译器插件(org.jetbrains.kotlin.plugin.compose)——capsule 纯几何库可省,本库与 krogress 不可省; - 上游
AnchoredDraggableState旧构造器在社区 1.12 线仍是 Warning 级 deprecation,源码自带的@Suppress("DEPRECATION")足够,零源码手术成立——但这个结论必须实测,不能从上游版本号推断; - 社区线
animateTo没有(target, velocity)形态(显式签名要AnchoredDragScope):release 走animateTo(target),速度只参与阈值判定,降级点写进契约; - Kotlin/Native 没有
String.format,且Float.toString可能进科学计数法:JSON 数字序列化用 4 位定点 + 去尾零的fnum,颜色 hex 手写UInt.toString(16)补零; - headless 引擎的 suspend 动画需要
MonotonicFrameClock:用delay(8ms)+ 单调钟时间戳的自制 FrameClock 驱动(krogress 模式),挂在专用单线程 dispatcher 上; - ArkTS 手势事件值是 vp 不是 px:delta 与 velocity 都要乘
densityPixels再过桥,否则阈值判定(125.dp 换算)失真; - 上游
.gitignore的/build与/*/build是根锚定模式,盖不住example/nativeApp/build/(三级目录)——提交前必须补任意层级build/规则,否则 klib 构建产物混进仓库; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();HAP 缺当前 ABI 的 so 会报安装失败9568347,abiFilters必须双 ABI、三件套缺一不可。
8.2 已知问题
- OHOS 尚无 Compose 组合宿主:三个 Sheet composable 编译进
.so但休眠,ArkTS Demo 按sheet.frame重绘面板; - settle 的"带速度起跑"降级:社区线
animateTo无 velocity 形态,松手弹簧从拖拽 offset 出发(判定仍用真实阈值与测量速度); - 阈值判定在门面重放(常量逐字取自库),是唯一一处非上游原件的运行逻辑——已在桥契约标注"真实性边界";
SideSheetDefaults的明暗色板是私有常量,tokens 以逐字抄写 + 行号引用方式导出(krogress catalog 模式),公开常量(scrim / handle / 宽度)读真实对象;- android / wasmJs / iOS 目标剥离(源文件留在树里),maven-publish + GPG + BCV 一并剥离,本 fork 不发 Maven Central;
- 验证产物为 debug so(约 27–29 MiB,含调试信息),release 裁剪是后续项;
sheet.snap未被 Demo 页面调用(协议在册),RTL 手势语义以rtl:false验收;- HAP 安装必须使用与 bundle name
org.terminator.ohos.shelf匹配的签名。
8.3 未来优化方向
- 评估社区线未来暴露 velocity 形态
animateTo(或AnchoredDragScope独立构造)后,把 release 升级为带速度起跑的完整 settle; - release 模式链接 + strip,把
.so从 ~28 MiB 降到发布量级; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)验收矩阵、RTL 布局与
sheet.snap的自动化用例。
九、总结
9.1 核心难点回顾
上游 shelf(全部 commonMain,零 expect/actual)
→ 验证 compose ohos 制品线(runtime/foundation/animation/ui 1.12.0-0.1.0-07)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 8.12→9.6.1 + 补 gradlew.bat)
→ 根构建替换(cmp_qrose 模式,sample/website 留盘)
→ ohosArm64 / ohosX64 KLIB + 唯一 376 行 headless 门面
→ C ABI(@CName × 2)+ 9 op 扁平 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS 手势采样 + 16ms 帧轮询
→ ArkUI 真机验收(侧板 / 拖拽 / 速度落位 / 底板回退)
9.2 封装层次
- 引擎层:上游原版
SideSheetState/AnchoredDraggableState(锚点、spring、decay、阈值、confirmValueChange); - 门面层:
ShelfHeadless(会话生命周期、HeadlessFrameClock、tokens 导出); - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:手势采样、会话管理、帧定位、scrim / 圆角 / 把手绘制与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 组件库的适配单位是"状态机"而不是"像素":
SideSheetState与渲染分离的架构让交互语义(锚点 / 弹簧 / 阈值)可以完整过桥——面板长什么样 ArkTS 画,面板怎么动 Kotlin 算。选库先看它的逻辑半边能不能独立构造; - 废弃 API 的可用性必须实测:上游钉在 foundation 1.8 的废弃构造器,社区 1.12 线仍是 Warning 级——"整库零手术"这个结论是从编译输出来的,不是从版本号推的;同理
animateTo的签名差异也是编译探测出来的,适配 UI 库要预备好做这种 API 考古; - 交叉验证的形态跟着上游走:上游有 51 项测试就跑测试(KStore),上游没有测试就用"同源码 × 上游制品线编译"当闸门——证明力都来自"同一份未改动源码",形式不必强求一致。
9.4 适配成果
当前适配已完成:
- compose-shelf
1.0.0-alpha01真实状态引擎的双 ABI Kotlin/Native 编译(上游commonMain零改动,仅 1 个 headless 门面新增源文件); - 社区 Compose ohos 制品线在 UI 组件库上的完整落地(compose
1.12.0-0.1.0-07+ coroutines1.11.0-0.1.0-07,jvm 线1.8.0交叉验证); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号),9 op 扁平 JSON 桥协议与契约文档; - DevEco Stage ArkUI 三页签验收页面(收件箱 + 手势侧板 + 底板回退 + tokens 明暗对照 + 版本握手,1181 行);
- 双 ABI
.so构建(arm64 27.8 MiB / x86_64 26.5 MiB debug)、HAP 构建、签名、真机安装和运行验证; - 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)