开源鸿蒙平台 CMP 三方库「SegmentedControl」适配全流程

本文记录 SegmentedControl 接入 OpenHarmony 的完整过程,覆盖 UI 库盘点、纯 Kotlin 内核提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

SegmentedControl 是基于 Compose Multiplatform 的 iOS 风格分段控件库:SegmentItem(label / icon / enabled)列表、三种风格 iOS / Material3 / Pill、Equal / Content 两种宽度分布、滑块 spring 动画、键盘方向键导航自动跳过禁用段、RTL 支持。本次适配与 KStore 那批「真编译整库」路线正好相反:上游公开 API 100% 是 androidx.compose (Modifier / Shape / Animatable / MaterialTheme),社区鸿蒙 CMP 还停在 1.9.2 时代且 Kotlin 线不同,整库编不过。因此走的是社区最早沉淀、已多仓过审的「内核提取」标准路线(qrose / swipe / Flippable / compose-collapsing-toolbar / WheelPickerCompose 同款):控件背后的全部算术------选中校验与 coerce、键盘导航跳禁用段、样式几何表、Material3 基线配色派生、滑块 spring 物理积分------被提取成零依赖纯 Kotlin 引擎 编译进 OpenHarmony 动态库,ArkUI 页面等价重写 UI 并逐帧驱动引擎,滑块动画的每一帧都来自 .so 内的物理积分。

项目地址: AtomGit/oh-tpc/segmented-control.git

上游仓库: NadeemIqbal/segmented-control

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

SegmentedControl 为 Compose Multiplatform 应用提供「iOS 风格分段控件」这一件事的完整答案:SegmentedControl(items, selectedIndex, onSelectionChange) 一个调用覆盖全场景,SegmentItem 支持纯文本、纯图标、图文混排三种段内容,enabled 支持整体与单段两级禁用,SegmentedControlStyle 三种预设风格,SegmentedControlWidth.Equal / Content 两种宽度分布,滑块用 spring(MediumBouncy, MediumLow) 在段之间弹性移动,桌面与 Web 上还支持左右方向键导航(自动跳过禁用段)。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套控件能力,但上游构建(Kotlin 2.3.21 / Compose Multiplatform 1.10.3 / AGP 9.2.0 / Gradle 9.5.0)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画出几个按钮,却无法证明控件的规则引擎------校验、导航、几何、配色、动画物理------在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证规则内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍 具体问题
目标缺失 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 产物。
UI 框架不可用 上游公开 API 全部站在 androidx.compose 上;社区鸿蒙 CMP 只有 1.9.2 时代、Kotlin 线不同,Modifier / Shape / Animatable / MaterialTheme 一行都编不过。
工具链不一致 上游 Kotlin 2.3.21 / Gradle 9.5.0 / AGP 9.2.0 与鸿蒙工具链无法共存于一个 Gradle 构建。
语言边界不同 ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
动画语义跨不了桥 Animatable + spring 是 Compose 概念,C ABI 只能同步一来一回;滑块的弹性手感不能靠 ArkTS 手写插值「像一下」。
交付链路复杂 .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:控件规则内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。规则语义完全由镜像上游的引擎负责,ArkTS 只负责测量、调用 Native、解析 JSON 和绘制。

1.2 库提供的能力

Demo 提供一个真实的控件会话,对应桥接的全部入口:

  • 版本握手 :info action 返回 fork 版本、上游版本、Kotlin 版本与全部默认常量,来自真实 Native 调用而非静态文字;
  • 三种风格 :iOS(分隔线 + surface 滑块 + 2dp 阴影)、Material3(primary 滑块、无分隔线、平面)、Pill(体育场形、3dp 边距),几何表全部来自引擎 spec;
  • 深色配色 :一个开关切换 light / dark,六个控件同时向引擎重新拉取 colors------Material3 基线 palette 派生发生在 .so 内;
  • 内容三态 :段可以是纯文本、纯图标(emoji 字形等价 ImageVector)、图文混排,对应上游 SegmentItem;
  • 宽度两模式 :Equal 等分并省略长标签,Content 按内容收缩;
  • 禁用语义:单段禁用灰显、不回调、仍占位;整控件禁用全员灰显;
  • 键盘导航 :◀ / ▶ 按钮等价上游方向键,nextIndex 跳过禁用段;
  • 滑块动画:每 16ms 一帧,位置与速度由引擎 spring 积分返回,ArkTS 零本地插值。

响应包含:

字段 作用
ok 操作是否成功,失败时 error 带原文(如空列表时上游 require 的原话)。
offset / width / offsetV / widthV / done thumb 一帧积分后的滑块位置、宽度、速度与收敛标记。
index / from / changed nextIndex 的导航结果。
track / thumb / selected ... colors 返回的六色 #AARRGGBB(禁用色已预混为不透明)。

引擎侧完整能力(校验、coerce、导航、几何、light/dark 配色、spring 物理)全部在 commonMain 的 SegmentedControlEngine 里随编译进入 .so,JVM 侧保留 18 项交叉验证用例,证明 ohos .so 里执行的规则与上游 Compose 实现没有漂移。

