本文记录 material3-windowsizeclass-multiplatform 接入 OpenHarmony 的完整过程,覆盖 KMP 公共窗口档位库盘点、ohosArm64/ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

material3-windowsizeclass-multiplatform 是 Material 3 窗口尺寸档位(WindowSizeClass)的 Compose Multiplatform 实现:一组固定的 dp 断点把任意窗口划分为 Compact / Medium / Expanded 三档,响应式布局按档位切换。本次适配让同一份公共分桶实现进入 OpenHarmony ARM64 运行时,ArkUI 页面作为真机验收宿主,Compose Multiplatform(CMP)应用可以通过同一个 KMP 依赖使用这些档位 API。

项目地址: AtomGit/oh-tpc/material3-windowsizeclass-multiplatform

上游仓库: chrisbanes/material3-windowsizeclass-multiplatform

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

material3-windowsizeclass-multiplatform 为 Kotlin 提供 WindowSizeClass、WindowWidthSizeClass、WindowHeightSizeClass 和 calculateFromSize() 窗口档位计算能力。KMP 项目通常把这些能力放在 commonMain,由 Android、iOS、桌面、Web 或其他 Native 宿主共享。OpenHarmony 的折叠屏、平板、分屏和自由窗口同样需要这套公共断点,但默认构建并不会生成 OpenHarmony 的 Kotlin/Native 变体。

如果只在 ArkTS 里写三行 if 判断 600dp/840dp,页面可以显示档位,却无法证明公共分桶实现已经在 OpenHarmony ARM64 设备上完成编译、链接和执行,而且手写断点没有与上游对齐的验收锚点,后续必然漂移。适配需要同时验证 KMP 核心、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64()/ohosX64() 才能生成 OpenHarmony 动态库。
工具链不一致HarmonyOS Kotlin、Native SDK、LLVM 和 Gradle 插件需要匹配。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
上游非 compose-freecommonMain 直接 import Dp/DpSize/Density 等 Compose 类型,而鸿蒙工具链没有对应 compose-ui 产物。
窗口采集差异上游 @Composable calculateWindowSizeClass() 依赖各平台 Compose 运行时,鸿蒙侧窗口像素与密度需由 ArkTS 采集。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在 KMP 目标配置、C ABI/N-API 桥接和 ArkUI 验收层。分桶语义仍由上游公共实现负责,ArkTS 只负责采集窗口、调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

示例提供两个可观察场景和一组自适应演示:

  • 当前窗口:真机/模拟器实时读取窗口像素与密度,windowSizeChange 触发即时重算,旋转、折叠、分屏拖拽全部实时刷新;
  • 模拟计算:输入任意像素、density、fontScale 和档位子集,复现任意设备的档位判定;
  • 自适应布局:列表列数与标题字号随宽度档位在 1/2/3 列、14/18/22 之间切换。

桥返回的 MwscResult 包含:

字段作用
widthSizeClass / heightSizeClass"Compact"、"Medium"、"Expanded" 三档判定结果。
widthDp / heightDp像素除以密度后的 dp 值,即档位判定的真实输入。
widthBreakpoint / heightBreakpoint命中档位的区间下界(宽 0/600/840,高 0/480/900)。
supportedWidth / supportedHeight本次调用的档位子集回显,验证收敛语义。
toString上游 WindowSizeClass.toString() 的逐字输出,交叉验证锚点。
ok / error非法输入(负数像素、空档位子集、density ≤ 0)返回明确错误。

JVM 侧还提供 14 项自检:11 个分桶用例(覆盖 0、599.99、600、839.99、840 等断点边界和手机竖横屏、折叠屏、平板、桌面窗口典型组合)+ 3 个错误与收敛用例(负数像素、缺字段、子集收敛)。

1.3 实现适配

维度要求
代码复用分桶算法全部位于上游 lib/src/commonMain,一行未改、一字未复制。
平台目标桥模块启用 ohosArm64 + ohosX64,生成双 ABI 动态库。
桥接稳定使用单函数 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址。
CMP 兼容CMP 应用复用同一 KMP 依赖;ArkUI 只负责真机验收。
UI 完整支持实时窗口、设备预设、边界输入、档位子集和错误展示。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。CMP 工程可以直接依赖 OpenHarmony KMP 变体,在自己的 Compose 页面中使用标准窗口档位 API。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点上游源集、Compose 依赖面和 OpenHarmony 交付物
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64/ohosX64、上游源码直引和单元 shim
第 3 阶段:窗口档位建模   ── 建立 JSON 契约、字段回显和 JVM 金标准
第 4 阶段:原生桥接       ── C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 实时窗口监听、模拟计算、自适应演示和错误边界
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
lib/                           上游库源码(未改动,不进鸿蒙 Gradle 构建)
example/nativeApp/             Kotlin/Native 动态库:单元 shim + MwscCalc 桥
example/harmonyApp/            DevEco Stage、CMake N-API 和 ArkTS 页面
example/(其余上游 sample)      上游各平台示例,保留对照,不进构建

