本文记录 chip-textfield 接入 OpenHarmony 的完整过程,覆盖 Compose Multiplatform 输入组件盘点、纯 Kotlin 引擎提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 六 Tab 真机页面、HAP 构建和真机验收。

chip-textfield 是基于 Compose Foundation(BasicTextField + 自绘 chip)的「Material-free」chip 输入框库:输入文本、遇分隔符或按 Done 成 chip,带校验拒绝、maxChips 上限、建议下拉、退格删尾和泛型 chip 模型。本次适配与社区 KStore 一文的"真编译整库"路线相反,与 qrose / scaffolds 的"零依赖内核提取"同路:上游 commonMain 的全部文件都绑死 androidx.compose.runtime(快照状态、@Composable、remember),而 ohos 制品线上没有 Compose 制品,因此把 Composable 内部的提交门与状态机提取为纯 Kotlin 的 ChipTextFieldEngine(commonMain,零第三方依赖),上游五个 Compose 文件原样隔离进 src/composeMain 不参与鸿蒙编译;ArkUI Demo 通过 @CName("ChipFieldStep") 单入口平面 JSON 桥逐动作驱动引擎,在 ArkUI 上重建上游全部六个示例,行为语义由 24 项 JVM 交叉验证钉底。

项目地址: AtomGit/oh-tpc/compose-chip-textfield

上游仓库: aldefy/compose-chip-textfield

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

chip-textfield 为 Compose Multiplatform 应用提供"文本 → chip 列表"这一件事的完整答案:ChipTextField(state, onCreateChip, ...) 一个 Composable 覆盖分隔符成 chip、校验拒绝、maxChips 门控、建议下拉、退格删尾、自定义 chip 渲染(chipContent / chipLeadingIcon / chipTrailingIcon 槽位)。Gmail 收件人输入、标签录入、优先级标记、状态打标,都是它的直接场景。上游把 KMP 目标铺到 Android / JVM / wasmJs / iOS,OpenHarmony 应用同样需要这套输入范式,但上游构建(Kotlin 2.1.20 / CMP 1.8.0 / AGP 8.9.0)不会生成 OpenHarmony 变体。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.1.20 / CMP 1.8.0 与鸿蒙 Kotlin 2.2.21-1.0.0 不同线;社区 CMP 1.9.2 的 compose.ui ohos 制品线尚未就位,BasicTextField / TextFieldValue / onPreviewKeyEvent 这些依赖进不来。
UI 绑死 runtime上游 commonMain 五个文件全部 import androidx.compose.*:ChipTextFieldState.chips 是快照状态承载的可观察列表,ChipTextField 是 @Composable,没有"剥掉注解就能编"的捷径。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
chip 是有渲染状态的列表与 qrose"编码器出图"不同,chip 列表要在 ArkTS 侧渲染,桥必须把列表状态编码过桥且不丢语义。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:纯 Kotlin 引擎的 ohos 编译、根构建替换(cmp_qrose 模式)和无状态 C ABI/N-API 桥接层。提交门、分隔符切分、删尾语义完全由引擎原语义负责,ArkTS 只负责转发击键、渲染 chip 和展示状态。

1.2 库提供的能力

Demo 提供六个示例场景,对应上游 sample 的全部六个入口:

  • 基本录入(basic):逗号 / 换行成 chip,Done 提交,空白拒绝;
  • 邮箱录入(email):正则 ^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ 校验,不合法的段被拒绝并留在输入框,分隔符额外支持空格;
  • Gmail 收件人(gmail):内置 8 人通讯录,按姓名 / 邮箱大小写不敏感匹配,未命中走正则并从 local part 自动生成显示名(john.doe@x.com → John Doe X);建议下拉实时过滤并排除已选;
  • 标签(tags):自动补 # 前缀,# 单独不算,maxChips = 5,满后输入框整体消失;
  • 优先级(priority):! 前缀 → 高、* 前缀 → 中、裸文本 → 低,chip 按级别着色;
  • 状态打标(status):三档状态选择器 + 六个 emoji 图标选择器,成 chip 时携带当前选择,chip 内绘制状态色圆点。

响应统一走共享状态行:握手行展示 chip-textfield 1.0.0-alpha01-ohos.1 · kotlin 2.2.21-1.0.0(来自真实 Native info 调用而非静态文字),每次操作回显动作与结果(如 commit · accepted=false reason=rejected by validator)。