1.3 实现适配

维度 要求
代码复用 规则算术逐语义镜像上游(校验文案、coerce、导航、几何数值、配色派生、spring 参数);上游 Compose 源码 7 文件 + 测试 2 文件原样保留 在 src/composeMain / src/composeTest,永不编译,作为演进基线。
平台目标 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离 根构建替换为极简 ohos 构建(cmp_qrose 模式);sample/ 五个上游示例工程留盘不进构建。
桥接稳定 使用 1 个 C ABI(7 action JSON 分发)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律 内核零第三方依赖------不需要 kotlinx 制品线,也就不受制品线版本牵制。
UI 完整 单页 11 张卡片覆盖全部桥接入口:握手、深色、三风格、内容、宽度、禁用、导航、自定义、日志。
仓库规范 项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。SegmentedControl 本身就是 UI 库,适配面天然最大------UI 必须等价重写,但规则必须留在 .so 里:ArkTS 只做测量与绘制,不做任何「像而已」的本地复刻。

二、实现路线图

text 复制代码
第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认社区 CMP 不可用、确定「内核提取」交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模     ── 7 action 扁平 JSON 契约、无状态引擎、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── onAreaChange 测量、16ms 引擎步进、三风格/禁用/导航卡片
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
text 复制代码
segmented-control/                    库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
segmented-control/src/commonMain/     新增:SegmentedControlEngine(纯 Kotlin 内核,全仓唯一新增 Kotlin 主源码)
segmented-control/src/commonTest/     新增:18 项交叉验证用例
segmented-control/src/composeMain/    上游 7 个 Compose 文件原样迁移,永不编译
segmented-control/src/composeTest/    上游逻辑/UI 测试原样迁移,永不编译
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
sample/                               上游 5 个示例工程,留盘不进构建

构建拓扑走 cmp_qrose 模式 (根构建即 ohos 构建):settings.gradle.kts 只 include :segmented-control 与 :example:nativeApp,rootProject.name = "ohos_segmented_control"。上游的 vanniktech 发布插件、AGP classpath、wasm 样例全部随根构建替换剥离------本 fork 不发 Maven Central。

1.2 工具链矩阵
项目 配置
Kotlin Multiplatform HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,含 ohosArm64/ohosX64 目标与 Konan)
JDK 21(org.gradle.java.home 指向 Adoptium Temurin jdk-21.0.12.101-hotspot)
OpenHarmony 目标 ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle 8.14.1(上游 wrapper 9.5.0 刻意降级;2.2.21 旧线的已验证配对,见 2.2)
第三方依赖 内核零依赖;compose 插件从版本目录整体移除(composeMain 不编译的前提)
DevEco product 5.1.0(18) 兼容
DevEco Studio 5.1.0 Release
ABI arm64-v8a + x86_64
Bundle org.terminator.ohos.segmentedcontrol
bash 复制代码
export JAVA_HOME="C:/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
位置 文件 依赖形态 处置
src/commonMain(上游) 7 compose.runtime/foundation/animation/ui/material3,零平台 API 整体迁移 至 src/composeMain,不注册源集、永不编译
src/commonTest + src/skikoTest(上游) 2 kotlin.test + compose.uiTest 迁移至 src/composeTest,同样不编译
新增内核 1 纯 Kotlin,零第三方 src/commonMain 编入 jvm + 双 ohos

上游虽然全文件站在 Compose 上,但规则本身是纯算术,可以逐语义镜像:

kotlin 复制代码
// 上游 SegmentedControlInternal.kt ------ 键盘导航,跳过禁用段
internal fun nextEnabledIndex(items: List<SegmentItem>, from: Int, direction: Int): Int {
    var i = from + direction
    while (i in items.indices) {
        if (items[i].enabled) return i
        i += direction
    }
    return from
}

// 内核 SegmentedControlEngine.kt ------ 同一条规则,桥友好的布尔列表签名
fun nextEnabledIndex(enabled: List<Boolean>, from: Int, direction: Int): Int {
    val dir = if (direction < 0) -1 else 1
    var i = from + dir
    while (i in enabled.indices) {
        if (enabled[i]) return i
        i += dir
    }
    return from
}

同样被镜像的还有:requireValidSegmentCount(连报错文案都逐字保留)、selectedIndex.coerceIn(0, lastIndex)、resolveStyleSpec 几何表(iOS = 分隔线 + 2dp 边距 + 2dp 阴影 + 8dp 圆角;Material3 = 无分隔线 + 0 阴影;Pill = RoundedCornerShape(percent = 50) 折算为高度一半的体育场形 + 3dp 边距 + 1dp 阴影)、SegmentedControlDefaults.colors 的 Material3 基线派生(禁用色 = onSurface 以 38% alpha 逐通道预混到 surface,ArkTS 可直接不透明绘制)。