鸿蒙构建是仓库根 Gradle 工程的一个 focused 子集:settings.gradle.kts 只 include :example:nativeApp,上游 Compose 世界(:lib、shared、android-app 等)原样留在磁盘上作为对照,不参与编译。这与社区已过审的 qrose 适配处理 qrose-core 的方式一致。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(来自 maven.eazytec-cloud.com)
JDK21
OpenHarmony 目标ohosArm64 + ohosX64
Gradle8.14.1
DevEco product5.1.0(18)(命令行 6.0.1 / API 21 SDK 验证通过)
DevEco Studio6.0.1 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.mwsc(版本 0.5.0-ohos.1)
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 页面边界
标题区          MATERIAL3-WINDOW-SIZE-CLASS / libohosmwsc.so + libmwsc_napi.so
Tab 区          当前窗口 / 模拟计算
结果区          宽度档位徽章、高度档位徽章、断点条、像素与 dp、toString
自适应区        随宽度档位切换 1/2/3 列的演示列表
操作区          设备预设、像素输入、密度选择、档位子集开关、计算按钮
状态区          实时刷新的运行状态与错误信息

页面启动时读取系统窗口与显示密度自动完成首次计算,无需手动触发。

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

2.1 加入 ohosArm64 / ohosX64
kotlin {
    applyDefaultHierarchyTemplate()

    jvmToolchain(21)
    jvm()

    ohosArm64 {
        binaries.sharedLib { baseName = "ohosmwsc" }
    }
    ohosX64 {
        binaries.sharedLib { baseName = "ohosmwsc" }
    }

    sourceSets {
        commonMain {
            // Compile the upstream library sources verbatim: no copy, no fork
            // of the breakpoint algorithm.
            kotlin.srcDir(rootProject.projectDir.resolve("lib/src/commonMain/kotlin"))
        }
    }
}

jvm() 与鸿蒙双目标并存:同一份源码在 JVM 和 OpenHarmony 两个后端编译,为交叉验证提供基准侧。

2.2 上游源码直引与单元 shim(本适配的关键差异)

上游 commonMain 不是 compose-free 的——WindowSizeClass.kt 直接 import androidx.compose.ui.unit.Dp/DpSize/Density、androidx.compose.ui.geometry.Size 和 androidx.compose.runtime.Composable/Immutable,而 HarmonyOS Kotlin 工具链下没有这些 compose-ui 产物。社区已过审的 qrose 适配中上游天然分成纯 Kotlin 的 commonMain 和画笔的 composeMain,只需要不编译后者;本库没有这个天然分界,因此采用源码目录直引 + 编译期 shim:

package androidx.compose.ui.unit

@kotlin.jvm.JvmInline
value class Dp(val value: Float) : Comparable<Dp> {
    override operator fun compareTo(other: Dp): Int = value.compareTo(other.value)
}

interface Density {
    val density: Float
    val fontScale: Float

    /** Dp -> px, identical to androidx.compose.ui.unit.Density. */
    fun Dp.toPx(): Float = value * density
}

fun Density(density: Float, fontScale: Float = 1f): Density = object : Density { ... }

val Int.dp: Dp get() = Dp(toFloat())

shim 只覆盖上游 import 的 7 个符号,算术与真实 Compose 逐字一致(Dp.toPx() = value * density);上游算法文件零改动、零复制进入鸿蒙二进制,漂移不可能。

2.3 构建产物
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
          :example:nativeApp:linkDebugSharedOhosX64

link task 通过 finalizedBy 自动把产物连同 Kotlin/Native 运行时一起搬运:

example/harmonyApp/entry/libs/arm64-v8a/libohosmwsc.so      真机
example/harmonyApp/entry/libs/arm64-v8a/libc++_shared.so    KN 运行时依赖
example/harmonyApp/entry/libs/x86_64/libohosmwsc.so         DevEco 模拟器
example/harmonyApp/entry/libs/x86_64/libc++_shared.so