1.3 实现适配

维度要求
语义复用提交门(trim → 空白拒绝 → maxChips 拒绝 → 校验器拒绝)、分隔符切分、首颗匹配删除、越界抛错,全部按上游 ChipTextField / ChipTextFieldState 的实现逐条移植进引擎,一个行为都不重写。
平台目标库模块 jvm() + ohosArm64() + ohosX64(),jvm 作交叉验证;example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 Compose UI 隔离进 src/composeMain(非标准源码集名,天然不进编译);sample / website 留盘不进 settings.gradle.kts。
桥接稳定单个 C ABI + UTF-8 平面 JSON,chip 列表用控制字符编码过桥;异常永远不跨 FFI。
状态无侧桥完全无状态:ArkTS 持有 chip 列表(编码串数组),每次请求带回,引擎按请求重建——和 scaffolds"ArkTS 持有 offset"同一纪律。
UI 完整六 Tab 覆盖上游六个示例的全部交互:录入、校验拒绝、建议选择、删尾、清空、选择器。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游 Compose UI 没有在鸿蒙上编译,ArkUI Demo 是按上游视觉参数重建的界面,不是 Compose 渲染;引擎语义与上游逐条对齐,这一点由 24 项 JVM 用例钉死。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认"引擎提取"路线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、引擎提取
第 3 阶段:桥协议建模     ── 九 action 平面 JSON 契约、chips 控制字符编码、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、DisposeString 纪律
第 5 阶段:ArkUI 宿主     ── 六 Tab Demo、共享状态行、选择器与建议列表
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
chip-textfield/src/commonMain/        新引擎 ChipTextFieldEngine(纯 Kotlin,进 ohos 构建)
chip-textfield/src/composeMain/       上游五个 Compose 文件原样隔离(不参与任何编译)
chip-textfield/src/jvmTest/           24 项交叉验证(13 项上游语义移植 + 11 项行为钉子)
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson + 示例校验器)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkUI 六 Tab 页面
sample/ website/                      上游示例与文档站,留盘不进 settings.gradle.kts
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :chip-textfield 与 :example:nativeApp,rootProject.name = "ohos_chip_textfield"。上游的 AGP、compose 插件、binary-compatibility-validator、maven-publish / signing、Maven Central 发布工作流全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 已写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 钉版;2.2.21 线不支持 9.x)
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.chiptextfield
1.3 源码依赖度盘点
上游文件依赖形态处置
ChipTextFieldState.ktmutableStateOf / remember / @Stable移入 composeMain;列表语义移植进引擎
ChipTextField.kt@Composable + FlowRow / BasicTextField / 快捷键移入 composeMain;processInput / commitText / removeLastChip / addChipAndClear 四个内部函数的算法移植进引擎
ChipContent.kt@Composable + Canvas移入 composeMain;chip 视觉由 ArkUI 按参数重建
ChipTextFieldColors.kt / ChipTextFieldDefaults.kt@Immutable / @Composable / isSystemInDarkTheme移入 composeMain;默认色值/圆角/分隔符常量进 ArkUI 与引擎常量

composeMain 能"整文件搬零修改"的关键:它不是 Kotlin 源码集,只是一个非标准目录名——KMP 只编译自己认识的默认源码集名,src/composeMain 从不进任何编译任务。这比逐个剥离 @Composable / @Immutable 注解干净得多:那些注解不是装饰,@Composable 改写函数签名(首参插入 Composer),文件离开 Compose 编译器根本无法表达,剥离等于重写。原样保留还有第二个价值:未来社区 CMP compose.ui ohos 制品就位后,把 composeMain 改回 commonMain 就能让真 Compose UI 回归,一行上游代码都没丢。

1.4 页面边界
标题区          ChipTextField / Compose Multiplatform · HarmonyOS(Kotlin/Native 桥)
握手行          chip-textfield 1.0.0-alpha01-ohos.1 · kotlin 2.2.21-1.0.0(info 握手)
状态行          最近一次桥操作结果(六 Tab 共享)
Tab 切换        基本 / 邮箱 / Gmail / 标签 / 优先级 / 状态
录入区          label(可选) + 前置图标(可选) + TextInput + chip 流式布局
操作区          退格删尾 / 清空 两按钮(演示空文本退格与清空语义)
附加区          Gmail 建议下拉 / 优先级配色 / 状态·图标选择器(按 Tab 分派)

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

