Compose Multiplatform 拖拽排序库 reorderable 的 OpenHarmony 鸿蒙化适配实战
Compose Multiplatform 拖拽排序库 reorderable 的 OpenHarmony 鸿蒙化适配实战(Kotlin/Native 编译 libkn.so + CPF 统一渲染 + 模拟器实测)
库版本:sh.calvin.reorderable 3.1.0-1.0.0(鸿蒙切片)|验证环境:Kotlin 2.2.21-0.4.0(鸿蒙定制版)|Compose Multiplatform 1.9.2-0.4.0|DevEco Studio 26.0.0|DevEco 模拟器|HarmonyOS 7.0.0(API 26)

reorderable 是 Compose 生态里做拖拽排序最常用的库——一行 rememberReorderableLazyListState 就能让 LazyColumn 支持长按拖拽重排。但要在鸿蒙上用它,不是改个依赖版本就行:鸿蒙的 Compose 支持走 CPF(Compose Platform Framework) 路线,所有 KMP 库必须编译成 Kotlin/Native 的 .so,再通过 CPF 统一渲染管线(Skia)上屏到 ArkUI。本文记录我把 reorderable 完整跑上鸿蒙的全过程:从选对鸿蒙切片版本、Kotlin/Native 编译双 ABI 的 libkn.so、修掉一个 IR linker fake-override 崩溃,到 DevEco 模拟器实测拖拽排序交互全部正常。

先睹为快:DevEco 模拟器实测。七个彩色卡片列表,长按左侧三点把手即可拖拽重排,松手后列表实时更新顺序
一、先看清楚:reorderable 的源码是怎么分层的
照例先翻上游源码分布。这个库比想象中轻——它不依赖任何平台 API,核心逻辑全部在 commonMain:
| 上游源集 | 内容 | 对平台的依赖 |
|---|---|---|
commonMain | ReorderableItem / rememberReorderableLazyListState / draggableHandle / longPressDraggableHandle 全部 Compose 修饰符和状态管理 | 纯 Compose,无平台依赖 |
androidMain / iosMain / desktopMain 等 | 空实现或极薄的平台桥接(如 HapticFeedback) | 可选 |
build.gradle.kts | KMP 插件配置 + 发布配置 | 无 |
关键观察:这个库没有 jvmMain / nativeMain 的区分——它的全部代码都在 commonMain,靠 Compose 的跨平台抽象(LazyListState / Modifier.pointerInput / Modifier.detectDragGesturesAfterLongPress)抹平平台差异。这意味着鸿蒙适配的核心工作不是改库代码,而是让它在鸿蒙的 Kotlin/Native 工具链下编译通过。
二、工程结构
cmp-reorderable-demo/
├── composeApp/ # CMP 共享模块
│ ├── src/commonMain/kotlin/ # 业务 UI(App.kt,7 个可拖拽卡片)
│ ├── src/ohosMain/kotlin/ # 鸿蒙入口(MainArkUIViewController)
│ └── build.gradle.kts # ohosArm64/ohosX64 双目标 + linker 配置
├── harmonyApp/ # DevEco 鸿蒙应用工程
│ └── entry/src/main/ets/pages/ # ArkTS 页面,加载 libkn.so
├── gradle/libs.versions.toml # 版本目录(Kotlin 2.2.21-0.4.0 等)
└── _ref/reorderable/ # 本地发布的 reorderable 源码(版本对齐用)
composeApp:标准 CMP 模块,commonMain里写 UI,ohosMain里写鸿蒙入口;harmonyApp:DevEco 工程,通过 NAPI 加载libkn.so,把 Compose 渲染结果上屏到 ArkUI;_ref/reorderable:从 GitHub clone 的 reorderable 源码,本地修改版本后publishToMavenLocal。

