开源鸿蒙平台 CMP 三方库「chip-textfield」适配全流程
本文记录
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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin,org.gradle.java.home 已写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 钉版;2.2.21 线不支持 9.x) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 26.0.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.chiptextfield |
1.3 源码依赖度盘点
| 上游文件 | 依赖形态 | 处置 |
|---|---|---|
ChipTextFieldState.kt | mutableStateOf / 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.chips | chips / removeChip / removeAt / clearChips | removeChip 只删首颗匹配(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 | 握手 |
input | kind/chips/text | ok/committed[]/text/chips[]/canAdd | onInput |
commit | kind/chips/text | ok/accepted/reason/chips[]/canAdd | commit |
backspace | kind/chips/text | ok/removed/chips[]/canAdd | onBackspace |
removeAt | kind/chips/index | ok/chips[]/canAdd | removeAt |
clear | kind/chips | ok/chips[]/canAdd | clearChips |
select | kind/chips/name/email | ok/accepted/chips[]/canAdd | addChip |
suggest | kind/query/chips | ok/suggestions[{name,email,initials,color}] | 建议过滤 |
describe | kind/chips | ok/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 方法 | 覆盖内核 |
|---|---|---|
ChipFieldStep | chipFieldStep(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 路径)——两条真实上游语义 |
| Gmail | To / 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.kt | 24 项交叉验证 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、九 action 分发、MiniJson、错误通道 |
nativeApp/.../ChipSamples.kt | 邮箱正则 / 标签前缀 / 优先级 / 通讯录 / 状态 + chips 编解码 |
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 | 鸿蒙适配主文档(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-API | chipFieldStep(request) | 向 ArkTS 暴露能力 |
| ArkTS | applyChips(next) | chips 更新 → describe 视图模型 → 渲染 |
| ArkTS | onInputChanged / 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 功能用例
- 启动后握手行出现
chip-textfield 1.0.0-alpha01-ohos.1 · kotlin 2.2.21-1.0.0(来自真实 Nativeinfo调用,非静态文字); - 基本 Tab:输入
kotlin,harmony→input ok · committed=1,逗号即成 chip,末段留在输入框;Done 提交并清空; - 邮箱 Tab:输入非法邮箱按 Done,
commit · accepted=false reason=...;输入合法邮箱 + 空格,空格分隔符即成 chip; - Gmail Tab:To 行初始带 Raina Shah 头像 chip;输入字母出建议下拉(已选联系人被排除),点建议即成 chip;
- 标签 Tab:初始
#kotlin#compose;连加到 5 颗后输入框整体消失; - 优先级 Tab:
!urgent/*normal/ 裸文本分别成高/中/低三级配色的 chip,✕ 点按删除对应removeAt; - 状态 Tab:切换状态/图标选择器后录入,chip 内 emoji 与状态色圆点随选择变化;初始 Server A / Deploy 两颗 chip 渲染正常;
- 每步操作回显共享状态行;拒绝原因与
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 踩坑复盘
- 只改 ArkTS 页面不算 CMP 适配,必须让引擎真正编译成
ohosArm64/ohosX64动态库,并且由测试证明语义没有漂; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- ArkUI 容器的
alignItems类型随容器变:Row/Column收VerticalAlign/HorizontalAlign,Flex才收ItemAlign——六处Row().alignItems(ItemAlign.Center)在 DevEco 里统一报10505001 ArkTS Compiler Error,全量替换为VerticalAlign.Center后CompileArkTS通过(这是本次真机前最后一个编译错误); - 根构建的
/build与*/build是浅层模式,盖不住example/nativeApp/build这类嵌套目录,gitignore 要写**/build,否则 klib 中间产物混进提交; - 上游 Compose 文件不要试图"剥注解后编进 ohos"——
@Composable改写函数签名,文件离开 Compose 编译器无法表达;整目录隔离进非标准源码集composeMain才是零修改方案; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 命令行
hvigorw assembleHap必须用 DevEco 自带 JBR,系统 JDK 8 会在PackageHap中途报spawn java ENOENT一类不相干错误(Windows 下把DevEco Studio/jbr/bin加入 PATH); - 命令行
hdc的本地路径参数总是相对当前目录解析(file recv/install都会拼接 cwd),在目标文件所在目录内用纯文件名调用最稳; - 真机(x86_64)接受 unsigned HAP,真机必须签名;
build-profile.json5的signingConfigs留空数组,勾选 IDE 自动签名后由 DevEco 回填,提交前保护该文件,证书绝不进入 AtomGit; - 顶部状态行是六 Tab 共享的"最近一次桥操作结果",切 Tab 后看到的是上一次动作的回显——这是与 qrose / scaffolds 同款的单状态行语义,不是状态错乱;
- 其他第三方代码托管镜像站的链接一律不进文档,仓库地址只保留 AtomGit;
8.2 已知问题
- HarmonyOS 上没有 Compose UI:上游
ChipTextField仍在composeMain,鸿蒙侧是 ArkUI 按上游视觉参数重建(默认样式、M3 light 色值、圆角、间距逐项对齐),不是 Compose 渲染; - 引擎无快照可观察性:
chips是普通列表,可观察性由 ArkTS@State承担——这正是无状态桥的边界; enabled/readOnly留在 UI 层:上游是 Composable 参数,引擎恒按可编辑处理,Demo 未暴露只读开关;- 上游
ChipContent.kt71/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.uiohos 制品就位后,把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 三条经验
- 路线选择跟着依赖走,不跟着惯例走:KStore"真编译整库"的前提是依赖制品线就位;依赖是 Compose 时,引擎提取是唯一可验收路径——同一社区、同一时期、两种路线并存才是常态,判据是"上游语义能否原样过桥";
- "怪"语义是契约的一部分:Done 无条件清空、chips 满后输入框消失、退格只在空文本时删尾——这些不像常规交互的行为恰恰是上游真实行为,原样保留 + 测试钉住,比在 UI 层"顺手修正"更值钱;
- 编译器会拦住的错误不值得怕,值得怕的是语义漂移:ArkTS 的
ItemAlign类型错误六处一起报、一次修完;而分隔符切分多吞一个空段、Done 多清一次输入框这类漂移只有交叉验证能拦——24 项 JVM 用例就是这个库的语义规格。
9.4 适配成果
当前适配已完成:
chip-textfield1.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。
参考文档
更多推荐


所有评论(0)