本文记录 FormValidator 接入 OpenHarmony 的完整过程,覆盖声明式表单校验库盘点、Compose 状态与验证引擎的源码集拆分、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

FormValidator 是基于 Jetpack Compose 的 Kotlin Multiplatform 声明式表单校验库:ValidationField 描述字段、Type 密封类描述规则(Required / Email / MustBeMoreThan / MustBeLessThan / MustBeInRange / MustBeEqualTo / Custom / Optional 共八种)、Flow 枚举描述校验方向(Down 自上而下短路 / Up 自下而上报最后 / Splash 全量一次校验),Form 组合函数把校验器挂进 Compose 树。本次适配与 KStore 的"真编译整库"路线有本质区别:社区 Maven 上没有能与上游对齐的 Compose 制品线(上游 Compose 1.10 / Kotlin 2.3,社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21),因此采用社区早期已过审的路线(qrose / haze 的"零依赖内核提取")——把验证引擎从 Compose 状态中拆出、以纯 Kotlin 编译成 OpenHarmony KLIB,Type 分发表、三种 Flow 语义、逐字错误消息全部运行在真机上,ArkUI 四 Tab 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/FormValidator.git

上游仓库: funyin/FormValidator

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

FormValidator 为 Compose Multiplatform 应用提供"声明式表单校验"这一件事的完整答案:一个 FormValidator 持有字段列表与流向,validate() 一次触发表单校验;每个 ValidationField 用 Type 声明规则,校验失败经 onError 回调驱动 UI 报错;Flow 决定"报第一个错、报最后一个错、还是全部一起报";errorSafe 把字段值、错误与脏状态收进一个可 remember 的单元。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套校验能力,但上游构建(Kotlin 2.3.0 / Gradle 8.11.1 / AGP 8.9.1 / Compose 1.10.0)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示输入框,却无法证明校验引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证规则引擎、Flow 短路语义、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.3.0 / Gradle 8.11.1 / AGP 8.9.1 与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵引擎核心类与 Compose 状态编织在一起:errorMessage / valid 是 mutableStateOf、LocalFormValidator 是 staticCompositionLocalOf——社区 CMP 1.9.2 制品线对不上上游 Compose 1.10。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
规则语义跨不了桥Custom 规则的校验 lambda 是 Kotlin 函数,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:验证引擎的纯 Kotlin 化与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。校验语义完全由上游原版实现负责(拆分时逐行保持),ArkTS 只负责调用 Native、解析 JSON、合并本地 Custom 规则和展示结果。

1.2 库提供的能力

Demo 提供一次真实的表单校验会话,对应桥接的全部入口:

  • 三种校验流:Down 自上而下短路在第一个非法字段(其后字段不再校验)、Up 全量校验后报告最后一个非法字段、Splash 全量校验且每个字段都拿到错误;
  • 八种规则类型:Required(非空白)、Email(正则)、MustBeMoreThan / MustBeLessThan(String 比模板长度、Number 比数值)、MustBeInRange(数值区间)、MustBeEqualTo(字符串全匹配 / 数字比长度 / 数值相等)、Optional(恒真)——错误消息逐字来自 Kotlin 引擎;
  • Custom 的 ArkTS 合并:Custom lambda 不跨 C ABI,Demo 的 Age 字段(偶数校验)在 ArkTS 实现后按所选 Flow 语义与原生结果合并;
  • 版本握手:formValidatorVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok请求是否合法,失败时 error 带原文(含 Custom 拒绝说明)。
valid本次 validate() 的整体结果。
errorMessage表单级错误:Down/Up 报肇事字段的消息,Splash 恒为 null(上游语义)。
fields[]逐字段结果:name / checked(本次是否真的被校验,仅 Down 短路后为 false)/ valid / error。

引擎侧完整能力(Type 分发表、Flow 状态机、onError 重置-回填协议)全部在上游逻辑里随编译进入 .so,JVM 侧保留 20 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的校验逻辑没有漂移。

