开源鸿蒙平台 CMP 三方库「kmp-showcase-compose」适配全流程

本文记录 kmp-showcase-compose 接入 OpenHarmony 的完整过程,覆盖引导库盘点、纯 Compose UI 库的内核移植边界、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

kmp-showcase-compose 是基于 Compose Multiplatform 的新手引导(onboarding showcase)库:ShowcaseController 驱动一串 ShowcaseStep,全屏遮罩按步骤挖孔高亮目标控件(圆 / 圆角矩形 / 矩形),可选水波纹 / 脉冲圆呼吸动画,Tooltip 气泡四方向智能定位并带箭头指向锚点,点击气泡按钮或高亮区前进步骤。本次适配与社区已有两条路线都不同:qrose / haze / PreCompose 走"零依赖内核提取",KStore 走"ohos 制品线齐备后的真编译整库"------而 kmp-showcase-compose 是纯 Compose UI 库 (全库 commonMain 都 import androidx.compose.*,ohos 上既没有 Compose 编译线也没有可绕开的纯引擎),因此走第三条路:内核按原包名移植 + Compose 值类型替换 + 几何在内核算、像素在 ArkTS 画 。状态机、步骤模型、气泡定位、气泡形状的运算逻辑与上游逐字一致,运行在真机的 .so 里;ArkUI 页面作为验收宿主持零引导状态。

项目地址: AtomGit/oh-tpc/kmp-showcase-compose

上游仓库: suwasto/kmp-showcase-compose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

kmp-showcase-compose 为 Compose Multiplatform 应用提供"高亮引导"这一件事的完整答案:rememberShowcaseController() 一行拿控制器,ShowcaseHost(controller) { ... } 包住整页,Modifier.captureBounds { } 上报目标控件矩形,ShowcaseStep 声明样式与内容,controller.start(steps) 开跑。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要这套新手引导能力,但上游构建(Kotlin 2.3.0 / Compose 1.9.3 / AGP 8.13.2 / 新版 com.android.kotlin.multiplatform.library 插件)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明引导状态机、挖孔几何、气泡定位在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍 具体问题
Compose 编译线缺失 全库源码 import androidx.compose.*;社区 HarmonyOS CMP 停在 1.9.2 / Kotlin 2.2.21 线,与上游 Compose 1.9.3 / Kotlin 2.3.0 对不齐,@Composable 无法编进 ohosArm64 / ohosX64。
没有可绕开的纯引擎 不像 qrose 有纯 Kotlin 编码器、KStore 有纯 Kotlin 存储引擎------本库 19 个源文件里 8 个重度 Compose,其余也普遍沾染 Dp / Color / Rect 值类型。
目标缺失 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 动态库。
工具链不一致 上游 Gradle 8.14.3 / Kotlin 2.3.0 与鸿蒙 Kotlin 2.2.21-1.0.0 无法共存于一个 Gradle 构建。
内容是 Composable lambda ShowcaseStep.content: @Composable (Rect) -> Unit 是内核与渲染的结构性接缝,C ABI 过不了 lambda。
交付链路复杂 .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:内核(状态机 / 步骤模型 / 样式常量 / 气泡定位 / 气泡形状 / 命中测试)的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。引导语义完全由上游原版实现(移植件运算符级一致)负责,ArkTS 只负责调用 Native、解析 JSON 和绘制结果。

1.2 库提供的能力

Demo 提供一个真实的引导会话,对应桥的全部入口:

  • 启动(start) :controller.start(steps) 重建会话回到第 0 步------ArkTS 上报目标矩形与实测内容尺寸,内核 inflate、算挖孔、摆气泡、解箭头;
  • 点击(tap) :落在膨胀后高亮矩形内且该步 clickAdvances 才前进------对应上游 detectTapGestures + rect.contains(tapOffset) + step.onClickHighlight();
  • 前进(next) :气泡 / 卡片按钮触发 controller.next(),越界自动 end() 清空;
  • 结束 / 状态(finish / state):手动结束或重渲染当前步;
  • 版本握手(version) :0.1.0-ohos.1 来自真实 Native 调用而非静态文字。

响应携带完整渲染模型:

字段组 作用
dim 暗场颜色 / 动画开关与时长 / 高亮中心 / 到四角最大半径 / 状态栏裁剪高度------对应上游 Animatable + saveLayer + 缩放圆生长。
cutout 挖孔形状(circle / rounded / rect)、矩形、圆角、半径------对应上游 drawCutout(BlendMode.Clear)。
highlight 水波纹(环数 3、环间距 0.3、基透明度 0.4)或脉冲(0.7↔1.3、透明度 0.3)的全部常量------对应上游 rememberInfiniteTransition。
content tooltip:气泡多边形顶点(圆角采样 + 箭头三角形)、文案区原点与尺寸;card:居中卡片矩形、内边距、入场延迟与初始缩放。

