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 模拟器实测拖拽排序交互全部正常。

CMP Reorderable 首屏
先睹为快:DevEco 模拟器实测。七个彩色卡片列表,长按左侧三点把手即可拖拽重排,松手后列表实时更新顺序

一、先看清楚:reorderable 的源码是怎么分层的

照例先翻上游源码分布。这个库比想象中轻——它不依赖任何平台 API,核心逻辑全部在 commonMain:

上游源集内容对平台的依赖
commonMainReorderableItem / rememberReorderableLazyListState / draggableHandle / longPressDraggableHandle 全部 Compose 修饰符和状态管理纯 Compose,无平台依赖
androidMain / iosMain / desktopMain 等空实现或极薄的平台桥接(如 HapticFeedback)可选
build.gradle.ktsKMP 插件配置 + 发布配置无

关键观察:这个库没有 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。

DevEco Studio 运行中

三、适配过程:五个关键步骤

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 必须用同一个编译器版本。

修复:

  1. 把 _ref/reorderable/gradle/libs.versions.toml 的 kotlin 和 composeMultiplatform 改成 2.2.21-0.4.0 / 1.9.2-0.4.0
  2. cd _ref/reorderable && .\gradlew.bat :reorderable:clean :reorderable:publishToMavenLocal
  3. 回到 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/...")
        }
    }
}

然后:

  1. cd harmonyApp && ohpm install(拉取 @cpf-kmp-cmp/compose 等 ohpm 依赖)
  2. hvigor assembleHap(打包 HAP)
  3. hap-sign-tool.jar sign-app(离线签名,需要先生成三级证书链)
  4. 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

reorderable demo 首屏
图 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 鸿蒙社区:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