唯一没有现成纯 Kotlin 对应物的是滑块动画:上游用 Animatable.animateTo(target, spring(MediumBouncy, MediumLow)) 同时驱动位置与宽度。内核把这条 spring 的微分方程写成了显式积分------x'' = stiffness·(target − x) − damping·v,damping = dampingRatio·2·√stiffness(stiffness 400 / dampingRatio 0.5),半隐式 Euler、每帧 4 子步:

kotlin 复制代码
fun springStep(state: SpringState, target: Double, dtMs: Double,
               stiffness: Double = DEFAULT_STIFFNESS,
               dampingRatio: Double = DEFAULT_DAMPING_RATIO): SpringState {
    val damping = dampingRatio * 2.0 * sqrt(stiffness)
    val substeps = 4
    val h = (dtMs / 1000.0) / substeps
    var x = state.value
    var v = state.velocity
    repeat(substeps) {
        val a = stiffness * (target - x) - damping * v
        v += a * h
        x += v * h
    }
    return SpringState(value = x, velocity = v)
}

MediumBouncy 的欠阻尼特性(阻尼比 0.5)保证了过冲:从 0 弹向 300,实测首帧 offset 17.55 / velocity 1673.43,约 40 帧内收敛,与 Compose Animatable 的手感同源。

1.4 页面边界
text 复制代码
标题区          SEGMENTEDCONTROL · OPENHARMONY / 握手行(0.1.0-ohos.1 · Kotlin 2.2.21-1.0.0)
                so 链路行(libohossegmentedcontrol.so → libsegmentedcontrol_napi.so → ArkTS)
深色开关        Dark palette (engine colors)------全页六控件重新向引擎拉 colors
风格卡 ×3       iOS / Material3 / Pill,各含 Day|Week|Month 控件
内容卡          Text|Icon|Both 切换 + 图文 showcase 控件(Today|Date|Month + emoji 图标)
宽度卡          Equal|Content 切换 + Recommended|Basic|Advanced 长标签演示省略号
禁用卡          A|B(禁用)|C 控件 + 两个开关(禁用 B / 整体 enabled)
导航卡          Inbox|Drafts(禁用)|Sent 控件 + ◀ ▶ 按钮(调 nextIndex)
自定义卡        自定义配色 + 12dp 圆角 + stiffness 800 / ratio 0.35 的硬弹簧
日志区          桥调用留痕(握手、validate 报错原文、safeIndex、导航、拾取,上限 12 条)

页面启动时先调 info 做握手,随后用 validate(count=0) 与 safeIndex(99, 3) 把上游的两条契约演示进日志。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上 Compose 的 ohos 制品现状。两个关键结论:

其一,HarmonyOS Kotlin 2.2.21-1.0.0 发行版自带 ohosArm64 / ohosX64 目标与配套 Konan(kotlin.native.distribution.downloadFromMaven=true),KMP 编译链路可用;其二,社区 Compose-for-ohos 制品线只有 runtime klib(org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-11,仅 molecule 等无 UI 运行时库在用),foundation / material3 / ui 没有可用的 ohos 坐标------上游这 7 个文件一个都编不过。

但这不构成死局:内核提取路线本来就不需要任何 kotlinx / compose 制品。SegmentedControlEngine 零第三方依赖,kotlin("test") 由 HarmonyOS Kotlin 发行版自带------版本目录里只剩一行 Kotlin 版本号,适配不再受生态制品牵制。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

上游是 Kotlin 2.3.21 / Compose 1.10.3 / AGP 9.2.0 / Gradle 9.5.0,与鸿蒙工具链无法共存。社区已过审的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / swipe / Flippable / compose-collapsing-toolbar 用的同款)。Gradle 方面,2.2.21 旧线的已验证配对是 8.14.1 ------上游 wrapper 9.5.0 刻意降级。这与 KStore 那批「2.4.20 新线沿用 Gradle 9.x」正好互为印证:两条工具链线的经验不能互相套用,Gradle 版本跟着 Kotlin 线走,而不是跟着上游走。

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_segmented_control"

include(":segmented-control")
include(":example:nativeApp")

版本目录收敛为一行:

toml 复制代码
[versions]
kotlin = "2.2.21-1.0.0"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }

compose 插件条目从目录整体移除------这是 composeMain 不再参与编译的构建期保证:即使有人误把源集注册回来,没有插件也配不过。

2.3 编译内核

segmented-control/build.gradle.kts 收敛为纯 KMP 模块:

kotlin 复制代码
kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

实测:

bash 复制代码
./gradlew.bat :segmented-control:jvmTest          # BUILD SUCCESSFUL --- 18 项
./gradlew.bat :segmented-control:compileKotlinOhosArm64
./gradlew.bat :segmented-control:compileKotlinOhosX64   # BUILD SUCCESSFUL
2.4 测试任务的处置:ohos 没有测试运行器

ohos 目标只有编译与链接,没有测试运行器。处置与 KStore 同款:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务(连同编译):

kotlin 复制代码
subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

