开源鸿蒙平台 CMP 三方库「material3-windowsizeclass-multiplatform」适配全流程
本文记录
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-free | commonMain 直接 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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(来自 maven.eazytec-cloud.com) |
| JDK | 21 |
| OpenHarmony 目标 | ohosArm64 + ohosX64 |
| Gradle | 8.14.1 |
| DevEco product | 5.1.0(18)(命令行 6.0.1 / API 21 SDK 验证通过) |
| DevEco Studio | 6.0.1 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.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.kts | eazytec 双源置顶,只 include :example:nativeApp |
example/nativeApp/build.gradle.kts | ohosArm64/ohosX64 双目标、srcDir 直引、copy task、JVM 自测 |
example/nativeApp/…/UnitShims.kt 等三个 shim | Dp/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.kt | JVM 金标准与命令行查询 |
example/harmonyApp/…/napi_init.cpp | N-API 导出、参数检查和内存释放 |
example/harmonyApp/…/Index.ets | ArkUI 实时窗口 + 模拟计算页面 |
README.OpenHarmony_CN.md | 25 节中文主文档(桥协议、踩坑表、验收剧本) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| KMP(上游) | WindowSizeClass.calculateFromSize | 像素 + 密度 → 档位,唯一计算入口 |
| KMP(上游) | WindowWidthSizeClass.Compact/Medium/Expanded | 档位模型 |
| Kotlin 桥 | mwscCalc(request) | 校验 + 调用上游 + JSON 组装 |
| Kotlin 桥 | MiniJson.parse/string | 扁平 JSON 解析与转义 |
| Native | MwscCalc | C ABI 单入口 |
| Native | libohosmwsc_symbols | KN 符号表,提供 DisposeString |
| N-API | mwscCalc | 向 ArkTS 暴露的同步函数 |
| ArkTS | recalcLive / 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 功能用例
- 启动后「当前窗口」Tab 自动完成首次计算,状态行显示实时像素与档位;
- 旋转屏幕,宽高档位互换(如 Compact/Medium → Medium/Compact),断点条同步高亮;
- 自适应列表随宽度档位在 1/2/3 列与 14/18/22 字号间切换;
- 「模拟计算」Tab 依次点六个设备预设,档位与预期一致(折叠屏展开 2208×1848 @2x → Expanded/Expanded);
- 手输边界值 599/600、839/840、479/480、899/900,档位逐级跳变;
- 档位子集只保留 Compact/Medium 时,1280×800 @1x 宽度收敛为 Medium,
supportedWidth回显; - 输入负数像素,状态区显示
失败: widthPx and heightPx must be >= 0,页面不中断; - 长按复制
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 踩坑复盘
- 只在 ArkTS 里写断点判断不算 KMP 适配,必须真正编译
ohosArm64动态库并跑上游代码; - Kotlin 2.2 禁止 BINARY 保留的 EXPRESSION 目标注解,shim 里的
@Composable必须删掉用不到的EXPRESSION目标; JavaExec的 classpath 在 Kotlin 2.2 下要用files(compilation.output) + configurations["jvmRuntimeClasspath"],且顶层main的主类名是MainKt而不是Main;- ArkTS 侧
Display类型没有fontScale字段,实时页固定 1.0(上游分桶只用 density,语义无损),模拟页仍可调节; - eazytec 双 Maven 源必须排在
gradlePluginPortal()之前,否则 HarmonyOS Kotlin 插件解析 404; - CMake 在
entry/libs/<abi>/缺 so 时应直接FATAL_ERROR提示先跑 gradle,把错误前移到同步阶段; - 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 三条经验
- 先让共享分桶实现在 JVM 和 Native 双后端通过金标准,再接入 ArkUI 或 CMP UI;
- 上游算法文件用"直引"而不是"复制"进入鸿蒙构建,配合
toString逐字比对,漂移无处藏身; - 分别记录自动测试、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。
参考文档
更多推荐




所有评论(0)