本文记录
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 提供一个真实的控件会话,对应桥接的全部入口:
- 版本握手 :
infoaction 返回 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 功能用例
- 启动后握手行显示
0.1.0-ohos.1 · Kotlin 2.2.21-1.0.0(来自真实 Native 调用,非静态文字),日志出现引擎版本与弹簧参数行; validate(0)在日志里回显上游报错原话SegmentedControl requires at least one item; the items list was empty.,safeIndex(99, 3)回显→ 2------两条上游契约跨桥演示;- iOS 卡:Day / Week / Month 点击切换,滑块带过冲弹性滑移,未选中段之间有细分隔线且永不贴着选中段;
- Material3 卡:滑块为 primary 紫色、平面无阴影、无分隔线;Pill 卡:滑块为体育场胶囊形;
- 打开深色开关,页内全部控件同时向引擎重新拉取 dark palette,轨道 / 滑块 / 文字 / 禁用色整体切换;
- 内容卡切到 Icon,showcase 控件变为三个纯图标段;切回 Both 恢复图文;宽度卡切 Content 后长标签
Recommended完整显示,切回 Equal 省略号为Recomme...; - 禁用卡:B 段灰显、点击无回调、仍占位;关闭「Control enabled」后整控件全员灰显、滑块阴影消失;
- 导航卡按 ▶:选中从 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 踎坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:segmented-control真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 2.2.21 旧线必须配 Gradle 8.14.1------上游 wrapper 9.5.0 不降级,插件解析阶段就过不去;2.4.20 新线留 9.x 的经验不能套用;
- Windows 上
gradlew.bat启动前必须显式JAVA_HOME指向 JDK 21,gradle.properties里的org.gradle.java.home只管守护进程、救不了 wrapper 启动; - compose 插件条目必须从版本目录整体移除,否则被排除的模块(如残留样例)一被 include 就配置失败------这是让
composeMain永不编译的构建期保险; - ArkTS 组件成员不能叫
enabled:遮蔽通用属性方法.enabled(value: boolean)会报 10505001(Type 'boolean' is not assignable to type '(value: boolean) => CommonAttribute'),改名controlEnabled即过; - ArkUI
ForEach按键差量重建,段外观依赖内容 / 禁用 / 选中 / 配色四个输入,键必须全部折叠,否则切选中段时文字颜色不刷新; - 通用
@Builder没有插槽语义,卡片容器不能做成「带 children 的组件」,逐卡显式展开最稳; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝------仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,提交前确认未把证书带进 AtomGit,entry/libs/与.hvigor/、entry/build/都在 gitignore 里。
8.2 已知问题
- Compose 版 API(
SegmentedControlcomposable /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 三条经验
- 路线跟着库的依赖形态走:KStore 那批库等到了 kotlinx ohos 制品线,正确答案是「真编译整库」;
SegmentedControl的全部价值站在 Compose UI 上,制品线缺失时唯一能让规则真跑在 Native 里的答案就是「内核提取 + UI 等价重写」------qrose / swipe / 本仓一条路,路线选择必须基于当天的制品可达性,而不是「别人整库编译成功了」的历史结论; - 工具链线要分清新旧:2.2.21 旧线降 Gradle 8.14.1,2.4.20 新线留 9.x,两条线的配对经验不能互相套用------降不降 wrapper 由 Kotlin 线决定,与上游用什么 Gradle 无关;
- UI 库的交叉验证重点在「手感」:18 项 JVM 用例钉住数值只是基础,滑块动画必须逐帧来自引擎(ArkTS 零本地插值),过冲与回弹才是这条 spring 的语义------UI 库适配的说服力在动画手感与上游同源,而不是页面长得像。
9.4 适配成果
当前适配已完成:
SegmentedControl0.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。