本文记录 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 公开门面与一个 @CName C 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 / devicewindow 操作的核心解析结果。
columns / columnWidths / rowCount / effectiveMinColumnWidthPxgrid 操作的列算结果。
fontScaleClass / minimumTouchTargetDpa11y 操作的档位与触控目标。
valuevalue 操作选中的档位值(字符串往返)。

解析侧完整能力(五档断点、短边/宽度双通道、回退链、余数逐列补 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(org.gradle.java.home / JAVA_HOME 指向本机 Temurin 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 从上游 9.1.0 降级;2.2.21 线不支持 Gradle 9)
kotlinx 制品线不需要——解析层零第三方依赖
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.kitflow
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点

对 commonMain 的 21 个 Kotlin 文件逐一分类(这是本适配的成败手):

类别文件数代表文件处置
纯逻辑16WindowInfoResolver / BreakpointResolver / AdaptiveFlowGridResolver / AdaptiveAccessibilityResolver / 各 enum 与 data class原样编译(jvm + 双 ohos)
Compose 门面12(计 4 个根级 + 1 个网格 + 7 个无障碍门面)AdaptiveKitProvider / AdaptiveLocals / AdaptiveFlowGrid / AdaptiveIconButton挪 composeMain 存档,不参与 ohos 编译
纯逻辑但带 Compose 注解4AdaptiveBreakpointThresholds / AdaptiveWindowInfo / AdaptiveAccessibilityThresholds(@Immutable)剥注解后留在 commonMain
Compose 类型入参2AdaptiveAccessibilityInfo / AdaptiveAccessibilityResolver(minimumTouchTarget: Dp)Dp → Int(minimumTouchTargetDp),逻辑不动
Compose expect1 处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 方法覆盖内核
KitflowResolvekitflowResolve(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.tomlKotlin 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.kt1 个 C ABI、四 op 分发、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 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)
NativeKitflowResolveC ABI JSON 导出(单符号四 op)
N-APIkitflowResolve(req)向 ArkTS 暴露能力
ArkTSresolveWindow() / 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 处断言随字段改名):

测试类用例数钉住什么
AdaptiveResolverTest9断点回退链、宽度→断点边界(359/360/480/600/840)、横屏短边不跳档、零尺寸安全默认、方形方向
AccessibilityResolverTest5字号三档边界(1.0/1.2/1.5)、阈值非法 fail-fast、触控目标随档位增长(48/56/64)
AdaptiveFlowGridResolverTest7两列精确边界(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 功能用例

  1. 启动进入窗口断点 Tab,"当前窗口(vp 实测)"显示真机实测尺寸(本次真机为 366 × 742 vp),状态行初始化为最近一次解析结果;
  2. 演练区 390 × 844 dp 时四联卡显示 screenClass=MD、layoutClass=MD、orientation=Portrait、device=Mobile,信息行 shortestSideDp=390、longestSideDp=844、aspectRatio=0.4620853——与 JVM 侧同输入输出逐字一致;
  3. 宽度拖过 840 dp 时 layoutClass 跳 DESKTOP,而短边 screenClass 保持 MD——双通道语义在真机滑杆上肉眼可见;
  4. 布局值 Tab 关掉 tab 档开关,TAB 断点取值立即回落 20(lg);desktop 档同理演示两级回落;
  5. 流式网格 Tab 卡片宽度实时跟随 columnWidths 重排,字号感知开启、字号拉到 1.50 时列数下降一档;
  6. 无障碍 Tab 字号 1.00 → Normal(触控目标 48dp),1.20 → Large(56dp),1.50 → ExtraLarge(64dp),触控目标对照区同步放大;
  7. 全程无闪退、无 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :shared 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游 Gradle 9.1.0 wrapper 下 HarmonyOS Kotlin 2.2.21 插件解析失败——降 8.14.1 的同时必须删掉 gradle/gradle-daemon-jvm.properties(Gradle 9 专属 daemon 工具链文件,留着会触发 foojay 下载链),foojay resolver 插件也一并移除;
  4. wrapper 脚本启动用的是系统 JAVA_HOME,~/.gradle/gradle.properties 里的 org.gradle.java.home 救不了 wrapper 本身——命令行先 export 再跑;
  5. Compose 文件没挪干净时,错误往往不在那个文件本身,而是 commonMain 编译报 androidx.compose 不存在——逐文件核对 @Immutable / @Composable / Dp / CompositionLocal 四类渗透点;
  6. 目标从 jvm("desktop") 改成 jvm() 后源码集改名(desktopMain → jvmMain),旧目录里若残留 actual 会与新文件双声明冲突——改名要连文件一起挪;
  7. minimumTouchTarget: Dp 这种"类型在 Compose、语义在解析层"的字段,改 Int 时要同步改测试断言,否则 commonTest 编译期才炸;
  8. 上游 resolver 全是 internal,桥模块够不着——加公开门面(KitFlowBridge)转发而不是复制算法;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. ArkTS 报 does not provide an export named 'kitflowResolve' 时先查两处:是否 import 了 KN so(必须只 import libkitflow_napi.so)、nm_modname 是否等于 “kitflow_napi”;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. DevEco 打开工程会生成 .clangd / .clang-tidy,属 IDE 本地配置,不入库——追加 gitignore,别混进提交;
  13. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  14. 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 三条经验

  1. "解析与渲染分离"的库适配面天然最小:KitFlow 上游把语义全部放在 resolver、把 Compose 当薄壳门面的设计,让 ohos 适配只需要"挪门面 + 真编译"两步——库的架构决定适配成本,这不是适配时能改变的,但选库和评审时值得优先看这一点;
  2. 工具链版本跟着依赖约束走:KStore 被 coroutines 制品线拉到 2.4.20 线、Gradle 留 9.x,KitFlow 零依赖、跟社区标准 2.2.21 线、Gradle 降 8.14.1——方向相反、原则同一条;反着套用任何一条历史结论都会在配置期碰壁;
  3. 交叉验证是"解析器没漂"的唯一可信证明: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。

参考文档

Logo

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

更多推荐