上游测试文件(含 compose.uiTest 的 UI 用例)零改动保留在 composeTest,内核的 18 项新用例在 commonTest------它们镜像上游 SegmentedControlLogicTest 的场景(导航四例、校验两例、几何、颜色),再补上 spring 物理与颜色预混的新用例。

第 3 阶段:桥协议建模

3.1 JSON 契约
text 复制代码
io.github.nadeemiqbal.segmentedcontrol/SegmentedControlEngine(规则内核)
        ↓ 校验 / 导航 / 几何 / 配色 / spring 积分
Bridge.kt(@CName × 1,7 action 扁平 JSON 分发)
        ↓ UTF-8 JSON
N-API segmentedControlDispatch()
        ↓
ArkUI Index.ets(单页滚动,11 卡)

请求与响应:

text 复制代码
请求 : {"action":"nextIndex","enabled":"1,0,1","from":0,"direction":1}
       {"action":"thumb","offset":0,"offsetV":0,"offsetTarget":120,
        "width":96,"widthV":0,"widthTarget":96,"dtMs":16}
响应 : {"ok":true,"index":2,"from":0,"changed":true}
       {"ok":true,"offset":7.02,"offsetV":669.37,"width":96,"widthV":0,"done":false}
       {"ok":false,"error":"SegmentedControl requires at least one item; ..."}

enabled 标志串用 "1,0,1" 逗号分隔表示段列表;数字回包走两位小数定点格式,避免科学计数法跨桥。

3.2 七个 action 的载荷设计
action 请求 响应 对应上游
info 无 版本 + 全部默认常量 + action 清单 引擎握手
validate count ok 或上游 require 原话 requireValidSegmentCount
safeIndex selectedIndex count coerce 后索引 selectedIndex.coerceIn(0, lastIndex)
nextIndex enabled from direction index from changed 键盘导航 nextEnabledIndex
spec style 分隔线/边距/阴影/圆角/形状 resolveStyleSpec 几何表
colors style dark 六色 #AARRGGBB SegmentedControlDefaults.colors
thumb 位置/速度/目标 ×2 + dtMs + 弹簧参数 新位置/速度 + done LaunchedThumbEffect 一帧积分

两个设计要点:引擎无状态 ------滑块的位置与速度存在 ArkTS 侧,每帧喂回、积分后返回,动画手感完全由 .so 内的物理决定,ArkTS 零本地插值;弹簧参数可覆盖 ------thumb 接受 stiffness / dampingRatio,自定义卡用 800 / 0.35 演示硬弹簧,默认值来自 info 握手(400 / 0.5)。

3.3 错误通道

导出整体 try/catch,Kotlin 异常永远不跨 FFI:

kotlin 复制代码
@CName("SegmentedControlDispatch")
fun segmentedControlDispatch(request: String): String {
    return try {
        val json = MiniJson.parse(request)
        when (json.str("action")) {
            "info" -> info()
            "validate" -> validate(json)
            /* ... spec / colors / thumb / nextIndex / safeIndex ... */
            else -> err("unknown action '${json.str("action")}'")
        }
    } catch (t: Throwable) {
        err(t.message ?: t::class.simpleName ?: "error")
    }
}

非法 JSON、未知 action、空段列表、越界索引,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进日志区。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
text 复制代码
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libsegmentedcontrol_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohossegmentedcontrol.so)
  │ SegmentedControlDispatch
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;让页面自己复刻规则又会让「so 里的引擎」变成摆设,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
kotlin 复制代码
@CName("SegmentedControlDispatch")
fun segmentedControlDispatch(request: String): String   // 7 action 一口进
C ABI 符号 ArkTS 方法 覆盖内核
SegmentedControlDispatch segmentedControlDispatch(req) 校验 / coerce / 导航 / 几何 / 配色 / spring 全部入口

与 KStore 的双符号不同,本库只有一个导出:action 字段即子协议,版本握手也在 info 里。