1.3 实现适配

维度要求
代码复用验证语义逐行保持:commonMain/FormValidator.kt 重写为纯 Kotlin(只剥离 Compose 状态),上游两个原件逐字搬入 composeMain 保留;引擎零外部依赖。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 app/(Android demo)、docs/ 等留盘不进构建。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律引擎零依赖——不需要 kotlinx 制品线;Kotlin 钉 2.2.21-1.0.0(qrose 同款已过审钉法),Gradle 降 8.14.1。
UI 完整四 Tab 覆盖全部桥接入口:常规表单 / SnackBar / ErrorSafe / 全规则,附版本握手关于区。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游 Form / ValidationSnackBar / errorSafe 属于 Compose 1.10 源码集,等社区 CMP 制品线与上游对齐后由 composeMain 接回;ArkTS Demo 复刻的是上游 demo 的三种表单模式(逐字段错误 / SnackBar / ErrorSafe 实时校验)。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 Compose 制品线缺口、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模     ── flow/fields JSON 契约、值类型跟随字面量、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 四 Tab 表单、Custom 规则合并、版本握手
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
FormValidator/                         验证引擎模块(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
FormValidator/src/commonMain/          引擎(纯 Kotlin 重写,零依赖)
FormValidator/src/composeMain/         上游 Compose 原件逐字保留(不进任何 target 的编译)
FormValidator/src/commonTest/          20 项 JVM 交叉验证(本次新增)
example/nativeApp/                     Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存
app/ docs/ mkdocs-docs/ overrides/     上游 Android demo 与文档站,留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :FormValidator 与 :example:nativeApp,rootProject.name = "ohos_formvalidator"。上游的 AGP / Compose 插件、dokka、nmcp 发布、GPG 签名段全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / haze 同款)
JDK21(org.gradle.java.home 指向 Eclipse Adoptium jdk-21.0.12.101-hotspot)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(上游 8.11.1 拉不起 HarmonyOS Kotlin 2.2.21 插件线,必须降级)
引擎依赖零(commonMain.dependencies {} 为空——拆分后不碰任何 2.3 / Compose 1.10 制品)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.formvalidator
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
文件内容依赖形态处置
commonMain/.../FormValidator.kt验证引擎 + Compose 状态编织(385 行)compose.runtime(mutableStateOf、staticCompositionLocalOf)拆分:引擎留 commonMain(重写去状态,语义逐行保持),原件逐字存 composeMain
commonMain/.../Components.ktForm / ValidationSnackBar / ErrorSafeValue / errorSafecompose.material / animation / foundation原件逐字搬 composeMain,ohos 不编
app/上游 Android demo(三表单 Tab)AGP + Compose BOM留盘不进 settings

值得说明拆分的最小性:引擎 385 行里真正"Compose"的只有 6 行——LocalFormValidator 声明 1 行、errorMessage 与 valid 两个 mutableStateOf 属性 2 行、外加 import。剥离后 Type 密封类、ValidationField.valid() 的三分支分发表(String / Number / 其他)、validate() 的 Flow 状态机与上游逐字一致;上游原件存 composeMain,与 qrose 保留上游 Painter 的方式相同——工具链对齐后整体接回。