三、适配过程:五个关键步骤
3.1 选对 reorderable 的鸿蒙切片版本
这是第一个坑。项目最初在 libs.versions.toml 里写的是 reorderable = "3.1.0",但 Gradle 解析直接失败——eazytec-cloud Nexus 上根本没有这个坐标。
我列出定制仓库里 sh.calvin.reorderable 的全部版本,发现可用的鸿蒙切片是 3.1.0-1.0.0:
sh.calvin.reorderable:reorderable:3.1.0-1.0.0
├── reorderable-ohosArm64-3.1.0-1.0.0.klib ✅
├── reorderable-ohosX64-3.1.0-1.0.0.klib ✅
├── reorderable-android-3.1.0-1.0.0.aar ✅
└── reorderable-iosarm64-3.1.0-1.0.0.klib ✅
但问题没这么简单。-1.0.0 后缀的 klib 是用 Kotlin 2.2.21-1.0.0 编译的,而 demo 工程用的是 Kotlin 2.2.21-0.4.0——版本后缀不一致会导致 Kotlin/Native IR linker 在链接阶段崩溃(IrFakeOverrideSymbol 相关,下文详述)。
解法:把 reorderable 源码 clone 到本地,修改 libs.versions.toml 里的 kotlin 和 composeMultiplatform 版本与 demo 对齐(都改成 -0.4.0),然后 publishToMavenLocal。
# _ref/reorderable/gradle/libs.versions.toml(修改后)
[versions]
kotlin = "2.2.21-0.4.0" # 原来是 2.2.21-1.0.0
composeMultiplatform = "1.9.2-0.4.0" # 原来是 1.9.2-1.0.0
// cmp-reorderable-demo/settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenLocal() // ← 启用本地仓库,优先于远程
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
}
}
经验:鸿蒙生态的 KMP 库版本号往往是
x.y.z-N.N.N这种定制切片,不能想当然写 upstream 版本号。更关键的是,所有参与链接的 klib 必须用同一个 Kotlin 编译器版本编译,否则 IR 层面的 ABI 不兼容,链接器直接崩溃。
3.2 用 Kotlin/Native 编译出双 ABI 的 libkn.so
鸿蒙的 Kotlin/Native target 是 ohosArm64(真机)和 ohosX64(模拟器)。在 composeApp/build.gradle.kts 里配置:
kotlin {
listOf(
ohosArm64(), // 真机 arm64
ohosX64() // 模拟器 x64
).forEach { ohosTarget ->
ohosTarget.binaries.sharedLib {
baseName = "kn"
export(libs.compose.multiplatform.export)
linkerOpts("-lz")
// CPF 统一渲染(Skia 后端)需要的系统库
linkerOpts(
"-lnative_drawing", // OH_Drawing_*(字体、绘制)
"-limage_source", // OH_ImageSourceNative_*(图像解码)
"-lpixelmap", // OH_PixelMap_*
"-lnative_window", // OH_NativeWindow_*
"-lace_napi.z", // N-API
"-lhilog_ndk.z", // HiLog 日志
"-lhitrace_ndk.z", // HiTrace 性能追踪
"-luv", // libuv 事件循环
"-lunwind", // 栈展开
"-licu", // ICU 文本处理
)
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(libs.reorderable) // ← 拖拽排序库
}
}
}
执行编译:
$env:JAVA_HOME = "C:\Users\nwu\.jdks-portable\jdk-17.0.13+11"
.\gradlew.bat :composeApp:linkDebugSharedOhosArm64 :composeApp:linkDebugSharedOhosX64
输出:
composeApp/build/bin/ohosArm64/debugShared/libkn.so (73 MB)
composeApp/build/bin/ohosX64/debugShared/libkn.so (71 MB)
composeApp/build/bin/ohosArm64/debugShared/libkn_api.h
composeApp/build/bin/ohosX64/debugShared/libkn_api.h
3.3 修掉 IR linker fake-override 崩溃
编译到 linkDebugSharedOhosArm64 阶段,直接崩溃:
error: java.lang.IllegalStateException: IrFakeOverrideSymbol for ...
at org.jetbrains.kotlin.ir.linkage.partial.PartialLinkageSupportForLinker ...
这是 Kotlin/Native 的 IR linker 在处理 klib 时,发现 reorderable 的 klib 和 demo 的 klib 用了不同版本的 Kotlin 编译器(2.2.21-1.0.0 vs 2.2.21-0.4.0),导致 fake-override 符号解析失败。
根因:CPF 鸿蒙定制版的 Kotlin 编译器在 -0.4.0 和 -1.0.0 之间有 ABI 变更(具体是 IrFakeOverrideSymbol 的序列化格式不同)。所有参与链接的 klib 必须用同一个编译器版本。
修复:
- 把
_ref/reorderable/gradle/libs.versions.toml的kotlin和composeMultiplatform改成2.2.21-0.4.0/1.9.2-0.4.0 cd _ref/reorderable && .\gradlew.bat :reorderable:clean :reorderable:publishToMavenLocal- 回到 demo 工程重新编译,链接通过
3.4 修复 Kotlin 编译错误 — longPressDraggableHandle() receiver 不匹配
编译 commonMain 时报错:
error: unresolved reference: longPressDraggableHandle
val handleModifier = this@ReorderableItem.longPressDraggableHandle()
ReorderableItem 是一个 @Composable lambda,其 receiver 是 ReorderableItemScope,但 longPressDraggableHandle() 是 Modifier 的扩展函数,不是 ReorderableItemScope 的成员。
修复:在 lambda 内直接用 Modifier.longPressDraggableHandle() 创建局部变量,不依赖 this@ReorderableItem:
ReorderableItem(reorderableState, key = item.id) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 8.dp else 1.dp, label = "elev")
// ✅ 正确:直接在 lambda 内调用 Modifier 扩展
val handleModifier = Modifier.longPressDraggableHandle()
TaskCard(
item = item,
isDragging = isDragging,
dragModifier = handleModifier,
modifier = Modifier.shadow(elevation, CardDefaults.shape),
)
}
经验:CMP 库里很多 Compose 修饰符是
Modifier扩展而不是作用域成员,写代码时注意区分 receiver。
3.5 把 libkn.so 发布到 harmonyApp 并打包签名
编译完成后,用 Gradle 任务把 .so 和头文件复制到 DevEco 工程:
// composeApp/build.gradle.kts
arrayOf("debug", "release").forEach { type ->
tasks.register<Copy>("publish${type.capitalizeUS()}BinariesToHarmonyApp") {
dependsOn("link${type.capitalizeUS()}SharedOhosArm64", "link${type.capitalizeUS()}SharedOhosX64")
into(rootProject.file("harmonyApp"))
from("build/bin/ohosArm64/${type}Shared/libkn.so") {
into("entry/libs/arm64-v8a/")
}
from("build/bin/ohosX64/${type}Shared/libkn.so") {
into("entry/libs/x86_64/")
}
from("build/bin/ohosArm64/${type}Shared/libkn_api.h") {
into("entry/src/main/cpp/include/arm64-v8a/")
}
// Compose 资源文件
from("src/commonMain/composeResources") {
into("entry/src/main/resources/rawfile/composeResources/...")
}
}
}
然后:
cd harmonyApp && ohpm install(拉取@cpf-kmp-cmp/compose等 ohpm 依赖)hvigor assembleHap(打包 HAP)hap-sign-tool.jar sign-app(离线签名,需要先生成三级证书链)hdc install entry-signed.hap
签名流程(无华为开发者账号,完全离线):
# 1. 生成 root CA
java -jar hap-sign-tool.jar generate-ca -keyAlias root_ca -keyAlg ECC -keySize NIST-P-384 \
-subject "C=CN,O=OpenHarmony,OU=OpenHarmony Team,CN=OpenHarmony Application Root CA" \
-keystoreFile rootCA.p12 -keystorePwd 123456 -outFile root_ca.cer
# 2. 生成 sub CA(root 签发)
java -jar hap-sign-tool.jar generate-ca -keyAlias sub_ca -keyAlg ECC -keySize NIST-P-384 \
-issuer "C=CN,O=OpenHarmony,OU=OpenHarmony Team,CN=OpenHarmony Application Root CA" \
-issuerKeyAlias root_ca -issuerKeystoreFile rootCA.p12 -issuerKeystorePwd 123456 \
-subject "C=CN,O=OpenHarmony,OU=OpenHarmony Team,CN=OpenHarmony Application CA" \
-keystoreFile subCA.p12 -keystorePwd 123456 -outFile sub_ca.cer
# 3. 生成 app 密钥对 + CSR + 证书链
java -jar hap-sign-tool.jar generate-keypair -keyAlias app_key -keyAlg ECC -keySize NIST-P-256 -keystoreFile app.p12 -keystorePwd 123456
java -jar hap-sign-tool.jar generate-csr -keyAlias app_key -subject "C=CN,O=OpenHarmony,OU=OpenHarmony Team,CN=OpenHarmony Application Release" -keystoreFile app.p12 -keystorePwd 123456 -outFile app.csr
java -jar hap-sign-tool.jar generate-app-cert -keyAlias app_key -issuerKeyAlias sub_ca -issuerKeystoreFile subCA.p12 -issuerKeystorePwd 123456 -keystoreFile app.p12 -keystorePwd 123456 -outForm certChain -outFile app_cert_chain.cer -rootCaCertFile root_ca.cer -subCaCertFile sub_ca.cer
# 4. 签名 profile(需要设备 UDID)
java -jar hap-sign-tool.jar sign-profile -mode localSign -keyAlias profile_key -profileCertFile profile_cert_chain.cer -inFile profile_debug.json -keystoreFile profile.p12 -keystorePwd 123456 -outFile profile_debug.p7b
# 5. 签名 HAP
java -jar hap-sign-tool.jar sign-app -mode localSign -keyAlias app_key -appCertFile app_cert_chain.cer -profileFile profile_debug.p7b -inFile entry-default-unsigned.hap -keystoreFile app.p12 -keystorePwd 123456 -outFile entry-signed.hap
四、使用 demo 演示接入
4.1 在 CMP 工程里引入
// composeApp/build.gradle.kts
commonMain.dependencies {
implementation("sh.calvin.reorderable:reorderable:3.1.0-1.0.0")
}
4.2 鸿蒙入口
// composeApp/src/ohosMain/kotlin/MainArkUIViewController.kt
@CName("MainArkUIViewController")
fun MainArkUIViewController(env: napi_env): napi_value {
initMainHandler(env)
return ComposeArkUIViewController(env) {
App() // 调用 commonMain 里的 @Composable
}
}
4.3 拖拽排序 UI 实现
// composeApp/src/commonMain/kotlin/App.kt
@Composable
fun ReorderableDemoScreen() {
var items by remember {
mutableStateOf(listOf(
TodoItem(1, "响应式布局适配", 0),
TodoItem(2, "统一渲染上屏验证", 1),
TodoItem(3, "三方库 reorderable 接入", 2),
// ... 共 7 项
))
}
val lazyListState = rememberLazyListState()
val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
items = items.toMutableList().apply {
add(to.index, removeAt(from.index))
}
}
LazyColumn(state = lazyListState) {
itemsIndexed(items, key = { _, item -> item.id }) { _, item ->
ReorderableItem(reorderableState, key = item.id) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 8.dp else 1.dp)
val handleModifier = Modifier.longPressDraggableHandle()
TaskCard(
item = item,
isDragging = isDragging,
dragModifier = handleModifier,
modifier = Modifier.shadow(elevation, CardDefaults.shape),
)
}
}
}
}
UI 设计要点:
- 每个卡片左侧有三点拖拽把手(
DragHandleDot),长按触发拖拽 - 拖拽时卡片 elevation 从 1dp 升到 8dp,产生浮起效果
- 卡片背景色从调色板循环取色,序号徽标用半透明背景
- 松手后
items列表实时更新,Compose 自动重组
五、验证效果
5.1 构建并安装
.\gradlew.bat :composeApp:publishDebugBinariesToHarmonyApp
cd harmonyApp
ohpm install
hvigor assembleHap
java -jar hap-sign-tool.jar sign-app ... # 签名
hdc install entry-signed.hap
hdc shell aa start -b ohos.cmp.reorderable -a EntryAbility
5.2 模拟器运行效果
图 0 DevEco Studio 开发环境:左侧项目结构 + 中间模拟器运行 CMP Reorderable + 底部 hdc 日志显示 successfully launched