2.1 路线选型(本适配的成败手)

KStore 一文的路线是"真编译整库"——前提是依赖的 kotlinx 制品线在 ohos 上齐了。chip-textfield 的依赖只有一件:androidx.compose.runtime / foundation / ui。社区 Maven(https://maven.eazytec-cloud.com)上这三件没有 ohosArm64 变体,上游 CMP 1.8.0 与社区 CMP 1.9.2 也不同代。结论与 qrose / scaffolds 一致:UI 进不来,语义必须出得来。把 Composable 内部的纯算法提出来,UI 交给 ArkUI 重建——这不是妥协,是这个库在当前工具链下唯一可验收的路径。

2.2 工具链钉版

settings.gradle.kts 把 eazytec 双仓写在 pluginManagement 最前(放后面配置期直接 Unresolved reference: ohosArm64):

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_chip_textfield"

include(":chip-textfield")
include(":example:nativeApp")

gradle.properties 钉 JDK 21 与 KN 下载源,gradle-wrapper.properties 钉 Gradle 8.14.1,libs.versions.toml 只剩一个插件别名:

org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=1.0.0-alpha01-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
2.3 提取纯 Kotlin 引擎

chip-textfield/build.gradle.kts 收敛为三目标零依赖:

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm()          // 交叉验证
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}     // 零第三方依赖,ohos 线没有 Compose 制品
        commonTest.dependencies { implementation(kotlin("test")) }
    }
}

引擎(io.github.aldefy.chiptextfield.engine.ChipTextFieldEngine)与上游内部函数逐条对应:

上游(Compose 内部)引擎方法语义
processInput(newValue)onInput(newText)文本含分隔符则切分,末段之前的每段过同一道提交门,末段留在输入框并回写 query
commitText(text)(Done 键)commit(text)trim → 空白拒绝 → maxChips 拒绝 → onCreateChip 返回 null 拒绝;无论成败输入框都清空(上游 onDone 无条件清空的真实语义)
removeLastChip()(空文本退格)onBackspace(currentText)仅当输入框为空时删末位 chip,返回被删项
addChipAndClear(chip)(建议直选)addChip(chip)绕过校验器、仍受 maxChips 约束,成功即清空输入框
ChipTextFieldState.chipschips / removeChip / removeAt / clearChipsremoveChip 只删首颗匹配(chips - chip);removeAt 越界抛 IndexOutOfBoundsException

引擎无快照状态、无回调副作用,可按请求重建——这正是无状态桥需要的形状:

class ChipTextFieldEngine<T>(
    private val onCreateChip: (String) -> T?,
    initialChips: List<T> = emptyList(),
    val delimiters: Set<Char> = DEFAULT_DELIMITERS,   // 上游默认 {',', '\n'}
    val maxChips: Int = DEFAULT_MAX_CHIPS,            // 上游默认 Int.MAX_VALUE
)
2.4 测试的处置:24 项 JVM 交叉验证

上游 ChipTextFieldStateTest 的 13 个用例逐条移植(初态、增删、首颗匹配删除、data class 相等性、越界抛错、重复 chip);另加 11 个行为钉子钉住 Composable 内部算法(无分隔符只回写 query、切分后末段保留、空白段跳过、自定义分隔符、Done 无条件清空、maxChips 双门、建议直选绕过校验器等)。根构建全局禁用 ohos 测试任务,测试统一 JVM 跑:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}
./gradlew :chip-textfield:jvmTest    # 24/24 通过

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.aldefy.chiptextfield.engine(纯 Kotlin 引擎,随编译进 .so)
        ↓ ChipTextFieldEngine<T>(提交门 / 切分 / 删尾)
Bridge.kt(@CName × 1,扁平 JSON 进出)+ ChipSamples.kt(六类示例校验器)
        ↓ UTF-8 JSON
N-API chipFieldStep()
        ↓
ArkUI Index.ets(六 Tab,chips 编码串数组为渲染状态)

单入口 chipFieldStep(request): string,九个 action 覆盖引擎全部接口:

action请求字段响应字段对应引擎接口
info—ok/fork/kotlin/kinds/actions握手
inputkind/chips/textok/committed[]/text/chips[]/canAddonInput
commitkind/chips/textok/accepted/reason/chips[]/canAddcommit
backspacekind/chips/textok/removed/chips[]/canAddonBackspace
removeAtkind/chips/indexok/chips[]/canAddremoveAt
clearkind/chipsok/chips[]/canAddclearChips
selectkind/chips/name/emailok/accepted/chips[]/canAddaddChip
suggestkind/query/chipsok/suggestions[{name,email,initials,color}]建议过滤
describekind/chipsok/chips[]/views[]渲染视图模型

kind 决定校验器与分隔符(basic / email / tags / gmail / priority / status),六类示例校验器按上游 sample 源码 1:1 移植在 ChipSamples.kt——业务逻辑全部在 KN 侧,ArkTS 不做第二份实现。

3.2 chips 编码:控制字符方案

chip 列表要在"无数组、无嵌套"的平面 JSON 契约里过桥。方案:ArkTS 侧持有编码串数组(每颗 chip 编成一个字符串),请求时用记录分隔符 RS(\u001E)join 成一个字段;chip 内部多字段用单元分隔符 US(\u001F)分隔:

basic/email/tags : 标签本身
priority         : level␟label          (如 3␟urgent)
gmail            : name␟email
status           : label␟statusName␟icon(如 Server A␟Active␟🚀)

选控制字符的理由:键盘打不出来,用户输入永远不会撞编码;JSON.stringify 会把控制字符转成 \u001e 转义、Kotlin 侧 MiniJson 解码还原,两端所见一致。桥因此保持与 qrose / scaffolds 同款的扁平 MiniJson(手写、逗号切分 + 引号状态机 + \uXXXX 解码),不引入 JSON 库。

3.3 错误通道

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

@CName("ChipFieldStep")
fun chipFieldStep(request: String): String = try {
    when (json.str("action")) { /* 九路分发 */ }
} catch (t: Throwable) {
    err(t.message ?: t::class.simpleName ?: "error")   // {"ok":false,"error":"..."}
}

上游语义里"抛异常"的路径在这里转成显式失败:removeAt 越界返回 ok:false(ArkTS 状态原样不动),校验器拒绝返回 accepted:false + reason(reason 直接进状态行,如「拒绝: rejected by validator」)。

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

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

HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务函数挂到 ArkTS 模块上——libohoschiptextfield.so 里能 llvm-nm 到 ChipFieldStep,但 ArkTS 直接 import 只能看到 stdlib 类型。必须用 CMake 再包一层 libchiptextfield_napi.so,这层才是 ArkTS 的 import 面。

4.2 Kotlin/Native 导出函数
@CName("ChipFieldStep")
fun chipFieldStep(request: String): String   // 九 action 一口进
C ABI 符号ArkTS 方法覆盖内核
ChipFieldStepchipFieldStep(req)引擎九接口 + 六类示例校验器 + 建议过滤 + 视图模型
4.3 C++ N-API 分发

每个回调统一四步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数、napi_create_string_utf8 后立刻 DisposeString 归还 Kotlin 运行时(绝不 free()):

const char *raw = ChipFieldStep(request.c_str());
napi_value out = nullptr;
napi_status st = napi_create_string_utf8(env, raw, NAPI_AUTO_LENGTH, &out);
DisposeKnString(raw);   // libohoschiptextfield_symbols()->DisposeString

libohoschiptextfield_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构、_symbols() 与业务符号),不拖整份生成头。

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

add_library(chiptextfield_napi SHARED napi_init.cpp)
target_link_libraries(chiptextfield_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoschiptextfield)

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

export const chipFieldStep: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 握手与状态行
aboutToAppear(): void {
  const res = JSON.parse(chipFieldStep(JSON.stringify({ action: 'info' }))) as InfoResult;
  if (res.ok) {
    this.handshake = `chip-textfield ${res.fork} · kotlin ${res.kotlin}`;
  }
}

握手行数据来自真实 Native 调用。状态行是六 Tab 共享的"最近一次桥操作结果"(与 qrose / scaffolds 的单状态行同款语义):每次动作回显结果摘要(input ok · committed=1 chips=3、commit · accepted=false reason=...),Native 调用本身抛错同样被 try/catch 收进状态行。

5.2 六 Tab 交互

每个 Tab 一个 ChipFieldScreen 组件实例(独立 chips/text/views 状态),按 renderer 分派 chip 视觉:

Tab交互要点
基本逗号/换行切分,Done 提交;chip 按上游默认样式(8 圆角、#E8DEF8 底、#79747E 描边、✕ 删除)
邮箱前置 ✉ 图标;非法邮箱 accepted=false 且文本留在输入框(切分路径)或被丢弃并清空(Done 路径)——两条真实上游语义
GmailTo / Cc 双字段;输入实时 suggest 过滤(排除已选),点建议走 select;chip 为圆形头像 + 姓名,整体点按即删(上游 GmailChip 语义)
标签初始 #kotlin #compose;满 5 颗后 canAdd=false,输入框整体隐藏(上游行为)
优先级!/* 前缀分级,chip 按 level 三档配色(M3 light 容器色)
状态三档状态选择器 + 六 emoji 图标选择器;commit 请求携带当前选择;chip 内绘制 emoji + 状态色圆点

chips 变化统一走 applyChips():更新编码串数组 → describe 取视图模型 → 刷新渲染。 ArkUI 没有可靠的"空文本退格"软键盘事件,因此提供「退格删尾」按钮显式演示该语义(text 随请求上送,引擎只在空文本时删尾)。

5.3 对比度和错误边界

chip 默认样式、优先级三档配色、状态三色(Active 绿 / Pending 琥珀 / Inactive 红)全部取自上游 sample 的 Material 3 light 色值;拒绝原因、ok:false 错误原文直接进状态行,桥调用整体 try/catch(对应 so 未加载时显示「异常: …」而不是闪退)。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoschiptextfield)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       ├── Bridge.kt                         @CName × 1 + MiniJson(扁平 JSON + \uXXXX 解码)
│       └── ChipSamples.kt                    六类示例校验器 + 通讯录 + chips 编解码
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.chiptextfield、versionName 1.0.0-alpha01-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/libohoschiptextfield_api.h
        ├── src/main/cpp/types/libchiptextfield_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 六 Tab 验收页面
6.2 构建、签名和安装
./gradlew :chip-textfield:jvmTest                                                # 24 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

命令行跑 hvigorw 要用 DevEco 自带 JBR(系统 JDK 8 会让 PackageHap 中途报不相干错误,Windows 下把 DevEco Studio/jbr/bin 加进 PATH 即可)。签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 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.chiptextfield -m entry

四、完整代码对照

4.1 整体架构

io.github.aldefy.chiptextfield.engine(纯 Kotlin 引擎,随编译进 .so)
    │ ChipTextFieldEngine<T>:提交门 / 分隔符切分 / 删尾 / 建议直选
example.nativeapp.Bridge + ChipSamples + ChipCodec
    │ ChipFieldStep / MiniJson / US·RS 编码 / 六类校验器
libchiptextfield_napi.so
    │ chipFieldStep
Index.ets
    │ JSON parse + @State 更新 + describe 视图模型
    ▼
ArkUI 六 Tab 页面(基本 / 邮箱 / Gmail / 标签 / 优先级 / 状态)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_chip_textfield、sample/website 移出
gradle/libs.versions.toml只剩 kotlin-multiplatform 插件别名(2.2.21-1.0.0)
build.gradle.kts(根)版本 1.0.0-alpha01-ohos.1、全局禁用 Ohos*Test* 任务
chip-textfield/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,零第三方依赖
chip-textfield/src/composeMain/**上游五个 Compose 文件原样隔离(非标准源码集,不参与编译)
chip-textfield/src/commonMain/.../ChipTextFieldEngine.kt唯一新增库源文件:提交门/切分/删尾/直选
chip-textfield/src/jvmTest/.../ChipTextFieldEngineTest.kt24 项交叉验证
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 C ABI、九 action 分发、MiniJson、错误通道
nativeApp/.../ChipSamples.kt邮箱正则 / 标签前缀 / 优先级 / 通讯录 / 状态 + chips 编解码
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鸿蒙适配主文档(17 节)

4.3 关键 API 对照

层次API作用
引擎onInput(newText)分隔符切分 + 末段前提交,返回 committed 与留在输入框的 remaining
引擎commit(text)Done 键提交门,返回 accepted/reason/chips
引擎onBackspace(currentText)空文本删末位 chip
引擎addChip(chip)建议直选,绕过校验器、受 maxChips 约束
引擎removeChip / removeAt / clearChips首颗匹配删除 / 越界抛错 / 清空
桥chipFieldStep(req)九 action 平面 JSON 导出
N-APIchipFieldStep(request)向 ArkTS 暴露能力
ArkTSapplyChips(next)chips 更新 → describe 视图模型 → 渲染
ArkTSonInputChanged / handleCommit / handleBackspace击键转发、Done 提交、退格删尾

4.4 ArkTS 与 Kotlin 的边界

// 输入 "a@b.com," → 桥切分并提交首段,末段回写输入框
const res = JSON.parse(chipFieldStep(JSON.stringify({
  action: 'input', kind: 'email',
  chips: this.chips.join('\u001E'),   // 编码串数组 join 成一个字段
  text: value
})));
// res.committed=['a@b.com'] res.text='' res.chips=[...新列表] res.canAdd=true
"input" -> {
    val engine = engineFor(kind, json)          // 按 kind 装配校验器/分隔符/maxChips,chips 从编码串解码
    val result = engine.onInput(json.str("text") ?: "")
    "{\"ok\":true,\"committed\":${codecArray(kind, result.committed)},...}"
}

两者之间只传输扁平 JSON 字符串与控制字符编码的 chip 列表,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:引擎提取而不是硬编 Compose(与 KStore 路线相反的原因)

KStore 走"真编译整库",因为它的 kotlinx 依赖在 ohos 制品线上齐了。chip-textfield 的唯一依赖是 Compose,ohos 线没有制品;且 @Composable 改写函数签名、快照状态绑 runtime,"剥注解硬编"不成立。把提交门/切分/删尾提取为纯 Kotlin、UI 交给 ArkUI 重建,是当前工具链下唯一可验收路径——与 qrose(编码器留在 commonMain、Painter 进 composeMain)、scaffolds(折叠数学进 CollapseEngine)完全同构。

决策 2:composeMain 非标准源码集隔离

上游五个文件原样移进 src/composeMain,不改一个字节。非标准目录名天然不进编译,比剥注解干净(@Composable 剥不掉),也为未来 CMP ohos 制品就位后"改回 commonMain 即复活"留了完整的上游快照。

决策 3:无状态桥 + 控制字符编码

scaffolds 的引擎无状态(ArkTS 持 offset),qrose 的编码器无状态(入参即数据);chip 列表却是有渲染状态的。两条路:会话式桥(KN 持状态、sessionId 寻址)或无状态桥(ArkTS 持 chips 编码串、每次带回)。选后者——引擎按请求重建的成本是微秒级,换来的是桥无副作用、可并发、行为完全确定;chips 用 US/RS 控制字符编码进平面 JSON,MiniJson 保持与社区审核模式同款的"无数组无嵌套"契约。

决策 4:上游"怪"语义原样保留

两条语义看起来像 bug,但是上游真实行为,原样保留并由测试钉住:其一,Done 提交无论成败都清空输入框(onDone 无条件 textFieldValue = TextFieldValue("")),所以 Done 路径下被拒绝的文本会消失、分隔符路径下被拒绝的段留在输入框——两种路径行为不同;其二,chips 满后输入框整体消失(canAddChips == false 时不 compose BasicTextField)。鸿蒙 Demo 按原语义复刻,不在 ArkTS 层"修正"。

决策 5:24 项 JVM 交叉验证钉底

引擎语义在 commonMain,JVM 与 ohos 跑同一份代码;13 项上游测试移植 + 11 项行为钉子,是"引擎没漂"的闸门。上游没有的引擎(本次新增),测试就是语义规格本身。

六、测试与验证

6.1 测试环境

本次验证使用 Windows x64、JDK 21(Temurin,org.gradle.java.home 钉版)、DevEco Studio 26.0、OpenHarmony 命令行工具(hvigor / hdc / llvm-nm)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :chip-textfield:jvmTest

24 项用例全部通过:

分组用例数钉住什么
上游状态语义移植13初态/增删、首颗匹配删除、data class 相等性、越界 IndexOutOfBoundsException、清空、重复 chip
输入/提交行为钉子11无分隔符只回写 query、切分后末段保留、空白段跳过、自定义分隔符、trim 后接受、空白拒绝、校验器拒绝、Done 无条件清空、maxChips 双门、空文本退格、建议直选绕过校验器

6.3 HAP 验证

:chip-textfield:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
  → arm64-v8a/libohoschiptextfield.so(3,725,352 B)+ libc++_shared.so
  → x86_64/libohoschiptextfield.so(3,403,024 B)+ libc++_shared.so
llvm-nm -D: ChipFieldStep / libohoschiptextfield_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libchiptextfield_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

entry-default-unsigned.hap 约 6.8 MB,内含双 ABI 各三件套(libchiptextfield_napi.so / libohoschiptextfield.so / libc++_shared.so);copyKnSo* 在 link 后自动把 KN so 与匹配 ABI 的 libc++ 回填进 entry/libs/<abi>/。真机(x86_64)接受 unsigned HAP 直接安装;真机走 DevEco 自动签名。

6.4 功能用例

  1. 启动后握手行出现 chip-textfield 1.0.0-alpha01-ohos.1 · kotlin 2.2.21-1.0.0(来自真实 Native info 调用,非静态文字);
  2. 基本 Tab:输入 kotlin,harmony → input ok · committed=1,逗号即成 chip,末段留在输入框;Done 提交并清空;
  3. 邮箱 Tab:输入非法邮箱按 Done,commit · accepted=false reason=...;输入合法邮箱 + 空格,空格分隔符即成 chip;
  4. Gmail Tab:To 行初始带 Raina Shah 头像 chip;输入字母出建议下拉(已选联系人被排除),点建议即成 chip;
  5. 标签 Tab:初始 #kotlin #compose;连加到 5 颗后输入框整体消失;
  6. 优先级 Tab:!urgent / *normal / 裸文本分别成高/中/低三级配色的 chip,✕ 点按删除对应 removeAt;
  7. 状态 Tab:切换状态/图标选择器后录入,chip 内 emoji 与状态色圆点随选择变化;初始 Server A / Deploy 两颗 chip 渲染正常;
  8. 每步操作回显共享状态行;拒绝原因与 ok:false 错误原文直接可见。

6.5 验证结论

纯 Kotlin 引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,六个 Tab 的录入、校验拒绝、建议选择、删尾、清空、选择器全部按上游语义工作(见下节效果图)。

七、运行效果

7.1 真机运行效果

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-chip-textfield.git
cd compose-chip-textfield
export JAVA_HOME="/path/to/jdk-21"
./gradlew :chip-textfield:jvmTest                                              # 24 项交叉验证
./gradlew :chip-textfield:compileKotlinOhosArm64                               # klib
./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.chiptextfield -m entry
hdc shell hilog | grep ChipTextField

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 CMP 适配,必须让引擎真正编译成 ohosArm64 / ohosX64 动态库,并且由测试证明语义没有漂;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. ArkUI 容器的 alignItems 类型随容器变:Row / Column 收 VerticalAlign / HorizontalAlign,Flex 才收 ItemAlign——六处 Row().alignItems(ItemAlign.Center) 在 DevEco 里统一报 10505001 ArkTS Compiler Error,全量替换为 VerticalAlign.Center 后 CompileArkTS 通过(这是本次真机前最后一个编译错误);
  4. 根构建的 /build 与 */build 是浅层模式,盖不住 example/nativeApp/build 这类嵌套目录,gitignore 要写 **/build,否则 klib 中间产物混进提交;
  5. 上游 Compose 文件不要试图"剥注解后编进 ohos"——@Composable 改写函数签名,文件离开 Compose 编译器无法表达;整目录隔离进非标准源码集 composeMain 才是零修改方案;
  6. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  7. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  8. 命令行 hvigorw assembleHap 必须用 DevEco 自带 JBR,系统 JDK 8 会在 PackageHap 中途报 spawn java ENOENT 一类不相干错误(Windows 下把 DevEco Studio/jbr/bin 加入 PATH);
  9. 命令行 hdc 的本地路径参数总是相对当前目录解析(file recv / install 都会拼接 cwd),在目标文件所在目录内用纯文件名调用最稳;
  10. 真机(x86_64)接受 unsigned HAP,真机必须签名;build-profile.json5 的 signingConfigs 留空数组,勾选 IDE 自动签名后由 DevEco 回填,提交前保护该文件,证书绝不进入 AtomGit;
  11. 顶部状态行是六 Tab 共享的"最近一次桥操作结果",切 Tab 后看到的是上一次动作的回显——这是与 qrose / scaffolds 同款的单状态行语义,不是状态错乱;
  12. 其他第三方代码托管镜像站的链接一律不进文档,仓库地址只保留 AtomGit;

8.2 已知问题

  • HarmonyOS 上没有 Compose UI:上游 ChipTextField 仍在 composeMain,鸿蒙侧是 ArkUI 按上游视觉参数重建(默认样式、M3 light 色值、圆角、间距逐项对齐),不是 Compose 渲染;
  • 引擎无快照可观察性:chips 是普通列表,可观察性由 ArkTS @State 承担——这正是无状态桥的边界;
  • enabled / readOnly 留在 UI 层:上游是 Composable 参数,引擎恒按可编辑处理,Demo 未暴露只读开关;
  • 上游 ChipContent.kt 71/84 行存疑:scope.leadingIcon() / scope.trailingIcon() 按标准 Kotlin 无法解析(ChipScope 无此成员),composeMain 不编译所以不影响本 fork,但未来接回 Compose 时要先修;
  • placeholderColor 上游声明但从未消费:引擎与 Demo 均未引入;
  • android / iOS / wasmJs 目标剥离(源文件留在树里),AGP、发布、binary-compatibility-validator 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.chiptextfield 匹配的签名。

8.3 未来优化方向

  • 状态行按 Tab 隔离(或加 Tab 前缀),消除跨 Tab 回显的观感歧义;
  • 六 Tab 效果图已用 uitest uiInput + snapshot_display 补齐(见 7.1),可进一步固化为一键 capture 脚本纳入仓库;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建(当前 CI 已含 jvmTest 与双 ABI link);
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 社区 CMP compose.ui ohos 制品就位后,把 composeMain 接回 commonMain,让真 Compose UI 在 ComposeArkUIViewController 里回归。

九、总结

9.1 核心难点回顾

上游 chip-textfield(commonMain 全部绑 Compose)
    → 判定"UI 进不来、语义必须出得来"(ohos 线无 Compose 制品)
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 8.14.1 钉版,eazytec 置前)
    → 根构建替换(cmp_qrose 模式)+ composeMain 非标准源码集隔离
    → 引擎提取:提交门/切分/删尾/直选 → 纯 Kotlin 零依赖
    → 24 项 jvmTest 交叉验证钉底
    → 无状态 C ABI(@CName × 1)+ US/RS 控制字符 chips 编码
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析 + describe 视图模型
    → ArkUI 六 Tab 真机验收(录入 / 校验拒绝 / 建议选择 / 删尾 / 清空 / 选择器)