1.4 页面边界
标题区          FORMVALIDATOR · OPENHARMONY / 版本握手行(CMP 表单校验库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        常规表单 / SnackBar / ErrorSafe / 全规则
Flow 切换       Down / Up / Splash(三 Tab 共用,全规则同样生效)
表单区          Name(Required)+ Age(ArkTS Custom:偶数)+ Email(Email)
                全规则 Tab 为八规则齐测(Required/Email/MoreThan/LessThan/InRange/EqualTo×2/Optional)
操作区          Submit 按钮(ErrorSafe Tab 为实时校验,无按钮)
结果区          Form is valid / Form is not valid 卡片 + 表单级错误文本
关于区          版本握手、库简介、架构分层、仓库与社区链接

页面启动时先调 formValidatorVersion() 做握手;SnackBar Tab 失败时以 3000ms toast 复刻上游 SnackBarProperties 默认时长。

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

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有能与上游对齐的 Compose 制品。结论:社区 CMP 线停在 1.9.2(HarmonyOS Kotlin 2.2.21),上游已经站在 Compose 1.10 / Kotlin 2.3——compose.runtime 的状态 API 与编译器插件都无法同编。这就是路线分岔点:KStore 依赖的 kotlinx 四件套在 ohos 制品线上与上游同版本,可以"真编译整库";FormValidator 的依赖全部是 Compose 系,没有对齐的制品线,走"引擎提取"。

提取的一个意外收益:引擎拆出 Compose 状态后是零依赖纯 Kotlin——不需要 coroutines、不需要 serialization、更不需要 Compose 制品,commonMain.dependencies {} 一行都不用写,桥的 JSON 解析沿用 qrose / KStore 的 MiniJson 手写路线(@CName 入口没有 serializer 作用域)。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 / Gradle 8.14.1)

钉 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 已过审同款)。上游虽然用 Kotlin 2.3.0,但引擎拆出后不解析任何 2.3 编译的制品,没有"元数据 2.4.0 读不了"的问题;Gradle 方面 2.2.21 旧线必须用 8.14.1(上游 8.11.1 在配置期直接拉不起 HarmonyOS Kotlin 插件)。

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 = "ohos_formvalidator"

include(":FormValidator")
include(":example:nativeApp")
2.3 编译真实引擎

FormValidator/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}          // 零依赖引擎
        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

实测:

.\gradlew.bat :FormValidator:compileKotlinJvm                    # BUILD SUCCESSFUL
.\gradlew.bat :FormValidator:compileKotlinOhosArm64             # BUILD SUCCESSFUL
.\gradlew.bat :FormValidator:compileKotlinOhosX64               # BUILD SUCCESSFUL

Type 分发表与 Flow 状态机一个没动,变的只有 6 行 Compose 状态;上游原件在 composeMain 等待接回。

2.4 测试的处置:上游没有实质测试,为引擎补交叉验证

上游 src/test 里只有模板生成的 ExampleUnitTest,没有一行校验行为测试——这与 KStore"上游 51 项测试零改动照跑"的处境相反。提取出的引擎需要自己的闸门:本次新增 commonTest/FormValidatorTest.kt 20 项用例,覆盖 8 种规则 × String/Number/其他值类型、三种 Flow、onError 回调协议,以及第 5 节要讲的上游怪癖。测试统一在 JVM 跑(jvmTest),根构建全局禁用 ohos 测试任务:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

第 3 阶段:桥协议建模

