本文记录 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——与上游 anchoredDraggable modifier 同一入口;
  • 速度感知落位:sheet.release 携带松手速度,引擎用库自身的阈值(125.dp 速度阈值、半距离位置阈值)决定 settle 目标,再跑真实 animateTo 弹簧;
  • 边缘切换:End / Start 双物理边缘(RTL 语义同上游 ModalSideSheet 的解析规则);
  • 底板回退:orientation: "bottom" 复刻 AdaptiveSheet 窄屏回退(垂直锚点、顶部圆角)。

sheet.frame 响应字段:

字段作用
current / target真实状态机当前值 / 目标值(hidden / expanded)。
offset真实面板偏移(px),页面按契约定位公式摆放。
progress0…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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(Adoptium 21.0.12.101,org.gradle.java.home 指向本机安装)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.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 版本(交叉验证)
kotlinxcoroutines 1.11.0-0.1.0-07(ohos 线)/ 1.9.0(jvm 线,同上游 catalog)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.shelf
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
shelf10(785 行)compose runtime/foundation/animation/ui + coroutines flow,全部纯 Kotlin commonMain,零 expect/actual整体编译(jvm + 双 ohos)
sample3 + 平台入口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 方法覆盖内核
ShelfDispatchshelfDispatch(req)会话创建 + 锚点接线 + show/hide/drag/release/snap/frame/tokens/dispose
ShelfVersionshelfVersion()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.tomlKotlin 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.kt2 个 C ABI、9 op 分发、MiniJson、fnum、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(1181 行)
example/BRIDGE_CONTRACT.md桥契约(两侧共同遵守)
README.OpenHarmony_CN.md鸿蒙适配主文档(15 章)

4.3 关键 API 对照

层次API作用
引擎SideSheetState(density, animationSpec)真实状态持有者(SpringSpec settle + confirmValueChange)
引擎updateAnchors / updateVerticalAnchorsHidden=±sheetPx / Expanded=0 的锚点接线(侧板 / 底板)
引擎anchoredDraggableState.dispatchRawDelta拖拽增量直达(与 anchoredDraggable modifier 同入口)
引擎expand() / hide() / snapTo()suspend 弹簧动画 / 程序化跳转
引擎currentValue / targetValue / offset帧采样的数据源
引擎SideSheetDefaults360dp / 1dp / 32% scrim / 28dp / 4×32 把手 + 明暗 M3 色板
门面ShelfHeadless.create/drag/release/frame/tokens会话生命周期与 JSON 化
NativeShelfDispatch / ShelfVersionC ABI JSON 导出
N-APIshelfDispatch(req) / shelfVersion()向 ArkTS 暴露能力
ArkTSpullFrame() / 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,含调试信息未裁剪):

ABIlibohosshelf.solibc++_shared.so
arm64-v8a29,130,840 字节(约 27.8 MiB)1,850,184 字节
x86_6427,832,736 字节(约 26.5 MiB)1,731,432 字节

DevEco Studio 打开 example/harmonyApp 完成自动签名后构建 HAP(双 ABI 三件套 so),真机安装运行成功。

6.4 功能用例

  1. 启动后 About 页与日志出现 shelfVersion() → 1.0.0-alpha01-ohos.1(真实 Native 调用,非静态文字);
  2. 状态行回显真实会话参数:sheet.create side · sheetPx=1088 · density=3.50(density 来自真机 densityPixels);
  3. 点 ☰ 打开侧板:scrim 以 32% 黑淡入,面板从右缘以真实弹簧滑入(28dp 内缘圆角、4×32 药丸把手、Filters 内容);
  4. 拖拽面板:offset 与手指同步(dispatchRawDelta 实时生效),scrim 透明度随 progress 变化;
  5. 中途松手:快速外甩 → settle 到 hidden;小幅拖动后松手 → 过半距离者胜回到 expanded——速度阈值与位置阈值路径均可复现;
  6. End / Start chips 切换后重开会话:面板从另一缘滑入,锚点方向随物理边缘翻转;
  7. Bottom sheet 页签:面板自底部滑入(顶部圆角、垂直锚点),垂直拖拽 / 松手同样由真实引擎驱动;
  8. 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :shelf 真正编译成 ohosArm64 / ohosX64 动态库——UI 库还要回答"composables 编译了谁来渲染",本仓的答案是 headless 状态机 + ArkTS 重绘;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Windows 下 gradlew.bat 启动 wrapper 需要先设 JAVA_HOME——gradle.properties 的 org.gradle.java.home 只管 daemon,管不了 wrapper 自己的 bootstrap JVM;
  4. 社区 compose 线不发布 *-desktop 子制品:jvm 交叉验证 target 必须单独指 Maven Central 上游线(1.8.0),依赖按 sourceSet 分线声明,commonMain 保持 line-agnostic;
  5. 库里有 @Composable 就必须应用 compose 编译器插件(org.jetbrains.kotlin.plugin.compose)——capsule 纯几何库可省,本库与 krogress 不可省;
  6. 上游 AnchoredDraggableState 旧构造器在社区 1.12 线仍是 Warning 级 deprecation,源码自带的 @Suppress("DEPRECATION") 足够,零源码手术成立——但这个结论必须实测,不能从上游版本号推断;
  7. 社区线 animateTo 没有 (target, velocity) 形态(显式签名要 AnchoredDragScope):release 走 animateTo(target),速度只参与阈值判定,降级点写进契约;
  8. Kotlin/Native 没有 String.format,且 Float.toString 可能进科学计数法:JSON 数字序列化用 4 位定点 + 去尾零的 fnum,颜色 hex 手写 UInt.toString(16) 补零;
  9. headless 引擎的 suspend 动画需要 MonotonicFrameClock:用 delay(8ms) + 单调钟时间戳的自制 FrameClock 驱动(krogress 模式),挂在专用单线程 dispatcher 上;
  10. ArkTS 手势事件值是 vp 不是 px:delta 与 velocity 都要乘 densityPixels 再过桥,否则阈值判定(125.dp 换算)失真;
  11. 上游 .gitignore 的 /build 与 /*/build 是根锚定模式,盖不住 example/nativeApp/build/(三级目录)——提交前必须补任意层级 build/ 规则,否则 klib 构建产物混进仓库;
  12. 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 三条经验

  1. UI 组件库的适配单位是"状态机"而不是"像素":SideSheetState 与渲染分离的架构让交互语义(锚点 / 弹簧 / 阈值)可以完整过桥——面板长什么样 ArkTS 画,面板怎么动 Kotlin 算。选库先看它的逻辑半边能不能独立构造;
  2. 废弃 API 的可用性必须实测:上游钉在 foundation 1.8 的废弃构造器,社区 1.12 线仍是 Warning 级——"整库零手术"这个结论是从编译输出来的,不是从版本号推的;同理 animateTo 的签名差异也是编译探测出来的,适配 UI 库要预备好做这种 API 考古;
  3. 交叉验证的形态跟着上游走:上游有 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 + coroutines 1.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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