9.2 封装层次

  • 引擎层:纯 Kotlin ChipTextFieldEngine<T>,提交门、分隔符切分、退格删尾、建议直选,零第三方依赖;
  • 示例层:ChipSamples.kt 六类校验器(邮箱正则、标签前缀、优先级、通讯录匹配、建议过滤、状态载荷)按上游 sample 1:1 移植;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理 chips 编码串状态、击键转发、视图模型渲染、状态行与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 路线选择跟着依赖走,不跟着惯例走:KStore"真编译整库"的前提是依赖制品线就位;依赖是 Compose 时,引擎提取是唯一可验收路径——同一社区、同一时期、两种路线并存才是常态,判据是"上游语义能否原样过桥";
  2. "怪"语义是契约的一部分:Done 无条件清空、chips 满后输入框消失、退格只在空文本时删尾——这些不像常规交互的行为恰恰是上游真实行为,原样保留 + 测试钉住,比在 UI 层"顺手修正"更值钱;
  3. 编译器会拦住的错误不值得怕,值得怕的是语义漂移:ArkTS 的 ItemAlign 类型错误六处一起报、一次修完;而分隔符切分多吞一个空段、Done 多清一次输入框这类漂移只有交叉验证能拦——24 项 JVM 用例就是这个库的语义规格。

9.4 适配成果

当前适配已完成:

  • chip-textfield 1.0.0-alpha01(1.0.0-alpha01-ohos.1)纯 Kotlin 引擎的双 ABI Kotlin/Native 编译(引擎零第三方依赖,上游 Compose 文件零修改隔离保留);
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号),九 action 平面 JSON 协议 + US/RS chips 编码;
  • DevEco Stage ArkUI 六 Tab 验收页面(基本 / 邮箱 / Gmail / 标签 / 优先级 / 状态,覆盖上游全部六个示例);
  • 24 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so,约 6.8 MB)、签名、设备安装和真机验证;
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