开源鸿蒙平台 CMP 三方库「KitFlow」适配全流程
本文记录
KitFlow接入 OpenHarmony 的完整过程,覆盖自适应库盘点、Compose 隔离策略、源码集拆分、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
KitFlow是基于 Compose Multiplatform 的自适应 UI 库:断点分桶(SM / MD / LG / TAB / DESKTOP)、双通道语义(screenClass取窗口短边、layoutClass取当前宽度)、窗口信息(rememberWindowInfo)、方向与设备识别、约束感知的AdaptiveFlowGrid流式网格,以及无障碍信息归一化(字号档位 / 最小触控目标 / 减弱动效)。它的设计原则是"解析自适应上下文与自适应值,不拥有 UI"。本次适配与 KStore 的"制品线补齐后真编译整库"路线又有不同:KitFlow 的解析层本来就是零第三方依赖的纯 Kotlin(dp 全部用Int),挡住适配的只有渗透在commonMain里的 Compose 类型(@Composable/@Immutable/Dp/CompositionLocal)。因此适配答案是隔离而非重写——上游 Compose 门面(Provider / Locals / FlowGrid)整体挪入不参与 ohos 编译的composeMain源码集,六个解析器(WindowInfoResolver/BreakpointResolver/AdaptiveFlowGridResolver/AdaptiveAccessibilityResolver及其类型)一行不改编进OpenHarmony动态库,加一个KitFlowBridge公开门面与一个@CNameC ABI,ArkUI 四 Tab 页面作为验收宿主,真机跑通。
项目地址: AtomGit/oh-tpc/KitFlow.git
上游仓库: vedangj72/KitFlow
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
KitFlow 为 Compose Multiplatform 应用提供"一套响应式规则,多端同构"的能力:AdaptiveKitProvider 包住应用根节点后,Adaptive.value(sm…desktop) 按断点取值(tab 缺省回落 lg、desktop 缺省回落 tab→lg),Adaptive.layoutValue 按当前宽度响应分屏与旋转,rememberWindowInfo 给出短边 / 长边 / 宽高比 / 方向 / 设备,AdaptiveFlowGrid 用自己的约束而不是设备标签决定列数——旋转、分屏、窗口缩放全部自然生效,rememberAdaptiveAccessibilityInfo 把系统字号缩放归一成 Normal / Large / ExtraLarge 三档并给出最小触控目标。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套自适应语义,但上游构建(Kotlin 2.4.0 / Gradle 9.1.0 / AGP 9.0.1 / CMP 1.11.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示滑杆和卡片,却无法证明公共解析层在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证断点/窗口/网格/无障碍四个解析器、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 变体。 |
| 工具链冲突 | 上游 Gradle 9.1.0 + AGP 9.0.1 与鸿蒙工具链无法共存于一个 Gradle 构建;HarmonyOS Kotlin 2.2.21 线不支持 Gradle 9,wrapper 必须降 8.14.1。 |
| Compose 类型渗透 | commonMain 里 12 个文件依赖 @Composable / CompositionLocal / Modifier / Dp,社区 CMP 只有 1.9.2(上游是 CMP 1.11 / Kotlin 2.4),这些类型编不进 ohos。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| UI 语义跨不了桥 | CompositionLocal 订阅式分发、remember* 响应式重组是 Compose 运行时概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:解析层的 ohos 编译、根构建替换(cmp_qrose 模式)、Compose 门面隔离和 C ABI/N-API 桥接层。断点与网格语义完全由上游原版实现负责,ArkTS 只负责采集输入(窗口尺寸、屏幕密度、字号缩放)、调用 Native、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的自适应解析会话,对应桥接的全部入口:
- 窗口断点(window):输入宽高 dp,一次返回
screenClass/layoutClass/orientation/device/shortestSideDp/longestSideDp/aspectRatio——双通道语义与阈值覆盖全部生效; - 布局值(value):输入断点与五档候选值,返回选中值——
tab缺省回落lg、desktop缺省回落tab → lg的回退链原样透出; - 流式网格(grid):输入可用宽度 px、最小列宽 px、列间距、最大列数、子项数、字号缩放,返回列数、逐列宽度、行数、有效最小列宽——
AdaptiveFlowGrid测量算法的完整输出; - 无障碍(a11y):输入字号缩放与开关位,返回字号档位、大小字号标记、最小触控目标 dp。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op | 回显本次操作(window / value / grid / a11y)。 |
screenClass / layoutClass / orientation / device | window 操作的核心解析结果。 |
columns / columnWidths / rowCount / effectiveMinColumnWidthPx | grid 操作的列算结果。 |
fontScaleClass / minimumTouchTargetDp | a11y 操作的档位与触控目标。 |
value | value 操作选中的档位值(字符串往返)。 |
解析侧完整能力(五档断点、短边/宽度双通道、回退链、余数逐列补 1 的像素级列宽、字号感知放大)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 21 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的解析逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | shared 模块全部解析器与类型原样编译,不提取、不重写;仅新增 KitFlowBridge 门面与 ohos 平台 actual 两个 Kotlin 源文件,另对 6 个文件做"剥 Compose 注解 / Dp→Int"的外科手术。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);manual-testing(Android/Desktop 手测 App)与 android / ios / js / wasmJs 平台源码集留盘不进构建。 |
| 桥接稳定 | 使用 1 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 解析层零第三方依赖——Compose 挪走后 commonMain 不依赖任何 kotlinx 制品,也就没有"制品线补齐"问题;上游测试仅 1 处断言随字段改名,其余零改动全绿。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:真实窗口联动、断点演练、回退链演示、网格重排、无障碍档位。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。KitFlow 上游的 Compose 门面(Provider / Locals / FlowGrid)整体隔离在
composeMain不参与 ohos 编译;ArkTS Demo 只是把桥暴露的解析语义画出来。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 commonMain 依赖形态、Compose 渗透面、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模 ── 四 op JSON 契约(window / value / grid / a11y)、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 真实窗口联动、密度换算、四 Tab 页面
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
shared/ 上游自适应库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
shared/src/commonMain/ 解析层:断点/窗口/方向/设备/网格/无障碍(零 Compose 依赖)
shared/src/composeMain/ 上游 Compose 门面存档(12 个文件,不参与 ohos 编译)
shared/src/composeTest/ 上游 compose-ui-test 存档(不参与编译)
shared/src/ohosMain/ 新增:getPlatform() actual
shared/src/jvmMain/ desktopMain 改名而来,仅保留 getPlatform() actual
shared/src/{androidMain,iosMain,jsMain,wasmJsMain}/ 上游平台源码集,留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
manual-testing/ 上游手测 App(Android/Desktop),留盘不进 settings
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :shared 与 :example:nativeApp,rootProject.name = "KitFlow"。上游的 AGP / CMP / Compose Compiler / vanniktech 发布插件全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(org.gradle.java.home / JAVA_HOME 指向本机 Temurin 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 从上游 9.1.0 降级;2.2.21 线不支持 Gradle 9) |
| kotlinx 制品线 | 不需要——解析层零第三方依赖 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.kitflow |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
对 commonMain 的 21 个 Kotlin 文件逐一分类(这是本适配的成败手):
| 类别 | 文件数 | 代表文件 | 处置 |
|---|---|---|---|
| 纯逻辑 | 16 | WindowInfoResolver / BreakpointResolver / AdaptiveFlowGridResolver / AdaptiveAccessibilityResolver / 各 enum 与 data class | 原样编译(jvm + 双 ohos) |
| Compose 门面 | 12(计 4 个根级 + 1 个网格 + 7 个无障碍门面) | AdaptiveKitProvider / AdaptiveLocals / AdaptiveFlowGrid / AdaptiveIconButton | 挪 composeMain 存档,不参与 ohos 编译 |
| 纯逻辑但带 Compose 注解 | 4 | AdaptiveBreakpointThresholds / AdaptiveWindowInfo / AdaptiveAccessibilityThresholds(@Immutable) | 剥注解后留在 commonMain |
| Compose 类型入参 | 2 | AdaptiveAccessibilityInfo / AdaptiveAccessibilityResolver(minimumTouchTarget: Dp) | Dp → Int(minimumTouchTargetDp),逻辑不动 |
| Compose expect | 1 处 | rememberPlatformAccessibilityInfo() | expect 与各平台 actual 一并挪存档 |
expect 声明全库只有 2 处:getPlatform()(保留,ohos 补 actual)与 rememberPlatformAccessibilityInfo()(Compose 专属,随门面隔离)。这就是整个 fork 在 Kotlin 侧的全部新增源文件:
// shared/src/ohosMain/kotlin/com/adaptive/kit_flow/platform/KitFlowPlatform.ohos.kt
class OhosPlatform : Platform {
override val name: String = "HarmonyOS (Kotlin/Native)"
}
actual fun getPlatform(): Platform = OhosPlatform()
上游 AdaptiveWindowInfo 的 widthDp / heightDp 本来就是 Int,sanitizeDp() 也是取整——dp 全链路 Int 是上游自己的约定,这让解析层天然离 Compose 很近,是"隔离即可适配"的根本原因。
1.4 页面边界
标题区 KitFlow · OpenHarmony / 副标题(CMP 自适应布局库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 窗口断点 / 布局值 / 流式网格 / 无障碍
窗口断点 Tab 当前窗口(vp 实测,onAreaChange 联动)、宽高滑杆演练区、
screenClass/layoutClass/orientation/device 四联卡、短边/长边/宽高比/阈值信息行
布局值 Tab 五档断点切换、sm/md/lg 固定值卡、tab/desktop 滑杆 + 提供开关、
解析结果大数字卡(回退链演示)、padding 预览卡片
流式网格 Tab 容器实测宽度行、最小列宽/最大列数/卡片数/列间距/字号缩放滑杆、
字号感知开关、columns/rowCount/有效最小列宽三联卡、逐列宽度彩色卡片网格
无障碍 Tab 字号缩放滑杆、减弱动效/高对比度开关、档位三联卡、
触控目标对照预览(40dp 偏小 vs 达标尺寸)
状态行 最近一次调用结果(如 "a11y 1.00 -> Normal")
页面启动时读取 display.getDefaultDisplaySync().densityPixels 备用,根节点 onAreaChange 实测窗口 vp 尺寸。
第 2 阶段:目标与依赖打通
2.1 依赖形态验证(本适配的成败手)
先盘点 commonMain 的依赖形态:把 12 个 Compose 门面文件挪走、4 个文件的 @Immutable 剥掉、2 个文件的 Dp 换成 Int 之后,shared 的 commonMain 不依赖任何第三方库——上游的 compose-runtime / foundation / material3 / ui 坐标全部属于被隔离的门面。三个关键结论:其一,没有 kotlinx 依赖就没有"制品线补齐"问题,不需要像 KStore 那样钉 2.4.20 线;其二,工具链可以跟社区标准钉法走(HarmonyOS Kotlin 2.2.21-1.0.0,qrose / haze 同款);其三,上游纯逻辑测试(kotlin-test 即可跑)原样保留,交叉验证零成本。
2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
上游是 Kotlin 2.4.0 / Gradle 9.1.0 / AGP 9.0.1。HarmonyOS Kotlin 2.2.21 线的 Gradle 插件不支持 Gradle 9,AGP 9.0.1 又强依赖 Gradle 9——两者不可兼得,而本 fork 不编 Android,取舍一目了然:wrapper 降 8.14.1、AGP 整体剥离、Kotlin 钉 2.2.21-1.0.0。同时删除上游的 gradle/gradle-daemon-jvm.properties(Gradle 9 专属的 daemon 工具链文件,8.14.1 下反而会触发 foojay 下载),settings.gradle.kts 里的 foojay resolver 插件一并移除,JDK 21 走 org.gradle.java.home / JAVA_HOME。
settings.gradle.kts 把 eazytec 仓库写在最前:
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 = "KitFlow"
include(":shared")
include(":example:nativeApp")
2.3 编译真实解析器
shared/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用(上游 jvm("desktop") 改名,源码集 desktopMain → jvmMain)
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖,这是盘点后的结论而不是省略
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
./gradlew :shared:compileKotlinJvm # BUILD SUCCESSFUL
./gradlew :shared:jvmTest # 21 项用例全绿
./gradlew :shared:compileKotlinOhosArm64 :shared:compileKotlinOhosX64 # BUILD SUCCESSFUL
断点、窗口、网格、无障碍四个解析器与全部类型一个没动,新增的只有 KitFlowBridge 门面与 ohos actual。
2.4 测试与 Compose 存档的处置:compose-ui-test 编不进 ohos
上游 commonTest 依赖 compose-ui-test(AdaptiveFlowGridUiTest 用 runComposeUiTest 量布局边界),commonTest 的依赖表对 ohos 测试编译同样可见,解析不到 ohos 变体就会卡死测试编译。处置是两条纪律的组合:UI 测试挪入未接线的 composeTest 源码集(与门面同等待遇,等 CMP 对齐后接回),纯逻辑测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件只剩 1 处改动(触控目标断言随 minimumTouchTarget: Dp → minimumTouchTargetDp: Int 改名),其余一个字节不动。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.adaptive.kit_flow/**(上游解析器 + 类型,原样编译)
↓ WindowInfoResolver / BreakpointResolver / AdaptiveFlowGridResolver / AdaptiveAccessibilityResolver
KitFlowBridge(公开门面,包装 internal resolver)
↓
Bridge.kt(@CName × 1,扁平 JSON 进出,四 op 分发)
↓ UTF-8 JSON
N-API kitflowResolve(request)
↓
ArkUI Index.ets(四 Tab)
请求与响应:
请求 : {"op":"window","width":390,"height":844,"md":360,"lg":480,"tab":600,"desktop":840}
{"op":"value","breakpoint":"TAB","sm":"12","md":"16","lg":"20","tab":"","desktop":""}
{"op":"grid","availableWidthPx":2532,"minColumnWidthPx":720,"horizontalSpacingPx":36,
"maxColumns":4,"itemCount":12,"fontScale":1.0,"fontScaleAware":true}
{"op":"a11y","fontScale":1.2,"reducedMotion":false,"highContrast":false}
响应 : {"ok":true,"op":"window","screenClass":"MD","layoutClass":"MD","orientation":"Portrait",
"device":"Mobile","shortestSideDp":390,"longestSideDp":844,"aspectRatio":0.4620853,...}
{"ok":true,"op":"grid","columns":3,"columnWidths":[826,825,825],"rowCount":4,
"effectiveMinColumnWidthPx":720}
{"ok":false,"error":"..."}
两个不对称设计:window 的阈值覆盖字段(md/lg/tab/desktop)缺省时用上游默认,只在传入时生效;grid 的所有长度字段用 px(上游 resolver 的原生契约),dp→px 换算收敛在 ArkTS 一处(乘屏幕密度),避免两处换算口径不一。
3.2 解析器直连为什么不复刻
桥内分发这样写:
@CName("KitflowResolve")
fun kitflowResolve(request: String): String {
return try {
val json = MiniJson.parse(request)
when (json.str("op").ifBlank { "window" }) {
"value" -> resolveValueOp(json)
"grid" -> resolveGridOp(json)
"a11y" -> resolveA11yOp(json)
else -> resolveWindowOp(json)
}
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
}
四个 resolveXxxOp 内部全部直连 KitFlowBridge——上游 resolver 都是 internal,桥模块访问不到,所以仿照 qrose 适配新增 Qrose.encodeQr 的先例,在 shared/commonMain 加了一个公开门面 KitFlowBridge,把 resolveWindow / resolveBreakpoint / resolveGrid / resolveAccessibility / resolveValue 五个函数暴露出来。门面只做转发,一个算法字符都不复制。op:"value" 的值以字符串往返,是因为上游 Adaptive.value<T> 是泛型,跨 C ABI 只有标量可用,字符串是最大公约数。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI。非法 JSON、未知 op、未知断点名(返回明确 error 而不是静默回落)、grid 参数越界(上游 resolver 的 require 语义原样透传,如 minColumnWidthPx 必须大于 0),全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libkitflow_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoskitflow.so)
│ KitflowResolve
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;让 ArkTS 自己实现断点逻辑又会让语义漂移,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("KitflowResolve")
fun kitflowResolve(request: String): String // window / value / grid / a11y 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
KitflowResolve | kitflowResolve(req) | 四个 resolver 全部入口 + 阈值覆盖 + 错误通道 |
单符号是刻意设计:四个 op 共享同一份 MiniJson 解析与错误通道,ArkTS 侧也只需要 JSON.parse 一条路。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"kitflowResolve", nullptr, NapiKitflowResolve, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr}};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskitflow_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskitflow_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoskitflow SHARED IMPORTED)
set_target_properties(ohoskitflow PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(kitflow_napi SHARED napi_init.cpp)
target_link_libraries(kitflow_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoskitflow)
OHOS_ARCH 为空或 libohoskitflow.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const kitflowResolve: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 真实窗口联动和触发解析
aboutToAppear(): void {
this.density = display.getDefaultDisplaySync().densityPixels;
this.resolveWindow();
this.resolveValue();
this.resolveA11y();
}
private resolve(req: Record<string, string | number | boolean>): string {
return kitflowResolve(JSON.stringify(req));
}
private resolveGrid(): void {
const availablePx = Math.round(this.gridBoxVp * this.density); // vp × 密度 = px
const raw = this.resolve({ op: 'grid', availableWidthPx: availablePx, ... });
this.gridRes = JSON.parse(raw) as GridResult; // @State 刷新三联卡与网格
}
根节点 onAreaChange 实测窗口 vp 尺寸(vp 即 dp),"同步到演练区"按钮把实测值灌进滑杆;网格 Tab 的卡片容器单独 onAreaChange,可用宽度变化时自动重新解析列算。
5.2 页面交互
四个 Tab 自上而下:窗口断点 Tab 是"实测 + 演练"双区,滑杆拖宽到 840 dp 时 layoutClass 跳 DESKTOP 而短边 screenClass 不跳;布局值 Tab 用开关演示回退链——关掉 tab 档,TAB 断点取值立即回落 lg 的 20;流式网格 Tab 的彩色卡片宽度直接绑定 columnWidths(px÷密度回 vp),拖滑杆观察重排;无障碍 Tab 用 40dp 偏小按钮与达标尺寸按钮做触控目标对照。每次调用后状态行显示 op 输入 -> 核心结果(如 a11y 1.00 -> Normal)。
5.3 对比度和错误边界
选中态 Tab 深色底白字、非选中浅灰底深字;四联结果卡用四种语义色区分;ok:false 时错误原文进状态行。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoskitflow)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 1 + 四 op 分发 + MiniJson(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.kitflow、versionName 1.0.4-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohoskitflow_api.h
├── src/main/cpp/types/libkitflow_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
./gradlew :shared:jvmTest # 21 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填带 material 的完整配置)。
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.kitflow -m entry
四、完整代码对照
4.1 整体架构
com.adaptive.kit_flow/**(上游解析器原样编译)
│ WindowInfoResolver / BreakpointResolver / AdaptiveFlowGridResolver / AdaptiveAccessibilityResolver
com.adaptive.kit_flow.KitFlowBridge(公开门面,转发 internal resolver)
│
Bridge.kt + MiniJson
│ KitflowResolve(@CName × 1,四 op)
libkitflow_napi.so
│ kitflowResolve
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(四 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、manual-testing 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版(唯一插件别名) |
build.gradle.kts(根) | 版本 1.0.4-ohos.1、全局禁用 Ohos*Test* 任务 |
shared/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64、零三方依赖 |
shared/src/commonMain/.../KitFlowBridge.kt | 新增门面(五个 resolve 转发) |
shared/src/ohosMain/.../KitFlowPlatform.ohos.kt | 新增 actual(OhosPlatform) |
shared/src/composeMain/(12 文件) | 上游 Compose 门面存档(git mv 保留历史) |
shared/src/composeTest/(1 文件) | compose-ui-test 存档 |
shared/src/jvmMain/ | desktopMain 改名,仅保留 getPlatform actual |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、四 op 分发、MiniJson、错误通道 |
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 四 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(25 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 解析器 | WindowInfoResolver.resolve(w, h, thresholds) | 双通道窗口解析(短边 screenClass / 宽度 layoutClass) |
| 解析器 | BreakpointResolver.resolve(bp, sm…desktop) | 断点取值(tab→lg、desktop→tab→lg 回退链) |
| 解析器 | AdaptiveFlowGridResolver.resolveColumnCount/Widths | 网格列算(px 契约、余数逐列补 1) |
| 解析器 | AdaptiveAccessibilityResolver.resolve | 字号档位与最小触控目标 |
| 门面 | KitFlowBridge.resolveWindow / resolveGrid / resolveAccessibility / resolveValue | 公开转发(桥的唯一入口) |
| Compose 存档 | AdaptiveKitProvider / Adaptive.value / AdaptiveFlowGrid | 不参与 ohos 编译(composeMain) |
| Native | KitflowResolve | C ABI JSON 导出(单符号四 op) |
| N-API | kitflowResolve(req) | 向 ArkTS 暴露能力 |
| ArkTS | resolveWindow() / resolveGrid() / resolveA11y() / resolveValue() | 触发解析并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const raw = kitflowResolve(JSON.stringify({
op: 'window', width: 390, height: 844
}));
const res = JSON.parse(raw) as WindowResult;
// res.screenClass === 'MD' && res.layoutClass === 'MD' && res.orientation === 'Portrait'
private fun resolveWindowOp(json: MiniJson): String {
val info = KitFlowBridge.resolveWindow(
widthDp = json.int("width", 0),
heightDp = json.int("height", 0),
thresholds = thresholdsOf(json) // md/lg/tab/desktop 缺省用上游默认
)
return """{"ok":true,"op":"window","screenClass":"${info.screenClass.name}",...}"""
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:解析层真编译 + Compose 门面隔离,而不是 ArkTS 重写
上游"不拥有 UI"的设计原则让适配面天然最小:12 个 Compose 门面文件是薄壳,真正的语义全在四个 resolver 里。挪门面、真编译解析器,断点阈值、回退链、列算公式、像素级分配全部是上游原件;ArkTS 重写则两套实现必然漂移,上游测试也无法复用。
决策 2:工具链降到 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1
KStore 因为 coroutines 制品线拉力升到 2.4.20 线、Gradle 沿用 9.x;KitFlow 方向恰好相反——解析层零依赖、没有版本拉力,跟社区标准钉法(2.2.21-1.0.0)走,代价是 Gradle 必须从 9.1.0 降到 8.14.1,同时剥离 AGP(它强依赖 Gradle 9)。两条工具链线的经验不能互相套用,但原则是同一条:工具链版本跟着依赖约束走。
决策 3:根构建替换(cmp_qrose 模式),上游遗留留盘
上游根构建(AGP / CMP / Compose Compiler / vanniktech 发布 / foojay)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。manual-testing(Android/Desktop 手测 App)与 shared/src/{androidMain,iosMain,jsMain,wasmJsMain} 留盘不进 settings.gradle.kts——源码保留等 CMP 对齐后接回,构建不背历史包袱。
决策 4:单入口四 op JSON 桥 + px 契约收敛
四个 op 共享一份 MiniJson 解析与错误通道,C ABI 恒为 const char* -> const char*;grid 的长度字段全部用 px(上游 resolver 的测量阶段契约),dp→px 换算只发生在 ArkTS 一处(乘屏幕密度),跨语言不会出现两套口径。
决策 5:jvmTest 交叉验证 + 测试近零改动
解析逻辑在 commonMain,JVM 与 ohos 跑同一份代码;21 项 jvmTest 跑的就是上游原测试文件,是"解析器没漂"的闸门。Dp→Int 的字段改名只动了 1 处断言(.minimumTouchTarget.value.toInt() → .minimumTouchTargetDp),compose-ui-test 挪存档而不是删除——交叉验证的有效性以"测试近零改动"为前提。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :shared:jvmTest
21 项用例全部通过(上游原测试文件,仅 1 处断言随字段改名):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
AdaptiveResolverTest | 9 | 断点回退链、宽度→断点边界(359/360/480/600/840)、横屏短边不跳档、零尺寸安全默认、方形方向 |
AccessibilityResolverTest | 5 | 字号三档边界(1.0/1.2/1.5)、阈值非法 fail-fast、触控目标随档位增长(48/56/64) |
AdaptiveFlowGridResolverTest | 7 | 两列精确边界(491/492)、旋转重排、列数被上限与子项数收紧、字号感知减列、非法 fontScale 回落、像素级列宽分配、非法输入 fail-fast |
手工对账样例(JVM 与真机同值):844px 容器 / 最小列宽 240 / 间距 12 → 3 列 [274, 273, 273];字号感知开、1.5x → 有效最小列宽 360、降 2 列;对账公式 列宽和 + 间距×(列数-1) == 可用宽。
6.3 HAP 验证
:shared:compileKotlinJvm / compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:shared:jvmTest: 21 项全绿
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: KitflowResolve / libohoskitflow_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libkitflow_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
动态库实测尺寸:libohoskitflow.so arm64 约 3.6 MB、x86_64 约 3.3 MB(含 Kotlin/Native 运行时),libc++_shared.so 约 1.8 / 1.7 MB,六件套随 HAP 打包。
6.4 功能用例
- 启动进入窗口断点 Tab,"当前窗口(vp 实测)"显示真机实测尺寸(本次真机为 366 × 742 vp),状态行初始化为最近一次解析结果;
- 演练区 390 × 844 dp 时四联卡显示
screenClass=MD、layoutClass=MD、orientation=Portrait、device=Mobile,信息行shortestSideDp=390、longestSideDp=844、aspectRatio=0.4620853——与 JVM 侧同输入输出逐字一致; - 宽度拖过 840 dp 时
layoutClass跳 DESKTOP,而短边screenClass保持 MD——双通道语义在真机滑杆上肉眼可见; - 布局值 Tab 关掉 tab 档开关,TAB 断点取值立即回落 20(lg);desktop 档同理演示两级回落;
- 流式网格 Tab 卡片宽度实时跟随
columnWidths重排,字号感知开启、字号拉到 1.50 时列数下降一档; - 无障碍 Tab 字号 1.00 → Normal(触控目标 48dp),1.20 → Large(56dp),1.50 → ExtraLarge(64dp),触控目标对照区同步放大;
- 全程无闪退、无 ANR,
hdc shell hilog | grep Kitflow可见 NAPI 层调用日志(TAG=KitflowNapi)。
6.5 验证结论
四个解析器、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的自适应解析会话:实测窗口联动、断点双通道、回退链、网格重排与无障碍档位(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/KitFlow.git
cd KitFlow
export JAVA_HOME="/path/to/jdk-21"
./gradlew :shared:compileKotlinOhosArm64 :shared:compileKotlinOhosX64 # klib
./gradlew :shared:jvmTest # 21 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
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.kitflow -m entry
hdc shell hilog | grep Kitflow
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:shared真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游 Gradle 9.1.0 wrapper 下 HarmonyOS Kotlin 2.2.21 插件解析失败——降 8.14.1 的同时必须删掉
gradle/gradle-daemon-jvm.properties(Gradle 9 专属 daemon 工具链文件,留着会触发 foojay 下载链),foojay resolver 插件也一并移除; - wrapper 脚本启动用的是系统
JAVA_HOME,~/.gradle/gradle.properties里的org.gradle.java.home救不了 wrapper 本身——命令行先 export 再跑; - Compose 文件没挪干净时,错误往往不在那个文件本身,而是
commonMain编译报androidx.compose不存在——逐文件核对@Immutable/@Composable/Dp/CompositionLocal四类渗透点; - 目标从
jvm("desktop")改成jvm()后源码集改名(desktopMain → jvmMain),旧目录里若残留 actual 会与新文件双声明冲突——改名要连文件一起挪; minimumTouchTarget: Dp这种"类型在 Compose、语义在解析层"的字段,改Int时要同步改测试断言,否则commonTest编译期才炸;- 上游 resolver 全是
internal,桥模块够不着——加公开门面(KitFlowBridge)转发而不是复制算法; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - ArkTS 报
does not provide an export named 'kitflowResolve'时先查两处:是否 import 了 KN so(必须只 importlibkitflow_napi.so)、nm_modname是否等于 “kitflow_napi”; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - DevEco 打开工程会生成
.clangd/.clang-tidy,属 IDE 本地配置,不入库——追加 gitignore,别混进提交; build-profile.json5的signingConfigs条目缺material字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;- CAdapter 生成的
api.cpp:448 warning: non-void function does not return a value是工具链噪音,忽略即可(qrose 适配同样出现)。
8.2 已知问题
- Compose 门面(
AdaptiveKitProvider/Adaptive.value/AdaptiveFlowGrid/rememberWindowInfo)不参与 ohos 编译,存档于composeMain,等社区 CMP 与上游 Compose/Kotlin 对齐后接回; - 无障碍平台实参(fontScale / reducedMotion / highContrast)由 ArkTS 采集后经 JSON 传入,桥不自取系统设置;Demo 里是滑杆模拟,接
@ohos.accessibility/ Configuration 是业务侧的事; op:"value"的值以字符串往返,数字/枚举由调用方转换;op:"a11y"的档位阈值固定用上游默认(large 1.20 / extraLarge 1.50),桥未开放覆盖字段;AdaptiveFlowGridUiTest(compose-ui-test)存档于composeTest不参与 CI;android / ios / js / wasmJs 目标剥离(源文件留在树里),本 fork 不发 Maven Central;- HAP 安装必须使用与 bundle name
org.terminator.ohos.kitflow匹配的签名。
8.3 未来优化方向
- 接入真机系统字号与减弱动效设置(
@ohos.accessibility),替代 Demo 滑杆; - 给
op:"a11y"开放阈值覆盖字段、给桥加批量解析入口(一次请求多个 op); - 模拟器(x86_64)与其他设备形态(折叠屏分屏)的验收矩阵和 uitest 自动化截图脚本;
- 社区 CMP 对齐后把
composeMain/composeTest接回编译,ArkTS Demo 降级为 fallback; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套。
九、总结
9.1 核心难点回顾
上游 shared(解析层 + Compose 门面混在 commonMain)
→ 源码依赖度盘点(21 文件逐个分类 Compose 渗透点)
→ Compose 门面挪 composeMain 存档(git mv 保留历史)
→ 剥 @Immutable / Dp→Int / Compose expect 随门面隔离
→ 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1、AGP 剥离)
→ 根构建替换(cmp_qrose 模式)
→ ohosArm64 / ohosX64 KLIB + KitFlowBridge 门面 + ohos actual
→ C ABI(@CName × 1,四 op JSON)
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机四 Tab 验收(实测窗口 / 回退链 / 网格重排 / 无障碍档位)
9.2 封装层次
- 解析层:上游原版
WindowInfoResolver/BreakpointResolver/AdaptiveFlowGridResolver/AdaptiveAccessibilityResolver与全部类型; - 门面层:
KitFlowBridge公开转发(桥不触碰 internal); - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、密度换算、解析分发、结果卡片与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- "解析与渲染分离"的库适配面天然最小:KitFlow 上游把语义全部放在 resolver、把 Compose 当薄壳门面的设计,让 ohos 适配只需要"挪门面 + 真编译"两步——库的架构决定适配成本,这不是适配时能改变的,但选库和评审时值得优先看这一点;
- 工具链版本跟着依赖约束走:KStore 被 coroutines 制品线拉到 2.4.20 线、Gradle 留 9.x,KitFlow 零依赖、跟社区标准 2.2.21 线、Gradle 降 8.14.1——方向相反、原则同一条;反着套用任何一条历史结论都会在配置期碰壁;
- 交叉验证是"解析器没漂"的唯一可信证明:21 项
jvmTest跑的是上游原测试文件,近零改动才让"隔离而非重写"有说服力;真机四联卡上的MD / Portrait / 0.4620853与 JVM 逐字一致,就是这条链路的最终闭环。
9.4 适配成果
当前适配已完成:
- KitFlow
1.0.4解析层的双 ABI Kotlin/Native 编译(上游commonMain解析器与类型零重写,新增KitFlowBridge门面与 ohos actual 两个源文件); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面(实测窗口联动、断点双通道、回退链、网格重排、无障碍档位);
- 21 项 JVM 交叉验证全绿(上游原测试文件,仅 1 处断言随字段改名)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- 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)