4.3 C++ N-API 方法分发
cpp 复制代码
napi_property_descriptor desc[] = {
    {"segmentedControlDispatch", nullptr, NapiSegmentedControlDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohossegmentedcontrol_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohossegmentedcontrol_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与一个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
cmake 复制代码
add_library(ohossegmentedcontrol SHARED IMPORTED)
set_target_properties(ohossegmentedcontrol PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(segmentedcontrol_napi SHARED napi_init.cpp)
target_link_libraries(segmentedcontrol_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohossegmentedcontrol)

OHOS_ARCH 为空或 libohossegmentedcontrol.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

typescript 复制代码
export const segmentedControlDispatch: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 测量与逐帧驱动

段的几何用 onAreaChange 回填(等价上游 onGloballyPositioned + positionInParent),滑块动画用 16ms 定时器逐帧调引擎(等价上游 LaunchedThumbEffect 里的两个 Animatable):

typescript 复制代码
private onSegArea(index: number, area: Area): void {
  const x = area.position.x as number;
  const w = area.width as number;
  this.segOffsets[index] = x + this.spec.trackPaddingDp;   // 滑块坐标系含轨道边距
  this.segWidths[index] = w;
  this.retarget();
}

private retarget(): void {
  const targetWidth = this.targetWidth();
  if (targetWidth <= 0) return;
  if (!this.measured) {
    // 首次真实测量------直接就位,不从 0 滑入(上游 snapTo 语义)
    this.thumbOffset = this.targetOffset();
    this.thumbWidth = targetWidth;
    this.measured = true;
    return;
  }
  /* 目标变化 → startSpring() */
}

private stepFrame(): void {
  const req = `{"action":"thumb","offset":${this.thumbOffset},"offsetV":${this.thumbOffsetV}` +
    `,"offsetTarget":${this.targetOffset()},"width":${this.thumbWidth},"widthV":${this.thumbWidthV}` +
    `,"widthTarget":${this.targetWidth()},"dtMs":16,"stiffness":${this.stiffness}` +
    `,"dampingRatio":${this.dampingRatio}}`;
  const parsed = JSON.parse(segmentedControlDispatch(req)) as ThumbResp;
  this.thumbOffset = parsed.offset;      // 每一帧都来自引擎积分
  this.thumbWidth = parsed.width;
  if (parsed.done) this.stopSpring();
}

分隔线规则也镜像上游:仅 iOS 风格、只画在两个未选中段之间、永不贴着选中段、最后一段之后不画。

5.2 页面交互

单页自上而下:握手标题区(fork / Kotlin 版本 + so 链路)、深色开关、iOS / Material3 / Pill 三张风格卡(Day|Week|Month)、内容模式卡(Text|Icon|Both 切换 + 图文 showcase)、宽度卡(Equal|Content + 长标签省略号演示)、禁用卡(B 段禁用 + 两级开关)、键盘导航卡(◀ ▶ 调 nextIndex,Drafts 段禁用)、自定义卡(自定义配色 + 12dp 圆角 + 硬弹簧)、Bridge 日志区(最近 12 条调用留痕,最新在前)。

5.3 ArkTS 状态边界

ArkUI 的 ForEach 按键差量重建,而段的外观依赖内容、禁用态、选中态、配色四个输入------键里必须把这些全部折叠进去,否则切选中段时文字颜色不刷新:

typescript 复制代码
private keyOf(item: SegmentDef, index: number): string {
  return `${item.label}|${item.icon}|${item.enabled ? 1 : 0}` +
    `|${index === this.selectedIndex ? 1 : 0}|${this.controlEnabled ? 1 : 0}|${this.dark ? 1 : 0}`;
}

另一个 ArkTS 专属的坑:组件成员不能叫 enabled------那会遮蔽通用属性方法 .enabled(value: boolean),CompileArkTS 直接报 10505001 类型不兼容,本仓改用 controlEnabled。

第 6 阶段:示例与验证

6.1 工程结构
text 复制代码
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohossegmentedcontrol)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 1 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.segmentedcontrol、versionName 0.1.0-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/libohossegmentedcontrol_api.h
        ├── src/main/cpp/types/libsegmentedcontrol_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(11 卡)
6.2 构建、签名和安装
bash 复制代码
./gradlew.bat :segmented-control:jvmTest                                   # 18 项交叉验证
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。

bash 复制代码
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.segmentedcontrol -m entry

四、完整代码对照

4.1 整体架构

text 复制代码
io.github.nadeemiqbal.segmentedcontrol/SegmentedControlEngine(规则内核)
    │ requireValidSegmentCount / safeIndex / nextEnabledIndex
    │ resolveStyleSpec / colors / blendArgb / springStep
src/composeMain/**(上游 Compose 原件,永不编译,演进基线)
Bridge.kt + MiniJson
    │ SegmentedControlDispatch(7 action)
libsegmentedcontrol_napi.so
    │ segmentedControlDispatch
Index.ets
    │ onAreaChange 测量 + 16ms JSON 往返 + @State 更新
    ▼
ArkUI 真机页面(单页滚动,11 卡)

4.2 文件清单

文件 职责
settings.gradle.kts 极简 ohos 构建、eazytec Maven 置前、ohos_segmented_control、sample 移出
gradle/libs.versions.toml 只剩 Kotlin 2.2.21-1.0.0 + multiplatform 插件别名
build.gradle.kts(根) 版本 0.1.0-ohos.1、全局禁用 Ohos*Test* 任务
segmented-control/build.gradle.kts 纯 KMP:jvm + ohosArm64 + ohosX64,无 Compose
.../src/commonMain/.../SegmentedControlEngine.kt 唯一新增主源码:校验 / 导航 / 几何 / 配色 / spring 内核
.../src/commonTest/.../SegmentedControlEngineTest.kt 18 项交叉验证
.../src/composeMain/ + .../src/composeTest/ 上游原件原样迁移,永不编译
example/nativeApp/build.gradle.kts 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt 1 个 C ABI、7 action 分发、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 验收页面(SegmentedControlView 组件 + 11 卡)
README.OpenHarmony_CN.md 鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次 API 作用
引擎 requireValidSegmentCount / safeIndex 空列表报错(原文逐字)/ 索引 coerce
引擎 nextEnabledIndex 键盘导航,跳过禁用段
引擎 resolveStyleSpec iOS / Material3 / Pill 几何表
引擎 colors / blendArgb Material3 基线 light/dark 六色,禁用色 38% 预混
引擎 springStep / springSettled MediumBouncy/MediumLow 一帧积分与收敛判定
Native SegmentedControlDispatch C ABI JSON 导出(7 action)
N-API segmentedControlDispatch(req) 向 ArkTS 暴露能力
ArkTS SegmentedControlView 等价重写的控件组件(测量 / 拾取 / 逐帧驱动)

4.4 ArkTS 与 Kotlin 的边界

typescript 复制代码
const next = JSON.parse(segmentedControlDispatch(
  '{"action":"nextIndex","enabled":"1,0,1","from":0,"direction":1}'));
// next.ok === true; next.index === 2 ------ Drafts 禁用,▶ 从 Inbox 落在 Sent

const frame = JSON.parse(segmentedControlDispatch(
  '{"action":"thumb","offset":0,"offsetV":0,"offsetTarget":120,' +
  '"width":96,"widthV":0,"widthTarget":96,"dtMs":16}'));
// frame.offset === 7.02 ------ 滑块第一帧由 so 内的 spring 积分决定
kotlin 复制代码
"nextIndex" -> {
    val enabled = flags(json.str("enabled"))
    if (enabled.isEmpty()) return err("enabled flags must not be empty")
    val from = SegmentedControlEngine.safeIndex(json.int("from", 0), enabled.size)
    val next = SegmentedControlEngine.nextEnabledIndex(enabled, from, json.int("direction", 1))
    "{\"ok\":true,\"index\":$next,\"from\":$from,\"changed\":${next != from}}"
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:提取规则内核,而不是等 Compose 对齐或让页面复刻规则

社区 Compose-for-ohos 制品线缺 foundation / material3 / ui,上游 7 个文件整库编译此路不通。另一个极端是让 ArkTS 自己写一遍滑块动画和配色------那 .so 就成了摆设,上游规则与鸿蒙表现无从对齐。中间路线是把「规则」与「像素」拆开:规则(校验、导航、几何、配色、物理)镜像进纯 Kotlin 内核编进 .so,像素由 ArkUI 等价重写。UI 库的适配面天然最大,这条拆分线是唯一能让规则真跑在 Native 里的路。

决策 2:Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(旧线配对)

内核零依赖,Kotlin 版本不必迁就 kotlinx 制品线,直接采用社区已过审的 2.2.21 钉法(qrose / swipe 同款)。Gradle 9.5.0 刻意降到 8.14.1------2.2.21 旧线的已验证配对;KStore 那批 2.4.20 新线可以留 Gradle 9.x,两条线的经验不能互套。降级唯一代价是 wrapper 属性一行,换来的是与已审仓库完全一致的构建行为。

决策 3:根构建替换(cmp_qrose 模式),sample 留盘

上游根构建(AGP / compose 插件 / vanniktech 发布 / wasm 样例)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample/ 五个示例工程(composeApp / androidApp / desktopApp / webApp / iosApp)需要 AGP 与 Jetpack Compose,留盘不进 settings.gradle.kts------它们是将来社区 CMP 对齐后恢复全平台构建的参照物。

决策 4:引擎无状态 + spring 逐帧跨桥

桥不保存任何会话:滑块的位置与速度由 ArkTS 持有、每帧喂回,thumb 返回积分后的新值。这样做的直接收益是动画手感完全由 .so 内的物理决定------ArkTS 一行插值都没有,MediumBouncy 的过冲、回弹、收敛全部是引擎算出来的;间接收益是桥天然线程安全、无生命周期问题。代价是动画期间每秒约 60 次 JSON 往返,实测真机完全无压力(单帧载荷不足 200 字节)。

决策 5:JVM 交叉验证 + composeMain 演进基线

内核逻辑在 commonMain,18 项 jvmTest 钉住校验文案、coerce、导航四例、几何数值、六色派生、spring 过冲与收敛。上游 Compose 原件不删不改编进 composeMain------它是「内核没漂」的对照组,也是社区 CMP 对齐后的恢复点:届时把源集移回编译、恢复 compose 插件条目即可,桥协议一行不用动。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Adoptium Temurin)、Gradle 8.14.1、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

bash 复制代码
./gradlew.bat :segmented-control:jvmTest

18 项用例全部通过:

分组 用例数 钉住什么
校验 2 空列表报错文案逐字一致("at least one item")、1/7 段放行
coerce 1 safeIndex(-3/99/2, 3) 收敛到 0/2/1,空列表回落 0
键盘导航 4 右移跳禁用、左移跳禁用、无可用邻居原地不动、两端不出界
几何 3 仅 iOS 显示分隔线;三风格边距/阴影/圆角数值表;风格名大小写与回退
配色 4 iOS 六色、M3/Pill 主色滑块与透明分隔线、dark 换 palette、38% 预混值与 hex 格式
spring 3 弹向 300 必过冲且 600 帧内收敛、已在目标时不动、起步单调逼近
元数据 1 info 握手的版本与风格清单

6.3 HAP 验证

text 复制代码
:segmented-control:jvmTest: BUILD SUCCESSFUL(18 项)
:segmented-control:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs
符号检查: SegmentedControlDispatch / libohossegmentedcontrol_symbols 均在 KN so 动态符号表中
BuildNativeWithCmake: finished(双 ABI 编出 libsegmentedcontrol_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 6.5 MB,内含双 ABI 各三件套:arm64-v8a 侧 libohossegmentedcontrol.so 1.9 MB、libc++_shared.so 1.3 MB、libsegmentedcontrol_napi.so 63 KB;x86_64 侧 1.8 MB / 1.3 MB / 65 KB(HAP 打包时已 strip;debugShared 未 strip 时 KN so 分别约 3.5 MB / 3.2 MB,含 Kotlin/Native 运行时)。

6.4 功能用例

  1. 启动后握手行显示 0.1.0-ohos.1 · Kotlin 2.2.21-1.0.0(来自真实 Native 调用,非静态文字),日志出现引擎版本与弹簧参数行;
  2. validate(0) 在日志里回显上游报错原话 SegmentedControl requires at least one item; the items list was empty.,safeIndex(99, 3) 回显 → 2------两条上游契约跨桥演示;
  3. iOS 卡:Day / Week / Month 点击切换,滑块带过冲弹性滑移,未选中段之间有细分隔线且永不贴着选中段;
  4. Material3 卡:滑块为 primary 紫色、平面无阴影、无分隔线;Pill 卡:滑块为体育场胶囊形;
  5. 打开深色开关,页内全部控件同时向引擎重新拉取 dark palette,轨道 / 滑块 / 文字 / 禁用色整体切换;
  6. 内容卡切到 Icon,showcase 控件变为三个纯图标段;切回 Both 恢复图文;宽度卡切 Content 后长标签 Recommended 完整显示,切回 Equal 省略号为 Recomme...;
  7. 禁用卡:B 段灰显、点击无回调、仍占位;关闭「Control enabled」后整控件全员灰显、滑块阴影消失;
  8. 导航卡按 ▶:选中从 Inbox 跳过禁用的 Drafts 落在 Sent,日志记录 nextIndex(dir=1) 0 → 2;自定义卡滑块以明显更硬的手感(800 / 0.35)移动。

6.5 验证结论

规则内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的控件会话:版本握手、三风格几何、深色配色派生、禁用语义、键盘导航与引擎驱动的滑块弹性动画(见下节效果图)。

七、运行效果

7.1 真机截图

7.2 命令速查

bash 复制代码
git clone https://atomgit.com/oh-tpc/segmented-control.git
cd segmented-control
export JAVA_HOME="C:/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
./gradlew.bat :segmented-control:jvmTest                                   # 18 项交叉验证
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp → 签名 → Run;或:
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.segmentedcontrol -m entry
hdc shell hilog | grep SegmentedControl

八、遗留问题与改进方向

8.1 踎坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :segmented-control 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 2.2.21 旧线必须配 Gradle 8.14.1------上游 wrapper 9.5.0 不降级,插件解析阶段就过不去;2.4.20 新线留 9.x 的经验不能套用;
  4. Windows 上 gradlew.bat 启动前必须显式 JAVA_HOME 指向 JDK 21,gradle.properties 里的 org.gradle.java.home 只管守护进程、救不了 wrapper 启动;
  5. compose 插件条目必须从版本目录整体移除,否则被排除的模块(如残留样例)一被 include 就配置失败------这是让 composeMain 永不编译的构建期保险;
  6. ArkTS 组件成员不能叫 enabled:遮蔽通用属性方法 .enabled(value: boolean) 会报 10505001(Type 'boolean' is not assignable to type '(value: boolean) => CommonAttribute'),改名 controlEnabled 即过;
  7. ArkUI ForEach 按键差量重建,段外观依赖内容 / 禁用 / 选中 / 配色四个输入,键必须全部折叠,否则切选中段时文字颜色不刷新;
  8. 通用 @Builder 没有插槽语义,卡片容器不能做成「带 children 的组件」,逐卡显式展开最稳;
  9. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝------仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前确认未把证书带进 AtomGit,entry/libs/ 与 .hvigor/、entry/build/ 都在 gitignore 里。

8.2 已知问题

  • Compose 版 API(SegmentedControl composable / SegmentItem(ImageVector) / SegmentedControlColors(Color))在鸿蒙侧不交付------交付物是 .so 内核 + NAPI 桥;
  • ImageVector 图标在 ArkTS demo 用 emoji 字形等价演示,视觉与 Material 图标有差异;
  • RTL 布局未在 demo 验收(ArkUI Row 会随系统语言翻转,上游 RTL 语义未逐项核对);
  • android / ios / desktop / wasmJs 样例与 vanniktech 发布链剥离(源文件留在树里),本 fork 不发 Maven Central;
  • ohos 目标无测试运行器,逻辑验证全部在 JVM;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.segmentedcontrol 匹配的签名。

8.3 未来优化方向

  • 等社区 CMP for HarmonyOS 补齐 foundation / material3 / ui 制品后,把 composeMain 移回编译,鸿蒙直接吃原生 Compose UI(桥协议不用动);
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充 DevEco 真机(x86_64)验收矩阵与 uitest 自动化截图;
  • 滑块动画可合并为批量的 thumbN 帧返回(一次 Native 调用积分 N 帧),进一步压低跨桥频次。

九、总结

9.1 核心难点回顾

text 复制代码
上游 segmented-control(7 个 Compose 文件)
    → 验证社区 CMP 制品线(foundation/material3/ui 缺 ohos 坐标,整库编译不通)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(旧线配对)
    → 根构建替换(cmp_qrose 模式,compose 插件整体移除)
    → 规则内核提取:校验 / 导航 / 几何 / 配色 / spring 物理 → commonMain
    → ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
    → C ABI(@CName × 1,7 action 扁平 JSON)
    → C++ N-API(DisposeString 纪律)
    → ArkTS 测量 + 16ms 引擎步进
    → ArkUI 真机单页验收(三风格 / 深色 / 禁用 / 导航 / 弹性滑块)

9.2 封装层次

  • 引擎层:校验与 coerce、键盘导航、样式几何表、Material3 基线配色(含 38% 预混)、spring 物理积分;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:SegmentedControlView 组件管理测量、拾取、逐帧驱动与卡片页面状态;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 路线跟着库的依赖形态走:KStore 那批库等到了 kotlinx ohos 制品线,正确答案是「真编译整库」;SegmentedControl 的全部价值站在 Compose UI 上,制品线缺失时唯一能让规则真跑在 Native 里的答案就是「内核提取 + UI 等价重写」------qrose / swipe / 本仓一条路,路线选择必须基于当天的制品可达性,而不是「别人整库编译成功了」的历史结论;
  2. 工具链线要分清新旧:2.2.21 旧线降 Gradle 8.14.1,2.4.20 新线留 9.x,两条线的配对经验不能互相套用------降不降 wrapper 由 Kotlin 线决定,与上游用什么 Gradle 无关;
  3. UI 库的交叉验证重点在「手感」:18 项 JVM 用例钉住数值只是基础,滑块动画必须逐帧来自引擎(ArkTS 零本地插值),过冲与回弹才是这条 spring 的语义------UI 库适配的说服力在动画手感与上游同源,而不是页面长得像。

9.4 适配成果

当前适配已完成:

  • SegmentedControl 0.1.0 规则内核的双 ABI Kotlin/Native 编译(校验文案、coerce、导航、几何数值、配色派生、spring 参数逐语义镜像上游,上游 Compose 原件原样保留为演进基线);
  • 1 个 C ABI 符号(7 action 扁平 JSON 分发)和 C++ N-API 包装层(动态符号表验证通过);
  • DevEco Stage ArkUI 单页验收页面(握手、三风格、深色、内容三态、宽度、禁用、导航、自定义、日志共 11 卡);
  • 18 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so,约 6.5 MB)、签名、设备安装和鸿蒙真机验证;
  • 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。

参考文档

相关推荐
miofly1 小时前
GitHub 日榜趋势速报 | 2026-10-30
开源·github
2401_882351523 小时前
开源鸿蒙平台 CMP 三方库「LlmTypewriter」适配全流程
华为·开源·harmonyos
wflynn10 小时前
GitHub 日榜趋势速报 | 2026-10-28
开源·github
孙琦Ray11 小时前
Meta 把 AI 网络 RDMA 传输层开源了
meta·开源·rdma·ai 网络·ocp 全球峰会·数据中心交换机
海盗123412 小时前
AI 新闻日报 10-28:Atria Dawn 完整版开源、Cursor Agent SDK 转正、Genie 4 实时世界模型
人工智能·机器人·开源·人工智能aigc
摩尔线程12 小时前
万卡万亿参数模型训练底座:摩尔线程开源Megatron-MUSA-Titan
开源·摩尔线程
冬奇Lab12 小时前
一天一个开源项目(第251篇):Claude SEO —— 装进 Claude Code 的 SEO 审计团队,26 个子技能 + 19 个专家 Agent 并行体检网站
人工智能·开源
2501_9481226312 小时前
开源鸿蒙平台 CMP 三方库「kmp-showcase-compose」适配全流程
华为·开源·harmonyos
OH_TPC12 小时前
HarmonyOS APP开发---“旅图“旅行摄影社区App,需要用到这个库
华为·harmonyos·鸿蒙