引擎侧完整能力(状态机生命周期、三种样式、四方向定位、窗口钳制、箭头钳制、半开区间命中)全部随移植内核进入 .so,JVM 侧保留 14 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的引导逻辑没有漂移。

1.3 实现适配

维度 要求
代码复用 内核按原包名移植进 example/nativeApp/commonMain(io.github.suwasto.showcasecompose.core/render/tooltip),纯 Kotlin 文件逐字复制,Compose 值类型文件仅替换类型不改算术;上游 showcasecompose / composeApp / iosApp 模块源码零改动留盘。
平台目标 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,example/nativeApp 出双 ABI 动态库。
构建隔离 根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose 模块留盘不进 settings.gradle.kts。
桥接稳定 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;lambda 不跨 FFI(onClickHighlight 语义收敛为布尔,content 收敛为声明式规格);异常永远不跨 FFI。
依赖纪律 桥模块 commonMain.dependencies 为空,手写 MiniJson,.so 除 KN runtime 外零第三方依赖。
UI 完整 单 App 覆盖全部桥接入口:四种引导模式(STANDAR / ANIMATED DIM / WATERDROP RIPPLE / PULSING CIRCLE)、双高亮目标(搜索图标 / Bag 按钮)、气泡与卡片两种内容、Replay 回放。
仓库规范 项目说明、文章、效果图和源码链接统一使用 AtomGit,全仓不出现其他托管平台链接。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。引导的渲染层(ShowcaseOverlay / Tooltip / ShowcaseHost)没有编进 ohos,ArkTS 按内核下发的几何模型重绘------这是"几何在内核、像素在 ArkTS"的分工,与 qrose"像素在 KN 光栅化 PNG"相反,原因见决策 5。

二、实现路线图

text 复制代码
第 0 阶段:查重            ── oh-tpc 组织内无本库鸿蒙版本;与已收录 ShowcaseLayoutCompose
                              (布局容器内嵌消息型引导)API 面不重叠,非重复收录
第 1 阶段:项目初始化      ── 19 个源文件逐个盘点 Compose 依赖度、确定"逐字 / 改类型 / 重绘"三分法
第 2 阶段:根构建替换      ── cmp_qrose 模式、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1
第 3 阶段:内核移植        ── core/render/tooltip 原包名移植 + Dp/Color/Rect 值类型替换 + 气泡多边形化
第 4 阶段:桥协议建模      ── start/tap/next/finish/state/version JSON 契约、渲染模型、错误通道
第 5 阶段:C ABI → N-API   ── @CName × 2、CMake IMPORTED so、napi_init.cpp 包装、DisposeString 纪律
第 6 阶段:ArkUI 宿主      ── 假商城 UI + 四模式引导 + Canvas 遮罩/气泡 + 卡片入场 + 真机验收

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
text 复制代码
showcasecompose/                   上游 Compose 库(留盘不进构建,源码零改动)
composeApp/ iosApp/ assets/        上游示例 App 与素材(留盘不进构建)
example/nativeApp/                 Kotlin/Native 动态库(内核移植 + bridge + 测试)
example/harmonyApp/                DevEco Stage、CMake N-API 和 ArkTS 页面
README.md / README.upstream.md     fork 主页与上游原文
README.OpenHarmony.md / _CN.md     鸿蒙适配说明(中英双份)
doc/                               本文与效果图

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

1.2 工具链矩阵
项目 配置
Kotlin Multiplatform HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / haze / ShowcaseLayoutCompose 同线)
Gradle 8.14.1(上游 wrapper 8.14.3 降至社区已审线)
JDK 21(org.gradle.java.home 钉 F:\cmp\.tools\jdk-21.0.12.1+1)
OpenHarmony 目标 ohosArm64 + ohosX64(jvm() 保留作交叉验证)
DevEco product 5.1.0(18) 兼容
DevEco Studio 26.0.0 Release
ABI arm64-v8a + x86_64
Bundle org.terminator.ohos.kmpshowcase
powershell 复制代码
$env:JAVA_HOME = "F:\cmp\.tools\jdk-21.0.12.1+1"
java -version
1.3 源码依赖度盘点(本适配的成败手)

上游库 19 个 .kt 逐个看 import,得出三分法:

处置 文件 Compose 依赖形态
逐字复制(零 import) tooltip/TooltipDirection.kt、tooltip/ArrowAlignment.kt 枚举,纯 Kotlin(后者上游即死代码,保留以对齐公共 API 面)
改类型移植(算术逐字一致) core/ShowcaseShape.kt、core/ShowcaseUtils.kt、core/ShowcaseState.kt、core/ShowcaseController.kt、core/ShowcaseStep.kt、tooltip/TooltipBubbleStyle.kt、tooltip/ReportingTooltipPositionProvider.kt → TooltipPositionMath.kt Dp→px Float、Color→打包 ARGB Long、Rect→本地移植、mutableStateListOf/mutableStateOf→普通集合、PopupPositionProvider 接口→纯函数、@Composable 工厂/lambda 删除或规格化
拆出纯模型 render/ShowcaseOverlay.kt 的 ShowcaseStyle → render/ShowcaseStyle.kt sealed interface 本身是纯数据,只是与渲染器同文件
几何移植 + ArkTS 重绘 tooltip/TooltipBubbleShape.kt → tooltip/BubbleGeometry.kt;ShowcaseOverlay 的 dim/cutout/ripple/pulse 常量 → bridge/ShowcaseGeometry.kt Shape/Path/Outline、Animatable/Canvas 换成多边形顶点与参数下发
仅 ArkTS 重绘(不进内核) render/ShowcaseHost.kt、render/ShowcaseOverlay.kt(渲染半)、tooltip/Tooltip.kt、tooltip/TooltipBubble.kt、tooltip/TooltipUtils.kt、modifier/ShowcaseModifier.kt 全 Compose 布局 / 绘制 / 手势

两个关键盘点结论:其一,全库 Compose 快照态只有 4 个符号 (mutableStateListOf / mutableStateOf / getValue / setValue),集中在 ShowcaseState.kt------内核化改造成本极低;其二,ShowcaseStep.content: @Composable (Rect) -> Unit 是唯一的结构性接缝,上游示例只用它构建 Tooltip 气泡与居中卡片两种内容,规格化后即可完整跨桥。

1.4 页面边界
text 复制代码
Landing 页        四按钮(STANDAR / ANIMATED DIM / WATERDROP RIPPLE / PULSING CIRCLE)+ 版本握手行
Home 页           FashionStore 假界面:顶栏(☰ / 标题 / 🔍 搜索图标=目标1)、分类 chips、
                  秋装 banner、2 列商品瀑布流、底部导航(Bag 居中=目标2)
引导覆盖层        全屏 Canvas(暗场 / 挖孔 / 波纹 / 脉冲 / 气泡多边形)+ 文案层(气泡文字)+
                  卡片层(居中卡片,600ms 后淡入放大)+ 结束后的 Replay/Back
交互              布局就绪自动 op=start;点 Next/Got it! 前进;点高亮区 op=tap;结束后 Replay

页面启动时先调 kmpShowcaseVersion() 做握手,onAreaChange 上报两个目标矩形后自动开始引导。

第 2 阶段:根构建替换与双 ABI 打通

2.1 为什么停在 Kotlin 2.2.21-1.0.0 老线

本库没有 kotlinx 依赖(commonMain 只依赖 Compose 制品),不存在 KStore 那种"制品线逼着升 2.4.20"的动力;反过来,Compose 编译线在鸿蒙社区仍停在 CMP 1.9.2 / 2.2.21 线,升到新线也编不进 @Composable。因此沿用社区已审钉法:Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(上游 wrapper 8.14.3 降一档,对齐 qrose / ShowcaseLayoutCompose 的已审组合)。

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

include(":example:nativeApp")

gradle/libs.versions.toml 从 48 行收缩到 5 行,只剩 kotlin-multiplatform = 2.2.21-1.0.0。

2.2 nativeApp:双 ABI sharedLib + 回填
kotlin 复制代码
kotlin {
    applyDefaultHierarchyTemplate()
    jvm()                                   // 交叉验证用
    ohosArm64 { binaries.sharedLib { baseName = "ohoskmpshowcase" } }
    ohosX64  { binaries.sharedLib { baseName = "ohoskmpshowcase" } }
}

tasks.matching { it.name == "linkDebugSharedOhosArm64" }.configureEach {
    finalizedBy("copyKnSoArm64")            // .so + libc++_shared.so → harmonyApp/entry/libs/arm64-v8a
}

实测:

bash 复制代码
./gradlew.bat :example:nativeApp:jvmTest                          # 14 项全绿
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# arm64 → libohoskmpshowcase.so 约 4.3 MB;x86_64 约 3.9 MB(debugShared 未 strip)

测试纪律与 KStore 同款:测试统一 JVM 跑,根构建全局禁用 Ohos*Test* 任务。

第 3 阶段:桥协议建模