3.1 JSON 契约
com.initbase.formvalidator/**(上游引擎,拆分后原样编译)
        ↓ FormValidator / ValidationField / Type / Flow
Bridge.kt(@CName × 2,扁平 JSON + 一层对象数组)
        ↓ UTF-8 JSON
N-API formValidatorValidate() / formValidatorVersion()
        ↓
ArkUI Index.ets(四 Tab 滚动页)

请求与响应:

请求 : {"flow":"Down","fields":[
         {"name":"Name","type":"Required","value":"Ada"},
         {"name":"Age","type":"InRange","min":0,"max":120,"value":30},
         {"name":"Code","type":"MoreThan","template":"123456","value":"1234567"},
         {"name":"Pin","type":"EqualTo","template":1111,"value":"1234"},
         {"name":"Note","type":"Optional","value":null}]}
响应 : {"ok":true,"valid":false,"errorMessage":"Email is not a valid email",
         "fields":[{"name":"...","checked":true,"valid":false,"error":"..."}]}
       {"ok":false,"error":"..."}

一个对称设计:value / template 的类型跟随 JSON 字面量——带引号解析为 String、null 解析为 null、数字字面量解析为 Number。这正好对上上游 when (value) { is String -> ... is Number -> ... } 的分发语义,桥上不需要额外的类型标注字段。

3.2 载荷类型为什么跟随字面量 + Custom 为什么明确拒绝

桥内校验这样跑:

val nodes = MiniJson.parseArray(fieldsRaw)
val fields = nodes.mapIndexed { index, node ->
    FormValidator.ValidationField(
        value = classifyValue(node.raw("value")),   // 引号→String,null→null,数字→Number
        name = node.str("name").ifBlank { "Field" },
        type = buildType(node),                      // "Required"/"Email"/"MoreThan"/... → 上游 Type
        onError = { lastErrors[index] = it }         // 记录每个字段最后一次回调
    )
}
val validator = FormValidator(fields = fields, flow = flow)
val valid = validator.validate()

buildType 把 "MoreThan" 一类的 kind 映射回上游密封类;"Custom" 被显式拒绝并返回说明文案——校验 lambda 是 Kotlin 函数,序列化不了。替代路径是 ArkTS 实现谓词后按 Flow 语义合并(Demo 的 Age 字段示范):Down 在"完整字段序"上短路、Up 报最后一个非法字段、Splash 全量展示。checked 字段由 field.errorMessage != null 推出——valid() 总会先赋默认消息,所以短路后未触达的字段自然为 false,与上游"重置后未再校验"的表现一致。

引擎是同步纯函数,桥不需要 runBlocking 收 suspend(KStore 桥需要)——这是这条路线里最轻的一类。

3.3 错误通道

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

@CName("FormValidatorValidate")
fun formValidatorValidate(request: String): String {
    return try {
        /* 解析 → 建字段 → validate() → 拼响应 */
    } catch (t: Throwable) {
        """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
    }
}

非法 JSON、缺 fields、未知 type、模板缺失、Custom 跨界,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到状态行。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libformvalidator_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosformvalidator.so)
  │ FormValidatorValidate / FormValidatorVersion
  ▼
UTF-8 JSON + DisposeString

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

4.2 Kotlin/Native 导出函数
@CName("FormValidatorValidate")
fun formValidatorValidate(request: String): String   // 一次完整表单校验,JSON 进 JSON 出

@CName("FormValidatorVersion")
fun formValidatorVersion(): String = "1.0.7-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
FormValidatorValidateformValidatorValidate(req)FormValidator 构造 + 8 种 Type + 3 种 Flow + validate()
FormValidatorVersionformValidatorVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"formValidatorValidate", nullptr, NapiFormValidatorValidate, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"formValidatorVersion", nullptr, NapiFormValidatorVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosformvalidator_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosformvalidator_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohosformvalidator SHARED IMPORTED)
set_target_properties(ohosformvalidator PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(formvalidator_napi SHARED napi_init.cpp)
target_link_libraries(formvalidator_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosformvalidator)

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

export const formValidatorValidate: (request: string) => string;
export const formValidatorVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 版本握手和触发校验
aboutToAppear(): void {
  this.version = formValidatorVersion();          // 版本握手:来自真实 Native 调用
}

private submitRegular(): void {
  const fieldsReq: FieldReq[] = [
    { name: 'Name', type: 'Required', value: this.rName },
    { name: 'Email', type: 'Email', value: this.rEmail }
  ];
  const res = this.callBridge(fieldsReq);          // JSON.stringify → native → JSON.parse
  const merged = this.mergeForm(res, this.rAge);   // Age 的 Custom 规则 ArkTS 侧合并
  this.rValid = merged.valid;
  this.rFormError = merged.formError;
  /* 逐字段错误写入 @State,UI 自动刷新 */
}

mergeForm 把原生结果(Name / Email)与本地谓词(Age 偶数)放进完整字段序 [Name, Age, Email] 按所选 Flow 归并:Down 短路在第一个非法字段、其后的错误清空;Up 报最后一个非法字段的消息;Splash 全量展示且表单级消息留空——与上游语义逐条对齐。

5.2 页面交互

四个 Tab 自上而下共享 Flow 切换行:常规表单(逐字段错误 + 表单级错误 + Submit)、SnackBar(失败弹 3000ms toast,不展示逐字段错误——对齐上游"一次只报一条"的设计说明)、ErrorSafe(每次输入即时校验 + Form has unsaved changes 脏状态提示,无 Submit)、全规则(八规则齐测,错误消息逐字来自引擎)。关于区展示版本握手行、库简介、ArkTS → NAPI → C ABI → Kotlin 引擎 分层说明与仓库 / 社区链接。

5.3 对比度和错误边界

主操作深色按钮白字,校验结果卡绿色(valid)/ 红色(invalid)语义对齐;输入框错误态红色描边加粗;ok:false 时错误原文进状态行与 hilog。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosformvalidator)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/org/terminator/ohos/formvalidator/
│       └── Bridge.kt                         @CName × 2 + MiniJson(扁平 JSON + 一层对象数组)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.formvalidator、versionName 1.0.7-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/libohosformvalidator_api.h
        ├── src/main/cpp/types/libformvalidator_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
.\gradlew.bat :FormValidator:jvmTest                                # 20 项交叉验证
.\gradlew.bat :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
hdc -t <设备序列号> install -r entry\build\default\outputs\default\entry-default-signed.hap
hdc -t <设备序列号> shell aa start -a EntryAbility -b org.terminator.ohos.formvalidator -m entry

四、完整代码对照

4.1 整体架构

com.initbase.formvalidator/**(上游引擎,拆分后原样编译)
    │ Type × 8 / Flow × 3 / ValidationField / validate()
composeMain/**(上游 Compose 原件,逐字保留不编译)
    │ Form / ValidationSnackBar / LocalFormValidator / errorSafe
Bridge.kt + MiniJson
    │ FormValidatorValidate / FormValidatorVersion
libformvalidator_napi.so
    │ formValidatorValidate / formValidatorVersion
Index.ets
    │ JSON parse + Custom 合并 + @State 更新
    ▼
ArkUI 真机页面(四 Tab 滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_formvalidator、app/docs 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单插件钉版
build.gradle.kts(根)版本 1.0.7-ohos.1(走 gradle.properties)、全局禁用 Ohos*Test* 任务
FormValidator/build.gradle.kts纯 KMP:jvm() + ohosArm64 + ohosX64,零依赖引擎
FormValidator/src/commonMain/.../FormValidator.kt验证引擎(重写去 Compose 状态,语义逐行保持)
FormValidator/src/composeMain/上游两文件逐字保留(diff 验证仅行尾差异)
FormValidator/src/commonTest/.../FormValidatorTest.kt20 项 JVM 交叉验证(含怪癖锁死用例)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、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鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
引擎FormValidator(fields, flow).validate()一次表单校验,返回整体结果并驱动 onError
引擎Type.Required / Email / Optional对象规则:非空白 / 邮箱正则 / 恒真
引擎Type.MustBeMoreThan / LessThan / EqualTo / InRange模板规则:String 比长度、Number 比数值
引擎Type.Custom { ... }自定义谓词(不跨 C ABI,ArkTS 合并)
引擎Flow.Down / Up / Splash短路报第一 / 报最后 / 全量报
引擎ValidationField.onError先重置(null)后回填的错误回调协议
NativeFormValidatorValidate / FormValidatorVersionC ABI JSON 导出
N-APIformValidatorValidate(req) / formValidatorVersion()向 ArkTS 暴露能力
ArkTScallBridge(fields) / mergeForm(res, age)触发校验并按 Flow 合并本地 Custom 规则

4.4 ArkTS 与 Kotlin 的边界

const raw: string = formValidatorValidate(JSON.stringify({
  flow: 'Down',
  fields: [
    { name: 'Name', type: 'Required', value: this.name },
    { name: 'Age', type: 'InRange', min: 0, max: 120, value: 30 },
    { name: 'Email', type: 'Email', value: this.email }
  ]
}));
const res = JSON.parse(raw);
// res.valid === false;res.errorMessage === 'Email is not a valid email'
// res.fields[i].checked / .valid / .error 逐字段渲染
val fields = nodes.mapIndexed { index, node ->
    FormValidator.ValidationField(
        value = classifyValue(node.raw("value")),
        name = node.str("name"),
        type = buildType(node),
        onError = { lastErrors[index] = it }
    )
}
val validator = FormValidator(fields = fields, flow = flow)
val valid = validator.validate()          // 错误消息在 Kotlin 侧生成,逐字穿过 JSON

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

五、关键决策说明

决策 1:提取零依赖引擎,而不是等 Compose 制品线对齐

社区 CMP 线(1.9.2 / Kotlin 2.2.21)落后上游(Compose 1.10 / Kotlin 2.3)一个版本代差,compose.runtime 状态 API 与编译器插件都不同编。表单校验的价值在规则语义而不是 UI 挂载——把 385 行里的 6 行 Compose 状态剥离,引擎即可零依赖编译,ohos 产物不背任何 Compose 版本包袱。这与 KStore 的"真编译整库"不冲突:路线由制品线可达性决定——kotlinx 四件套有 ohos 制品就整库编,Compose 没有就提内核。

决策 2:为引擎补 20 项交叉验证,把"零漂移"变成可检验命题

上游没有实质测试。拆分重写是有漂移风险的操作,所以同一份 commonMain 在 JVM 上跑 20 项用例:规则矩阵 ×15、模板长度怪癖 ×1、三种 Flow ×3、回调协议 ×1。so 里执行的就是这份被测试锁死的代码。过程中还真踩到两处自己写测试时的误判(errorMessage 只在 valid() 内赋值;模板按 toString() 长度比较),反过来证明这类锁死的必要性。

决策 3:根构建替换(cmp_qrose 模式),app/docs 留盘

上游根构建(AGP / Compose / dokka / nmcp / GPG 签名)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。app/ 是上游 Android demo、docs/ + mkdocs-docs/ 是文档站,鸿蒙构建不消费,留盘不进 settings.gradle.kts——它们的存在方式只有这一种。

决策 4:类型跟随 JSON 字面量 + Custom 明确拒绝

桥不引入类型标注字段:带引号 → String、null → null、数字 → Number,天然对上上游 when (value) 分发。Custom 不做半吊子支持,直接 ok:false 拒绝并给出替代路径文案——lambda 序列化不了就是不行,Demo 的 Age 字段完整示范了"ArkTS 谓词 + 按 Flow 合并"的正确姿势。

决策 5:上游怪癖逐字保留,不"顺手修复"

上游有一批文档没写的行为(见 8.1):String 值上模板按 toString().length 比较(MustBeEqualTo(4) 等价"长度==1")、数值消息插值带 .0、Splash 表单级错误恒空、Number + Required 恒真。修掉任何一条都会让鸿蒙与 Android/iOS 行为分叉,这些怪癖被交叉验证用例显式锁死,修复留给上游 PR。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 11(x64)、JDK 21(Eclipse Adoptium jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

.\gradlew.bat :FormValidator:jvmTest

20 项用例全部通过(本次为提取的引擎新增,上游无实质测试):

用例组数量钉住什么
规则矩阵158 种 Type × String / Number / 其他值类型的分发表与错误消息
模板长度怪癖1MustBeEqualTo(4) → 长度 1、MustBeEqualTo(1111) → 长度 4(上游行为锁死)
Flow 状态机3Down 短路(后续字段不再校验)、Up 报最后、Splash 全量且表单级错误为空
回调协议1onError 先重置(null)后回填;onValidate 通知;valid 属性同步

拆分有没有漂移,这份矩阵就是答案:引擎里每一行 defaultErrorMessage 赋值都有对应断言,包括 "Age must be greater than 6.0" 这种连 toFloatOrNull() 插值细节都在内的消息。

6.3 HAP 验证

:FormValidator:compileKotlinJvm: BUILD SUCCESSFUL
:FormValidator:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: FormValidatorValidate / FormValidatorVersion / libohosformvalidator_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libformvalidator_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

KN so(debugShared 未 strip)arm64 约 3.7 MB、x86_64 约 3.4 MB,各配 libc++_shared.so 约 1.85 MB / 1.7 MB,经 copyKnSo* 回填 entry/libs/ 对应 ABI 目录,HAP 打包时随 NAPI so 一并装入。真机安装、启动与页面验收由维护者在 DevEco Studio 中完成签名后执行(见下节效果图)。

6.4 功能用例

  1. 启动后关于区出现 libohosformvalidator.so @ 1.0.7-ohos.1(来自真实 Native 调用,非静态文字);
  2. 常规表单空表单提交:Name is required,Flow=Down 时 Email 不显示错误(短路,与上游一致);
  3. Flow 切到 Up:报告字段序上最后一个非法字段的消息;
  4. Flow 切到 Splash:Name 与 Email 同时报错、表单级消息为空(上游语义);
  5. Age 输入奇数:Age must be divisible by two(ArkTS Custom 规则按 Flow 合并的结果);
  6. SnackBar Tab:校验失败弹 3000ms toast + 红色 Form is not valid 卡片,逐字段错误不展示(对齐上游 SnackBar 表单的设计说明);
  7. ErrorSafe Tab:每次输入即时触发桥调用,Form has unsaved changes 脏状态提示随输入出现;
  8. 全规则 Tab:八种规则齐测,must be longer than 6 characters、must be in range 0 - 120、must match to ... 等消息逐字来自 Kotlin 引擎;
  9. 全部字段合法:绿色 Form is valid 卡片。

6.5 验证结论

规则引擎、Flow 短路语义、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的校验会话:版本握手、四种表单模式、全规则矩阵与 Custom 合并(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/FormValidator.git
cd FormValidator
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :FormValidator:compileKotlinOhosArm64 :FormValidator:compileKotlinOhosX64   # klib
.\gradlew.bat :FormValidator:jvmTest                                                     # 20 项交叉验证
.\gradlew.bat :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.formvalidator -m entry
hdc shell hilog | grep FormValidator

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :FormValidator 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Gradle 8.11.1(上游 wrapper)拉不起 HarmonyOS Kotlin 2.2.21-1.0.0 插件线,必须降 8.14.1——工具链经验按线区分,不能拿 KStore 的"沿用上游 Gradle 9.6.1"硬套;
  4. 上游 Kotlin 2.3.0 元数据不可读的前提是"你还依赖 2.3 编译的制品"——引擎拆成零依赖后这个问题整体消失,版本决策的依据是依赖形态而不是上游用什么;
  5. ArkTS 不能 import libohosformvalidator.so(HarmonyOS CAdapter 不导出 @CName 业务符号),必须走 libformvalidator_napi.so,否则报 does not provide an export named 'formValidatorValidate';
  6. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++_shared.so)缺一不可;
  7. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  8. 上游语义两处高频误判(写交叉验证时亲历):ValidationField.errorMessage 只在 valid() 内赋值,构造后直接断言必为 null;String 值上 MustBeXxx 比较的是模板 toString() 的长度——MustBeEqualTo(4) 是"长度1"而不是"长度4";
  9. 根构建的 /build/ 是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/.idea/、.clangd/、.clang-tidy 与 example/nativeApp/build/ 要单独追加 gitignore,否则混进提交(后两者是 IDE 自动生成的检查配置,已过审模板不收录);
  10. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝(报 00303038 must have required property 'material')——仓库内留空数组且 product 不引用签名,勾选自动签名后由 IDE 回填完整配置;
  11. 签名材料由 DevEco 自动写入 build-profile.json5,提交前用 git update-index --skip-worktree 保护,证书绝不进入 AtomGit;
  12. HAP 打包用 DevEco 自带 JBR,系统 JDK 8 会让 hvigorw assembleHap 半路报不相干的错。

8.2 已知问题

  • 鸿蒙侧无 Compose:Form / ValidationSnackBar / LocalFormValidator / errorSafe 不在鸿蒙产物里,ArkTS Demo 用原生 ArkUI 复刻上游三种表单模式;
  • Custom 规则不跨 C ABI:桥以 ok:false 明确拒绝,替代路径是 ArkTS 实现谓词后按 Flow 语义合并(Demo 已示范);
  • 表单级可观察状态(mutableStateOf 驱动的 valid / errorMessage)退化为"每次调用返回快照",实时校验由 ArkTS 在 onChange 里驱动;
  • composeMain 的上游 Compose 源码要等社区 compose.runtime ohos 制品与上游 Compose 版本对齐后接回;接回前 Android / iOS / Desktop 消费者继续用上游原仓;
  • 桥的 MiniJson 只支持"扁平对象 + 一层扁平对象数组",协议变更时需同步扩展解析器与文档;
  • 上游怪癖(模板长度、6.0 插值、Splash 表单级错误恒空、Number + Required 恒真)全部逐字保留,不在本仓修复;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.formvalidator 匹配的签名。

8.3 未来优化方向

  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 把 composeMain 接回时间点与社区 CMP 制品线对齐,届时 ArkUI Demo 降级为 fallback;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本;
  • 将怪癖清单整理成上游 issue / PR(模板长度语义、README 与代码不一致处)。

九、总结

9.1 核心难点回顾

上游 commonMain(引擎 + Compose 状态编织)
    → 确认 Compose 制品线缺口(社区 1.9.2 vs 上游 1.10)→ 走引擎提取路线
    → 拆源码集:引擎重写为零依赖纯 Kotlin(语义逐行保持),上游原件逐字存 composeMain
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1)
    → 根构建替换(cmp_qrose 模式)
    → ohosArm64 / ohosX64 KLIB
    → C ABI(@CName × 2)+ 类型跟随字面量 + Custom 明确拒绝
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + Custom 按流合并
    → ArkUI 真机四 Tab 验收(版本握手 / 三表单模式 / 全规则)

9.2 封装层次

  • 引擎层:上游原版 Type 分发表、Flow 状态机、onError 回调协议(拆分后零依赖);
  • 归档层:上游 Compose 原件在 composeMain 逐字保留,等制品线对齐接回;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 组装、Custom 规则合并、错误文本与 toast;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线由当天的制品线可达性决定:kotlinx 四件套有 ohos 制品,KStore 就"真编译整库";Compose 没有对齐制品,FormValidator 就"提取零依赖引擎"。同一个社区里两条路线都对——判断依据是依赖形态,不是历史结论;
  2. 工具链经验按线区分:2.2.21 旧线必须配 Gradle 8.14.1,2.4.20 新线可以留 Gradle 9.x;而"引擎零依赖化"能让上游 Kotlin 2.3 的元数据问题整体消失——版本决策要看依赖闭包,不是看上游用什么;
  3. 交叉验证是"内核没漂"的唯一可信证明:上游有 51 项测试就零改动照跑(KStore),上游没有就自己补 20 项(FormValidator)——两种情况下规则相同:so 里执行的必须是同一份被测试锁死的代码,连怪癖都要锁死,"顺手修复"就是制造分叉。

9.4 适配成果

当前适配已完成:

  • FormValidator 1.0.7 验证引擎的双 ABI Kotlin/Native 编译(上游语义逐行保持,上游 Compose 原件在 composeMain 逐字归档);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 四 Tab 验收页面(常规表单 / SnackBar / ErrorSafe / 全规则、版本握手、关于区);
  • 20 项 JVM 交叉验证全绿(含上游怪癖锁死用例)+ 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

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