图 1 demo 首屏:七个彩色卡片垂直排列,每个卡片左侧有三点拖拽把手,序号徽标+标题+副标题。Material 3 主题,深色文字在浅色背景上

图 2 拖拽中:长按" NAPI 桥接调试"卡片后拖动,该卡片浮起(elevation 8dp),下方"响应式布局适配"卡片自动让位,产生平滑的位移动画

图 3 拖拽完成:松手后列表更新为新顺序,"NAPI 桥接调试"和"发布到 AtomGit"已交换位置,序号徽标不变(key = item.id 保持 identity)
交互验证:
- 长按任意卡片左侧的三点把手 → 卡片浮起(elevation 8dp),可上下拖动
- 拖动到其他卡片位置 → 目标位置卡片自动让位,产生平滑的位移动画
- 松手 → 列表更新为新顺序,序号徽标不变(
key = item.id保持 identity)
六、FAQ:适配过程与使用问题
Q1:编译报 IrFakeOverrideSymbol 崩溃
所有参与链接的 klib 必须用同一个 Kotlin 编译器版本。检查每个依赖的 libs.versions.toml,确保 kotlin 和 composeMultiplatform 版本后缀一致(如都是 -0.4.0)。不一致的库需要本地重新编译发布。
Q2:编译报 unresolved reference: longPressDraggableHandle
longPressDraggableHandle() 是 Modifier 的扩展函数,不是 ReorderableItemScope 的成员。不要在 this@ReorderableItem 上调用,直接在 lambda 内 Modifier.longPressDraggableHandle()。
Q3:hvigor 打包报 spawn java ENOENT
DevEco Studio 的 hvigor 需要 Java 环境,但 Windows 上 DevEco 不自动把 JDK 加入 PATH。在 build_hap.bat 里显式加:
set PATH=C:\Users\<user>\.jdks-portable\jdk-17.0.13+11\bin;%PATH%
hvigor assembleHap
Q4:ohpm install 静默失败
如果 ohpm install 之前执行了 ohpm config get,后者会读取 stdin 导致 bat 脚本中断。去掉 config get,直接执行 ohpm install。
Q5:模拟器上拖拽不生效
确认 reorderableState 的 onMove lambda 里正确更新了列表:
val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
items = items.toMutableList().apply {
add(to.index, removeAt(from.index)) // ← 必须真正修改列表
}
}
Q6:libkn.so 太大(70+ MB)
Release 模式可以开启优化:
if (buildType == NativeBuildType.RELEASE) {
optimized = true // 注意:DevirtualizationAnalysis 可能 OOM,需增加 Gradle 内存
}
同时在 gradle.properties 里加:
kotlin.native.memory.allocator=std
org.gradle.jvmargs=-Xmx8g
七、相关链接
欢迎加入 CPF-KMP-CMP 鸿蒙社区:
- CPF-KMP-CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP
- CMP 鸿蒙开发环境搭建指南:https://atomgit.com/CPF-KMP-CMP/cmp-docs
- 本次配套仓库:https://atomgit.com/oh-tpc/reorderable
- reorderable 原始项目:https://github.com/Calvin-LL/Reorderable
- 问题反馈:https://atomgit.com/oh-tpc/reorderable/issues
更多推荐




所有评论(0)