3.1 JSON 契约
text 复制代码
io.github.suwasto.showcasecompose/**(移植内核,原包名)
        ↓ ShowcaseState / ShowcaseController / TooltipPositionMath / BubbleGeometry
ShowcaseSession + ShowcaseDispatch(扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API kmpShowcaseVersion() / kmpShowcaseDispatch()
        ↓
ArkUI Index.ets(Landing + Home + 覆盖层)

请求与响应:

text 复制代码
请求 : {"op":"version"}
       {"op":"start","window":{...},"statusBarPx":132,"tooltipMarginPx":16,"steps":[...]}
       {"op":"tap","x":540,"y":2150}
       {"op":"next"} / {"op":"finish"} / {"op":"state"}
响应 : {"ok":true,"op":"...","active":true,"index":0,"count":2,
        "dim":{...},"cutout":{...},"highlight":{...}|null,"content":{...}|null}
       {"ok":false,"error":"..."}

两个不对称设计:尺寸上行、几何下行 ------文字宽高由 ArkTS 用 @ohos.measure 实测后随 contentWidthPx/contentHeightPx 上传(对应 Compose 里 Popup 对气泡的实测布局角色),挖孔 / 气泡多边形 / 卡片位置全部由内核算好下行,ArkTS 拿到什么画什么;渲染模型整帧下行------每次交互响应都携带完整渲染模型,ArkTS 零引导状态,换步即重绘。

3.2 Composable 接缝的规格化

onClickHighlight: () -> Unit = {} 收敛为 clickAdvances: Boolean------上游示例只有 {} 与 { controller.next() } 两种取值,布尔无损承载。content: @Composable (Rect) -> Unit 收敛为声明式规格:

kotlin 复制代码
sealed class StepContent {
    data class Tooltip(direction, title, body, counter, action,
                       style: TooltipBubbleStyle, contentWidthPx, contentHeightPx) : StepContent()
    data class Card(title, body, counter, action, appearDelayMillis,
                    contentWidthPx, contentHeightPx, innerPaddingPx) : StepContent()
}

lambda 不跨 C ABI,但引导语义一步没少。

3.3 错误通道

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

kotlin 复制代码
@CName("KmpShowcaseDispatch")
fun kmpShowcaseDispatch(request: String): String = ShowcaseDispatch.handle(request)

fun handle(request: String): String = try { dispatchOrThrow(request) }
    catch (t: Throwable) { ShowcaseSession.errorBody(t.message ?: t.toString()) }

非法 JSON、缺 op、未知 op、无会话先于 start、start 空步骤,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。

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

4.1 三层桥接
text 复制代码
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libkmpshowcase_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohoskmpshowcase.so)
  │ KmpShowcaseDispatch / KmpShowcaseVersion
  ▼
UTF-8 JSON + DisposeString
4.2 Kotlin/Native 导出函数
kotlin 复制代码
@CName("KmpShowcaseVersion")
fun kmpShowcaseVersion(): String = ShowcaseSession.FORK_VERSION   // "0.1.0-ohos.1"

@CName("KmpShowcaseDispatch")
fun kmpShowcaseDispatch(request: String): String = ShowcaseDispatch.handle(request)
C ABI 符号 ArkTS 方法 覆盖内核
KmpShowcaseDispatch kmpShowcaseDispatch(req) 会话状态机 + start / tap / next / finish / state 全部分发
KmpShowcaseVersion kmpShowcaseVersion() fork 版本握手(Landing 页版本行的数据来源)
4.3 C++ N-API 方法分发
cpp 复制代码
napi_property_descriptor desc[] = {
    {"kmpShowcaseDispatch", nullptr, NapiKmpShowcaseDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"kmpShowcaseVersion", nullptr, NapiKmpShowcaseVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskmpshowcase_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskmpshowcase_api.h 从 linkDebugSharedOhosArm64 生成的头手工拷入(只声明 ExportedSymbols 结构与两个业务符号)。

4.4 CMake 和类型声明
cmake 复制代码
add_library(ohoskmpshowcase SHARED IMPORTED)
set_target_properties(ohoskmpshowcase PROPERTIES
  IMPORTED_LOCATION ${KN_SO}          # entry/libs/${OHOS_ARCH}/libohoskmpshowcase.so
  IMPORTED_NO_SONAME TRUE)

add_library(kmpshowcase_napi SHARED napi_init.cpp)
target_link_libraries(kmpshowcase_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoskmpshowcase)

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

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

第 5 阶段:ArkUI 宿主

5.1 目标上报与内容实测
typescript 复制代码
Text('🔍')
  .onAreaChange((_o: Area, n: Area) => {
    this.searchRectPx = this.rectFromArea(n);   // globalPosition(vp) × density → 窗口 px
    this.maybeStart();                          // 双目标就绪即 op=start
  })

private textWidthVp(text: string, fontSize: number, weight: number): number {
  const opts: TextMeasureOpts = { textContent: text, fontSize: `${fontSize}fp`,
    fontWeight: weight, fontFamily: 'sans-serif' };
  return measure.measureText(opts);             // @ohos.measure,气泡/卡片内容实测
}

Area.globalPosition 是 Length 类型,严格 ArkTS 禁止直接算术,先经 lengthToVp() 归一为 number;文字按贪心分词 + 1.5 倍行高估算折行高度,对应上游 Column(padding 12) + title(24sp) + gap16 + body(16sp) + gap24 + counter 行 的内容度量。

5.2 覆盖层绘制

Canvas 承担全部连续像素:暗场(动画期画"半径 × smoothstep(progress)"的生长圆,等价上游 translate/scale 包裹 drawCircle(maxRadius),到 1 切全屏矩形并按 clipTopPx 裁掉状态栏);挖孔用 globalCompositeOperation='destination-out' 对应上游 BlendMode.Clear;水波纹第 i 环 (t/duration + i×0.3) % 1、半径 base + p×maxRadiusPx、透明度 0.4×(1−p);脉冲缩放因子 0.7↔1.3 往返。气泡多边形(内核 BubbleGeometry 圆角 8 段采样 + 箭头三角形)在 Canvas 填充,文字用 Text 组件按 textX/textY/textWidth 绝对定位盖在上面;卡片层 600ms 后 animateTo 淡入放大(初始缩放 0.8 来自上游 scaleIn(initialScale = 0.8f))。

5.3 交互与错误边界

覆盖层 Canvas 的 onClick 把 e.x × density 换算成窗口 px 发 op=tap;点气泡 Next / 卡片按钮走 op=next。ok:false 时错误原文进 hilog;Native 调用本身抛错同样被 try/catch 收进日志。

第 6 阶段:示例与验证

6.1 工程结构
text 复制代码
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoskmpshowcase)+ copyKnSo*
│   └── src/
│       ├── commonMain/.../showcasecompose/
│       │   ├── core/                         Rect / Shape / Utils / State / Controller / Step(移植)
│       │   ├── render/                       ShowcaseStyle(拆出)+ StepContent(规格)
│       │   ├── tooltip/                      Direction·Alignment(逐字)/ BubbleStyle /
│       │   │                                 TooltipPositionMath / BubbleGeometry(移植)
│       │   └── bridge/                       MiniJson / ShowcaseModel / ShowcaseGeometry /
│       │                                     ShowcaseSession + ShowcaseDispatch
│       ├── commonTest/.../ShowcaseBridgeTest.kt   14 项交叉验证
│       └── ohosMain/kotlin/Bridge.kt         @CName × 2
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.kmpshowcase、0.1.0-ohos.1
    └── entry/
        ├── build-profile.json5               externalNativeOptions + abiFilters [arm64-v8a, x86_64]
        ├── libs/{arm64-v8a,x86_64}/          copyKnSo* 回填三件套(gitignore)
        ├── src/main/cpp/                     CMakeLists / napi_init.cpp / api.h / types 声明包
        └── src/main/ets/                     EntryAbility(避让区高度)+ pages/Index.ets
6.2 构建、签名和安装
bash 复制代码
./gradlew.bat :example:nativeApp:jvmTest                                       # 14 项交叉验证
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
ohpm install
hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon

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

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.kmpshowcase -m entry

四、完整代码对照

4.1 整体架构

text 复制代码
io.github.suwasto.showcasecompose.core/**(移植内核:状态机 / 步骤 / 几何基元)
    │ ShowcaseState / ShowcaseController / Rect.inflate
io.github.suwasto.showcasecompose.render/**(样式模型 + 内容规格)
    │ ShowcaseStyle / StepContent
io.github.suwasto.showcasecompose.tooltip/**(移植内核:定位与气泡形状)
    │ TooltipPositionMath / BubbleGeometry / TooltipBubbleStyle
bridge/**(ShowcaseModel 解析 + ShowcaseGeometry 渲染模型 + ShowcaseSession 会话)
    │ KmpShowcaseDispatch / KmpShowcaseVersion
libkmpshowcase_napi.so
    │ kmpShowcaseDispatch / kmpShowcaseVersion
Index.ets
    │ JSON.parse + Canvas / Text 绘制
    ▼
ArkUI 真机页面(Landing 四模式 + FashionStore 双目标引导)

4.2 文件清单

文件 职责
settings.gradle.kts 极简 ohos 构建、eazytec Maven 置前、ohos_kmpshowcasecompose、Compose 模块移出
gradle/libs.versions.toml 收缩为 Kotlin 2.2.21-1.0.0 单插件
build.gradle.kts(根) 版本 0.1.0-ohos.1、全局禁用 Ohos*Test* 任务
gradle.properties JDK 21 钉版、konan 开关(新增入库)
nativeApp/.../core/(6 文件) 上游内核移植:Rect 补位 + Shape/Utils/State/Controller/Step
nativeApp/.../render/(2 文件) ShowcaseStyle 自 Overlay 拆出 + StepContent 规格
nativeApp/.../tooltip/(5 文件) Direction/Alignment 逐字 + BubbleStyle/PositionMath/BubbleGeometry 移植
nativeApp/.../bridge/(4 文件) MiniJson 手写 JSON / Model 解析 / Geometry 渲染模型 / Session+Dispatch
nativeApp/.../ohosMain/Bridge.kt 2 个 C ABI 绑定(唯一 ohos 源文件)
nativeApp/.../commonTest/ShowcaseBridgeTest.kt 14 项 JVM 交叉验证
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 四模式验收页面
README.OpenHarmony_CN.md 鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次 API 作用
内核 ShowcaseState.addStep/next/end/isActive 引导生命周期(越界自动清空)
内核 ShowcaseController.start(steps) 重建会话回到第 0 步
内核 Rect.inflate(padding) 高亮矩形四边外扩(命中与挖孔共用)
内核 TooltipPositionMath.calculate 四方向原始偏移 + 窗口钳制 + 箭头中心解算
内核 BubbleGeometry.outline 圆角体多边形 + 箭头三角形(含 radius+arrow 钳制)
模型 ShowcaseStyle.Standard/WaterDropRipple/PulsingCircle 挖孔 / 水波纹 / 脉冲三样式(上游常量)
模型 StepContent.Tooltip/Card Composable 内容的声明式规格
Native KmpShowcaseDispatch / KmpShowcaseVersion C ABI JSON 导出
N-API kmpShowcaseDispatch(req) / kmpShowcaseVersion() 向 ArkTS 暴露能力
ArkTS dispatch(op) / onTap(x,y) 触发操作并整帧重绘

4.4 ArkTS 与 Kotlin 的边界

typescript 复制代码
const r: StepResp = JSON.parse(kmpShowcaseDispatch(JSON.stringify({
  op: 'tap', x: 540 * density, y: 2150 * density
}))) as StepResp;
// r.tap === 'inside';该步 clickAdvances=true → r.active===false(最后一步点完自动结束)
kotlin 复制代码
fun tap(x: Float, y: Float): String {
    val step = state.steps[state.currentIndex]
    val rect = step.rect.inflate(step.highlightPaddingPx)
    val inside = rect.contains(x, y)          // 半开区间,与上游 rect.contains 一致
    if (inside && step.onClickHighlightAdvances) controller.next()
    return response("tap")                    // 整帧渲染模型下行
}

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

五、关键决策说明

决策 1:内核移植而不是真编译整库

KStore 能真编译整库,是因为社区 Maven 补齐了与上游同版本的 kotlinx ohos 制品线。本库的全部依赖是 androidx.compose.*------社区 CMP 停在 1.9.2 / Kotlin 2.2.21,上游在 1.9.3 / 2.3.0,这条制品线不存在。把纯逻辑按原包名复制进 nativeApp/commonMain(ShowcaseLayoutCompose 同法),既是可审计的复用证据(diff 可逐行核对),又不触碰上游模块源码;代价是内核与上游双份维护,等 Compose 编译线对齐后应重新挂回 KLIB。

决策 2:工具链钉 2.2.21-1.0.0 老线 + Gradle 8.14.1

本库零 kotlinx 依赖,没有"制品线逼着升级"的外力;Compose 编译线在鸿蒙社区停在 2.2.21 一代,升级也无收益。沿用 qrose / ShowcaseLayoutCompose 已审组合(Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21),上游 wrapper 8.14.3 降一档------与 KStore"2.4.20 线留 Gradle 9.x"是两条线的经验,不能互相套用。

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

上游根构建(AGP 8.13.2 classpath、新版 com.android.kotlin.multiplatform.library 库插件、vanniktech Maven Central 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。showcasecompose / composeApp / iosApp 留盘不进 settings.gradle.kts------源码零改动,工具链对齐后按原样接回。

决策 4:lambda 不跨 FFI------clickAdvances 布尔 + content 声明式规格

C ABI 过不了 () -> Unit 和 @Composable。上游示例里 onClickHighlight 只有 {} 与 {controller.next()} 两种取值,布尔无损承载;content 只产出 Tooltip 气泡与居中卡片两种 UI,规格化(文案 + 样式 + ArkTS 实测尺寸)后语义一步没少。凡是"语义可枚举"的 lambda 都值得这样收敛,"语义开放"的才需要回调通道。

决策 5:像素在 ArkTS 生成,几何在内核算

qrose 在 KN 里软件光栅化 PNG,因为条码是静态图;引导是强交互 UI(点击 / 无限动画 / 换步重绘),PNG 方案每帧跨一次 FFI 不现实。折中是:所有判定性几何(挖孔矩形、气泡位置、箭头顶点、卡片矩形)由内核算好随响应下行,ArkTS 只做参数化绘制与 ticker 驱动------动画常量(环间距 0.3、脉冲 0.7↔1.3、时长)与上游同源,但没有在 ArkTS 复制一份上游实现。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)、JDK 21、Gradle 8.14.1、DevEco Studio 26.0 命令行工具(hvigor / ohpm / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

bash 复制代码
./gradlew.bat :example:nativeApp:jvmTest

14 项用例全部通过(与 .so 链接的是同一份 commonMain 代码):

用例组 用例数 钉住什么
状态机生命周期 3 空态非激活、next 越界自动清空、start 重建归零
几何基元 2 inflate 四边外扩、contains 半开区间边界
气泡定位 2 Bottom 方向居中偏移 + 箭头中心数值断言、窗口钳制
气泡形状 2 Top 方向 body 高度排除箭头区、箭头中心 radius+arrow 钳制
样式解析 1 WaterDropRipple 上游默认值(3 环 / 2000ms / maxRadius 60dp)
协议端到端 1 start→tap 外→next→tap 内→finish 全链(circle 半径 70、active 翻转、结束态全 null)
错误与序列化 3 非法 JSON / 缺 op / 无会话、版本握手、MiniJson 转义往返

6.3 HAP 验证

text 复制代码
:example:nativeApp:jvmTest: 14 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
BuildNativeWithCmake: finished(双 ABI 编出 libkmpshowcase_napi.so)
CompileArkTS: finished
PackageHap / SignHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 7.4 MB,内含双 ABI 各三件套(libkmpshowcase_napi.so 约 65 KB / libohoskmpshowcase.so arm64 约 2.2 MB、x86_64 约 2.1 MB(HAP 内 strip 后)/ libc++_shared.so);debugShared 未 strip 时 KN so 分别约 4.3 MB / 3.9 MB。

6.4 功能用例

  1. Landing 页底部显示 bridge 0.1.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. 进入 Home 后布局就绪自动开始引导,第 1 步挖孔跟随顶栏搜索图标、第 2 步跟随底部 Bag 按钮;
  3. STANDAR:静态暗场 + 气泡在搜索图标下方,箭头指向图标中心;
  4. ANIMATED DIM:暗场从高亮中心生长(800ms FastOutSlowIn)后切全屏;
  5. WATERDROP:圆孔外 3 环青色水波纹循环扩散 + 居中卡片 600ms 后淡入放大;
  6. PULSING:圆孔外青色描边呼吸圈 0.7↔1.3 往返;
  7. 点气泡 Next / 2/2 Got it! 前进步骤;WATERDROP / PULSING 模式点 Bag 高亮区即前进(clickAdvances);点暗场空白处无反应(tap outside);
  8. 结束后出现 Replay / Back,Replay 重发 op=start 完整回放。

6.5 验证结论

移植内核、渲染模型、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的引导会话:版本握手、双目标挖孔、气泡定位与箭头指向、四种引导样式与前进/回放闭环(见下节效果图)。

七、运行效果

7.1 真机截图

7.2 命令速查

bash 复制代码
git clone https://atomgit.com/oh-tpc/kmp-showcase-compose.git
cd kmp-showcase-compose
export JAVA_HOME="/path/to/jdk-21"
./gradlew.bat :example:nativeApp:jvmTest                                    # 14 项交叉验证
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
ohpm install
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.kmpshowcase -m entry
hdc shell hilog | grep KmpShowcase

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让移植内核真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Math.PI 是 java.lang 的类------JVM 编译通过、compileKotlinOhosArm64 才炸,统一用 kotlin.math.PI;
  4. String.format 是 JVM API,KN 上颜色转十六进制必须手写 nibble 循环;
  5. Kotlin value class 只接受单构造参数,四字段的 Rect 降级为普通 data class(成员语义不变);
  6. 严格 ArkTS 下 Area.globalPosition / Area.width 是 Length(string | number | Resource),直接算术报 Length+Length------先经 lengthToVp() 归一为 number;
  7. import measure from '@ohos.measure' 的默认导入不能当命名空间用类型(measure.MeasureOptions 报 "only refers to a type"),@kit.ArkUI 又没有导出 measure------用本地结构等价接口声明 MeasureOptions;
  8. CLI 打 HAP 时 PackageHap 依赖 java,spawn java ENOENT------JDK 21 进 PATH(DevEco IDE 内置,命令行需自带);
  9. HAP 缺当前 ABI 的 so 报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. ClickEvent 坐标是 vp、Canvas 画布单位也是 vp,而桥协议全程窗口 px------上报 ×density、绘制 ÷density,漏一处即整体错位;
  12. 上游 .gitignore 是根锚定 + 忽略 gradle.properties 的模式,DevEco 的 .hvigor/、oh_modules/、entry/libs/、oh-package-lock.json5 要单独追加段,且 fork 的 gradle.properties(JDK 钉版)需要 ! 反向规则重新入库。

8.2 已知问题

  • Compose 渲染层(ShowcaseOverlay / Tooltip / ShowcaseHost / captureBounds)未编进 ohos,由 ArkTS 重绘;社区 CMP 工具链对齐后应重新挂回;
  • Tooltip 的弹簧入场(spring / scale+alpha)以 250ms EaseOut 近似,参数(0.8→1)来自上游 scaleIn(initialScale = 0.8f);
  • 文字换行以贪心分词 + 1.5 倍行高估算(上游由 Compose 文本布局完成),极端字号需校准;
  • 内核以复制方式移植,上游若改内核需手工同步(JVM 用例是漂移闸门);
  • ArrowAlignment 上游即死代码,移植保留未接线;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.kmpshowcase 匹配的签名。

8.3 未来优化方向

  • 等社区 CMP 1.9.3+ / Kotlin 2.3 ohos 编译线,把 showcasecompose 模块重新挂回构建出 KLIB,桥退化为纯示例;
  • 上游新增样式 / 内容类型时先补 ShowcaseModel/Geometry 协议再补 JVM 用例;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

text 复制代码
上游 kmp-showcase-compose(19 文件全 Compose)
    → 源码依赖度盘点(4 个逐字 / 7 个改类型 / 8 个重绘)
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1)
    → 根构建替换(cmp_qrose 模式,Compose 模块留盘)
    → 内核原包名移植 + Dp/Color/Rect 值类型替换 + 气泡多边形化
    → C ABI(@CName × 2)+ 尺寸上行/几何下行 JSON 契约
    → C++ N-API(DisposeString 纪律)
    → ArkTS Canvas 遮罩 + Text 气泡 + 卡片入场
    → ArkUI 真机四模式验收(挖孔 / 定位 / 前进 / 回放)

9.2 封装层次

  • 内核层:上游原版状态机、步骤模型、样式常量、四方向定位、气泡形状钳制(移植件运算符级一致);
  • 桥层:请求解析 + 渲染模型计算 + 会话管理 + 错误通道;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:目标矩形上报、内容实测、Canvas 绘制、动画 ticker、交互分发;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 纯 Compose UI 库的适配面由"依赖度盘点"决定 :全库快照态只有 4 个符号、值类型沾染只有 Dp/Color/Rect 三种------盘点清楚了,内核化成本极低;盘点不清,要么过度重写、要么漏掉语义。三分法(逐字 / 改类型 / 重绘)是可复用的盘点产出;
  2. lambda 接缝先看语义可枚举性 :onClickHighlight 两种取值收敛为布尔、content 两种 UI 收敛为规格,都比开回调通道便宜且不丢语义;反过来 updates Flow 这类开放语义(KStore)才真的过不了桥;
  3. 像素放哪边由交互形态决定:静态产出(条码)适合 KN 光栅化 PNG,强交互 UI(引导)适合"几何在内核、像素在宿主"------判定逻辑留在可交叉验证的 Kotlin 侧,绘制循环留在宿主动画系统里,两边都不复制对方的实现。

9.4 适配成果

当前适配已完成:

  • kmp-showcase-compose 0.1.0(Apache-2.0)内核的双 ABI Kotlin/Native 编译(14 个移植文件,运算符级一致,上游模块源码零改动);
  • 2 个 C ABI 符号和 C++ N-API 包装层(真机 HAP 内双 ABI 六件套 so 验证);
  • DevEco Stage ArkUI 验收页面(四种引导模式、双高亮目标、气泡与卡片两种内容、Replay 回放、版本握手);
  • 14 项 JVM 交叉验证全绿 + HAP 构建(约 7.4MB)、签名、设备安装和真机验证;
  • 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。

参考文档

相关推荐
OH_TPC1 小时前
HarmonyOS APP开发---“旅图“旅行摄影社区App,需要用到这个库
华为·harmonyos·鸿蒙
独隅4 小时前
HarmonyOS 系统能力与体验深度评测
华为·harmonyos·harmonyos 7.0
2601_949575864 小时前
开源鸿蒙平台 CMP 三方库「Persian-Date-picker」适配全流程
华为·开源·harmonyos
wflynn5 小时前
GitHub 今日推荐|rea:用 MCP 把反编译器和 AI agent 接到一起的逆向工具
typescript·开源·github·agent·agent-skills·binary-analysis
KevinChaos5 小时前
灶台菜谱设计手记 02|HarmonyOS 7.0 空间化:菜图滚动实现
harmonyos
m4Rk_6 小时前
【论文阅读】Agent记忆机制(114):MemoryTax——当 Agent 能自主搜索历史,专门的记忆系统还划算吗?
论文阅读·人工智能·学习·开源·github
独隅6 小时前
HarmonyOS 全场景智能应用落地指南
华为·harmonyos·harmonyos 7.0
熊猫钓鱼>_>6 小时前
鸿蒙PC构建实战:hvigor modelVersion 降级 + schema 清理 + ArkTS 编译三连坑完整复盘
开发语言·华为·ai编程·harmonyos·openharmony·数据·ai生成
2601_949970696 小时前
开源鸿蒙平台 CMP 三方库「SaltUI」适配全流程
华为·开源·harmonyos