llvm-nm 确认 libohosmwsc.so 导出 MwscCalc 与 libohosmwsc_symbols 两个符号。

第 3 阶段:窗口档位场景建模

3.1 JSON 契约
WindowSizeClass.kt(上游)
        ↓ calculateFromSize(size, density, supported…)
MwscBridge.kt
        ↓ 档位徽章 + dp + 断点 + toString
mwscCalc(request)
        ↓ UTF-8 JSON
N-API mwscCalc()
        ↓
ArkUI Index.ets

页面不复制分桶判断,每个档位结果由上游公共代码计算。

3.2 桥的输入与响应
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
fun mwscCalc(request: String): String = try {
    val json = MiniJson.parse(request)
    // … 参数校验 …
    val windowSizeClass = WindowSizeClass.calculateFromSize(
        size = Size(widthPx.toFloat(), heightPx.toFloat()),
        density = Density(density.toFloat(), fontScale.toFloat()),
        supportedWidthSizeClasses = supportedWidth,
        supportedHeightSizeClasses = supportedHeight,
    )
    // … 组装 JSON 响应 …
} catch (t: Throwable) {
    """{"ok":false,"error":${MiniJson.string(t.message ?: t.toString())}}"""
}

请求字段:widthPx/heightPx(必填,像素)、density(默认 1.0)、fontScale(默认 1.0)、supportedWidth/supportedHeight(逗号分隔的档位子集,缺省全部三档)。

3.3 JVM 金标准
val cases = listOf(
    Case(599.99, 479.99, 1.0, "Compact", "Compact", …),
    Case(600.0, 480.0, 1.0, "Medium", "Medium", …),
    Case(839.99, 899.99, 1.0, "Medium", "Medium", …),
    Case(840.0, 900.0, 1.0, "Expanded", "Expanded", …),
    Case(1080.0, 2340.0, 3.0, "Compact", "Medium", …),   // 360×780dp 手机竖屏
    Case(1536.0, 2048.0, 2.0, "Medium", "Expanded", …),   // 768×1024dp 平板竖屏
    // …
)

期望值直接取自 Material 公开断点规范,是独立于库实现的验收 oracle。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry
  │ const char*
  ▼
Kotlin/Native C ABI
  │ MwscCalc
  ▼
UTF-8 JSON + explicit free

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制分桶语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)

@CName("MwscCalc")
fun mwscCalcNative(request: String): String = mwscCalc(request)

Kotlin String 参数与返回值自动映射为 const char*。返回的内存在 Kotlin 侧分配,C++ 拷贝完成后必须通过 KN 约定导出的 libohosmwsc_symbols()->DisposeString 释放,否则内存泄漏。

4.3 C++ N-API 方法分发
static napi_value NapiMwscCalc(napi_env env, napi_callback_info info) {
    // … 参数个数与字符串类型检查 …
    const std::string request = JsToUtf8(env, args[0]);
    const char *raw = MwscCalc(request.c_str());
    napi_value out = nullptr;
    napi_status st = napi_create_string_utf8(env, raw, NAPI_AUTO_LENGTH, &out);
    DisposeKnString(raw);   // 经 libohosmwsc_symbols() 释放 KN 内存
    return out;
}

napi_property_descriptor desc[] = {
    {"mwscCalc", nullptr, NapiMwscCalc, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};

N-API 只负责参数类型、字符串创建和释放,不实现档位语义。

4.4 CMake 和类型声明
add_library(ohosmwsc SHARED IMPORTED)
set_target_properties(ohosmwsc PROPERTIES
  IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH}/libohosmwsc.so")

add_library(mwsc_napi SHARED napi_init.cpp)
target_link_libraries(mwsc_napi PUBLIC libace_napi.z.so libhilog_ndk.z.so ohosmwsc)
export const mwscCalc: (request: string) => string;

$ORIGIN rpath 与 POST_BUILD 拷贝保证 dlopen 时 Kotlin/Native 运行时与 libc++_shared.so 就位;缺少 so 时 CMake 直接 FATAL_ERROR 提示先跑 gradle。

第 5 阶段:ArkUI 宿主

5.1 实时窗口监听
private async attachWindow(): Promise<void> {
    const ctx = this.getUIContext().getHostContext() as common.Context;
    const win = await window.getLastWindow(ctx);
    const props = win.getWindowProperties();
    const disp = display.getDefaultDisplaySync();
    this.liveWidthPx = props.windowRect.width;
    this.liveHeightPx = props.windowRect.height;
    this.liveDensity = disp.densityPixels;
    this.recalcLive();
    win.on('windowSizeChange', (size: window.Size) => {
        this.liveWidthPx = size.width;
        this.liveHeightPx = size.height;
        this.recalcLive();
    });
}

旋转、折叠展开、分屏拖拽都会触发 windowSizeChange,档位徽章与自适应列表即时刷新。

5.2 页面交互

「模拟计算」Tab 提供六个设备预设(手机竖屏/横屏、折叠屏展开、平板竖屏/横屏、桌面窗口)、像素输入、density/fontScale 选择和六个档位子集开关;断点条按命中档位高亮对应区段;toString 文本支持长按复制,用于与 JVM 侧逐字比对。

5.3 对比度和错误边界

档位徽章采用固定配色:Compact 红色、Medium 橙色、Expanded 绿色;选中 Tab 深蓝底白字,浅色按钮深色文字。非法输入(负数、空子集)与 Native 异常统一显示在页面底部状态区,不中断页面。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/src/commonMain/kotlin/
│   ├── androidx/compose/ui/{unit,geometry,runtime}/…Shims.kt   单元 shim
│   └── sample/nativeapp/{MwscBridge,MiniJson}.kt               桥实现
├── nativeApp/src/ohosMain/kotlin/sample/nativeapp/Bridge.kt    @CName 导出
├── nativeApp/src/jvmMain/kotlin/sample/nativeapp/Main.kt       JVM 金标准
└── harmonyApp/
    ├── entry/src/main/cpp/{CMakeLists.txt,napi_init.cpp}
    └── entry/src/main/ets/pages/Index.ets                       ArkUI 页面
6.2 构建、签名和安装
export JAVA_HOME="/path/to/jdk-21"
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
          :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
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.mwsc

模拟器(x86_64)可直接安装 unsigned HAP;真机在 DevEco Studio 中配置自动签名后运行。

四、完整代码对照

4.1 整体架构

WindowSizeClass.kt(上游,零改动)
    │ calculateFromSize / fromWidth / fromHeight
    ▼
MwscBridge.kt(+ 单元 shim + MiniJson)
    │ @CName("MwscCalc") JSON in/out
    ▼
libmwsc_napi.so
    │ mwscCalc
    ▼
Index.ets
    │ 窗口采集 + JSON parse + state update
    ▼
ArkUI 真机页面

4.2 文件清单

文件职责
settings.gradle.ktseazytec 双源置顶,只 include :example:nativeApp
example/nativeApp/build.gradle.ktsohosArm64/ohosX64 双目标、srcDir 直引、copy task、JVM 自测
example/nativeApp/…/UnitShims.kt 等三个 shimDp/DpSize/Density/Size/dp/Composable/Immutable
example/nativeApp/…/MwscBridge.kt桥实现:校验、上游调用、JSON 组装
example/nativeApp/…/MiniJson.kt手写极简 JSON 解析与转义
example/nativeApp/…/Bridge.kt@CName("MwscCalc") 导出
example/nativeApp/…/Main.ktJVM 金标准与命令行查询
example/harmonyApp/…/napi_init.cppN-API 导出、参数检查和内存释放
example/harmonyApp/…/Index.etsArkUI 实时窗口 + 模拟计算页面
README.OpenHarmony_CN.md25 节中文主文档(桥协议、踩坑表、验收剧本)

4.3 关键 API 对照

层次API作用
KMP(上游)WindowSizeClass.calculateFromSize像素 + 密度 → 档位,唯一计算入口
KMP(上游)WindowWidthSizeClass.Compact/Medium/Expanded档位模型
Kotlin 桥mwscCalc(request)校验 + 调用上游 + JSON 组装
Kotlin 桥MiniJson.parse/string扁平 JSON 解析与转义
NativeMwscCalcC ABI 单入口
Nativelibohosmwsc_symbolsKN 符号表,提供 DisposeString
N-APImwscCalc向 ArkTS 暴露的同步函数
ArkTSrecalcLive / simCalculate窗口事件与模拟输入触发计算

4.4 ArkTS 与 Kotlin 的边界

const raw: string = mwscCalc(JSON.stringify({
    widthPx: rect.width, heightPx: rect.height,
    density: density, fontScale: fontScale,
}));
this.liveResult = JSON.parse(raw) as MwscResult;
val windowSizeClass = WindowSizeClass.calculateFromSize(
    size = Size(widthPx.toFloat(), heightPx.toFloat()),
    density = Density(density.toFloat(), fontScale.toFloat()),
)

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

五、关键决策说明

决策 1:双 ABI 而不是只出 arm64

ohosX64 让 DevEco 模拟器(缺 x86_64 so 会报安装错误 9568347)与真机共用同一套工程,演示和回归成本最低。

决策 2:上游源码直引 + 单元 shim,而不是重构或复制

给上游"抽一个纯 Kotlin 模块"会大幅改动上游文件;复制断点表会制造第二份会漂移的实现。srcDir 直引让鸿蒙二进制里跑的就是上游字节,验收锚点唯一。

决策 3:单函数 JSON 桥

一个 MwscCalc 覆盖全部能力(实时、模拟、子集、错误),参数可演进,ABI 面最小;与社区已过审的 qrose / Vico / Reorderable 路线一致。

决策 4:保留 JVM 目标做交叉验证

同一份源码 + 同一套 shim 在 JVM 与 OpenHarmony 两个后端编译运行;toString 字段逐字比对,任何一侧漂移立即暴露。

决策 5:ArkUI 作为验收宿主,CMP 复用 KMP API

当前页面验证 OpenHarmony Native 链路,CMP 应用则直接依赖相同 KMP API,在自己的 Compose UI 中调用 calculateFromSize。

六、测试与验证

6.1 测试环境

本次验证使用 macOS(arm64)、JDK 21(Homebrew OpenJDK)、Gradle 8.14.1、HarmonyOS Kotlin 2.2.21-1.0.0,以及 DevEco 命令行工具 6.0.1(hvigor 6.21.2 / API 21 SDK)完成 HAP 构建;真机运行使用 HUAWEI 5G 真机(窗口 1260×2720 px、density 3.25,与截图一致)。

6.2 共享样例测试

./gradlew :example:nativeApp:mwscSelfTest
./gradlew :example:nativeApp:mwscSelfTest -PmainArgs=1080,2340,3.0
PASS 599.99x479.99@1.0 -> Compact/Compact
PASS 600.0x480.0@1.0 -> Medium/Medium
PASS 839.99x899.99@1.0 -> Medium/Medium
PASS 840.0x900.0@1.0 -> Expanded/Expanded
PASS 1080.0x2340.0@3.0 -> Compact/Medium
PASS 1536.0x2048.0@2.0 -> Medium/Expanded
…
SELF-TEST PASSED (11 buckets + 3 error/clamp cases)

测试验证断点边界、典型设备档位、负数像素拒绝、缺字段拒绝和档位子集收敛。

6.3 HAP 验证

CompileArkTS: finished
BuildNativeWithCmake / BuildNativeWithNinja: finished
PackageHap: finished
SignHap: skipped (unsigned build)
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

构建产物 entry-default-unsigned.hap 内含双 ABI 各三份动态库(libmwsc_napi.so + libohosmwsc.so + libc++_shared.so);llvm-nm 确认 MwscCalc/libohosmwsc_symbols 已导出。

6.4 功能用例

  1. 启动后「当前窗口」Tab 自动完成首次计算,状态行显示实时像素与档位;
  2. 旋转屏幕,宽高档位互换(如 Compact/Medium → Medium/Compact),断点条同步高亮;
  3. 自适应列表随宽度档位在 1/2/3 列与 14/18/22 字号间切换;
  4. 「模拟计算」Tab 依次点六个设备预设,档位与预期一致(折叠屏展开 2208×1848 @2x → Expanded/Expanded);
  5. 手输边界值 599/600、839/840、479/480、899/900,档位逐级跳变;
  6. 档位子集只保留 Compact/Medium 时,1280×800 @1x 宽度收敛为 Medium,supportedWidth 回显;
  7. 输入负数像素,状态区显示 失败: widthPx and heightPx must be >= 0,页面不中断;
  8. 长按复制 toString,与 JVM 侧 -PmainArgs=1080,2340,3.0 输出逐字一致。

6.5 验证结论

Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在 HUAWEI 真机上成功安装并进入前台,「当前窗口」页实时显示 1260×2720 px @3.25x → 宽度 387.69dp Compact、高度 836.92dp Medium,自适应列表按 Compact 档位呈现 1 列,toString 与上游格式逐字一致。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/material3-windowsizeclass-multiplatform.git
cd material3-windowsizeclass-multiplatform
export JAVA_HOME="/path/to/jdk-21"
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
          :example:nativeApp:linkDebugSharedOhosX64
./gradlew :example:nativeApp:mwscSelfTest
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只在 ArkTS 里写断点判断不算 KMP 适配,必须真正编译 ohosArm64 动态库并跑上游代码;
  2. Kotlin 2.2 禁止 BINARY 保留的 EXPRESSION 目标注解,shim 里的 @Composable 必须删掉用不到的 EXPRESSION 目标;
  3. JavaExec 的 classpath 在 Kotlin 2.2 下要用 files(compilation.output) + configurations["jvmRuntimeClasspath"],且顶层 main 的主类名是 MainKt 而不是 Main;
  4. ArkTS 侧 Display 类型没有 fontScale 字段,实时页固定 1.0(上游分桶只用 density,语义无损),模拟页仍可调节;
  5. eazytec 双 Maven 源必须排在 gradlePluginPortal() 之前,否则 HarmonyOS Kotlin 插件解析 404;
  6. CMake 在 entry/libs/<abi>/ 缺 so 时应直接 FATAL_ERROR 提示先跑 gradle,把错误前移到同步阶段;
  7. Native 返回字符串交给 N-API 后必须经 DisposeString 释放,签名材料与产物目录一律不进仓库。

8.2 已知问题

  • 上游库已在 0.5.0 后标记 Deprecated(JetBrains 自 CMP 1.7 起内置同名 API),但那是 Compose 世界内部的事,鸿蒙侧无 Compose 运行时,本适配仍是该断点算法在 ohos 上的可用路径;
  • 鸿蒙侧没有 @Composable calculateWindowSizeClass(),窗口采集由 ArkTS 完成后走桥;
  • 单元 shim 只覆盖上游 import 的 7 个符号,不可发布、不可与真实 Compose classpath 混用;
  • MiniJson 只支持扁平对象,协议刻意不含嵌套与数组;
  • 真机安装必须使用与 bundle name 匹配的签名,本仓库不含任何签名材料。

8.3 未来优化方向

  • 补充折叠屏展开/合上、分屏、自由窗口的真机验收矩阵与截图;
  • 将结果封装为可订阅的档位状态流,演示 CMP 状态层消费;
  • 增加 Compose Multiplatform 示例宿主,直接在 Compose 页面调用 KMP API;
  • 在 CI 中加入 Native 链接、ELF 符号检查和 HAP 构建;
  • 增加桥版本字段和 ABI 兼容检查,为协议演进留余地。

九、总结

9.1 核心难点回顾

KMP commonMain 分桶实现(依赖 Compose 单元类型)
    → 源码直引 + 单元 shim(零改动进入构建)
    → ohosArm64/ohosX64 Kotlin/Native
    → C ABI @CName("MwscCalc")
    → C++ N-API
    → ArkTS JSON 解析
    → ArkUI 真机页面

9.2 封装层次

  • KMP 层:提供 WindowSizeClass 计算、档位模型和子集收敛语义,一行未改;
  • shim 层:以 7 个符号复刻 Compose 单元算术,让上游源码原样编译;
  • Native 层:生成双 ABI 动态库并通过单函数 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理窗口监听、模拟输入、档位徽章、断点条和错误状态;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先让共享分桶实现在 JVM 和 Native 双后端通过金标准,再接入 ArkUI 或 CMP UI;
  2. 上游算法文件用"直引"而不是"复制"进入鸿蒙构建,配合 toString 逐字比对,漂移无处藏身;
  3. 分别记录自动测试、Native 链接、HAP 构建和真机运行,避免把编译成功误认为设备链路可用。

9.4 适配成果

当前适配已完成:

  • 上游 0.5.0 分桶算法的 ohosArm64 + ohosX64 双目标编译(版本 0.5.0-ohos.1);
  • 7 符号 Compose 单元 shim 与上游源码直引方案;
  • 单函数 JSON 桥、C++ N-API 和双 ABI 动态库;
  • JVM 金标准(11 分桶 + 3 错误/收敛用例)全部通过;
  • DevEco Stage ArkUI 验收页面(实时窗口 + 模拟计算 + 自适应布局);
  • HAP 构建、真机安装和 HUAWEI 真机运行验证;
  • 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。

参考文档

Logo

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

更多推荐