本文记录 number-keyboard 接入 OpenHarmony 的完整过程,覆盖数字键盘库盘点、输入策略从 Composable 中的 1:1 抽取、鸿蒙工具链钉版、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机四 Tab 页面、HAP 构建和真机验收。

number-keyboard 是基于 Compose Multiplatform 的 KMP 数字键盘库:给定当前 amount 字符串、金额上限、小数位上限、货币符号和布局格式(Normal / Inverted / Scrambled / AlwaysScrambled),NumberKeyboard Composable 自己负责"这一按收不收",每次有效输入通过 NumberKeyboardListener.onUpdated(NumberKeyboardData) 把原始金额与八种读数(rawAmount / byte / short / int / long / float / double / currency)吐回给调用方。本次适配与社区两条既有路线都有本质区别:既不是 KStore 式的"整库真编译"——它的输入策略不是现成的纯 Kotlin 模块,而是内联在 Composable 监听器肚子里的三段逻辑加两个私有函数;也不是 qrose / haze 式的"零依赖内核提取"——让 ArkTS 重写接受/拒绝规则必然与 Android / iOS 行为漂移。实际做法是先做1:1 策略抽取(新增 NumberKeyboardEngine + KeyboardLayout 进 commonMain,逐行对照原实现,连 replace(',', '.') 的上游怪癖都原样保留),再编成 OpenHarmony KLIB;上游 Compose UI 原样隔离进 src/composeMain 不参与 ohos 编译,ArkUI 四 Tab 页面作为验收宿主,12 项 JVM 测试锁住"策略没漂"。

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

上游仓库: davidmigloz/number-keyboard

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

number-keyboard 为 KMP 应用提供"金额输入"这一件事的完整答案:键盘不碰业务状态,调用方持有 amount 字符串,按键的接受与拒绝全部由库内策略裁决——空串按 0 拒绝、首位零替换、小数位超限拒绝、超上限按 roundUpToMax 回弹到 formatMaxAmount 拼装的上限值、退格含整数键盘 x.00 尾零截断;NumberKeyboardData 在此之上给出八种读数,currency 的千分位分组与货币符前后缀也是上游实现。按钮长什么样由调用方通过 @Composable lambda 全权定制,NumberKeyboardFormat 四种格式覆盖电话拨号式、计算器式与两种乱序防窥键盘。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套金额输入能力(支付、转账、密码键盘),但上游构建(Kotlin 2.2.0 / Compose Multiplatform 1.8.2 / AGP 8.11.1)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以画出按钮,却无法证明输入策略在 OpenHarmony ARM64 设备上完成编译、链接和执行——"按了没反应"到底是策略拒绝还是 UI 没刷新,永远说不清。适配需要同时验证输入策略、数据模型、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.2.0 + Compose Multiplatform 1.8.2 + AGP 8.11.1 与鸿蒙工具链无法共存于一个 Gradle 构建;社区已审钉法是 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1。
策略位置输入策略内联在 NumberKeyboard Composable 的 NumberKeyboardClickedListener 里,不抽出来就没有东西可编译。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
状态归属上游语义是调用方持有 amount,桥必须保持无状态,否则"两份真相"会让拒绝语义失真。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:输入策略的 1:1 抽取与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。接受/拒绝语义与金额格式化完全由上游原版逻辑负责,ArkTS 只负责持有 amount 状态、调用 Native、解析 JSON 和渲染按钮。

1.2 库提供的能力

Demo 提供四个真实键盘会话,对应上游 sample 的四个页面与桥接的全部入口(上游三平台效果与鸿蒙真机效果对照见第七节):

  • Integer:maxAllowedDecimals = 0,右辅助退格键,大字显示 int——空串按 0 被拒、超上限回弹、退格到空显示 0;
  • Decimal:默认参数(上限 10000、2 位小数、$),左辅助小数点、右辅助退格,大字显示 currency——千分位分组、双小数点拒绝;
  • Biometric:上限 9999、0 位小数、roundUpToMax = false,左辅助指纹占位(上游 clicked = {},不发起调用)——到 9999 后再按被拒且不回弹;
  • Custom:上限 8888.888、3 位小数、€、圆形按钮,Normal / Inverted / Scrambled / AlwaysScrambled 四格式切换——行序镜像、进页乱序、每键重排。

每次按键的响应包含:

字段作用
ok桥通且参数合法,失败时 error 带原文。
accepted策略是否放行;false 对应上游监听器"不回调"的静默语义,UI 保持原状态。
amount接受时的新金额字符串('' 表示退格到空)。
rawAmount / byte / short / int / long / float / doubleNumberKeyboardData 的读数 1:1 直出,不是 ArkTS 侧算的。
currency带千分位分组与货币符的格式化串,走上游 formatAmount。

引擎侧完整能力(首位零、上限钳制、尾零截断、四种布局格式、乱序置换校验)全部在 commonMain 里随编译进入 .so,JVM 侧保留 12 项 desktopTest 交叉验证用例,证明 ohos .so 里执行的输入策略没有漂移。

1.3 实现适配

维度要求
代码复用上游 data/ / listener/ / expect.kt 原样编译,一行不改;新增仅三个纯 Kotlin 文件(NumberKeyboardEngine、KeyboardLayout、ohos actual),Compose UI 原样移入 composeMain 保留对照。
策略抽取监听器逻辑逐行对照抽成 NumberKeyboardEngine,接受返回新 amount、拒绝返回 null,与上游"回调不触发"严格同构。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sample/ Compose 示例与 androidMain / iosMain 源集留盘不进构建。
桥接稳定单个 C ABI 和 UTF-8 JSON,桥无状态,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律commonMain 零第三方依赖(无 Compose、无 serialization),ohos 侧没有依赖矩阵问题。
UI 完整四 Tab 覆盖全部桥接入口:布局拉取、数字 / 小数点 / 退格、拒绝反馈、状态行与格式切换。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游 Compose BOM 1.8.2 与社区 CMP 1.9.2 / HarmonyOS Kotlin 工具链对不齐,Composable 无法编进 ohos;ArkTS Demo 把桥暴露的输入策略画出来,composeMain 里的上游 Composable 保留,等 compose.ui ohos 制品对齐后接回。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与策略位置、确定抽取边界、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、源集重排
第 3 阶段:桥协议建模     ── amount 状态归属、press/decimal/delete/layout JSON 契约、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 布局拉取、四 Tab 键盘、拒绝反馈和状态行
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
lib/                                  上游库模块(进 ohos 构建,jvm("desktop") + ohosArm64 + ohosX64)
lib/src/commonMain/.../data/          NumberKeyboardData + NumberKeyboardFormat(上游原样)
lib/src/commonMain/.../listener/      两个监听接口(上游原样)
lib/src/commonMain/.../expect.kt      三个 expect 声明(上游原样)
lib/src/commonMain/.../engine/        新增:NumberKeyboardEngine + KeyboardLayout(策略抽取)
lib/src/composeMain/                  上游 Compose UI(NumberKeyboard / 按钮,ohos 不编,对照基线)
lib/src/ohosMain/                     新增:expect 的 ohos actual(en_US 风格分隔符)
lib/src/desktopTest/                  引擎 JVM 测试(12 项,交叉验证)
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
sample/ buildSrc/ androidMain/ iosMain/  上游示例与平台源集,留盘不进 settings

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :lib 与 :example:nativeApp,rootProject.name = "ohos_numberkeyboard"。上游的 AGP、Compose Multiplatform 插件、compose-bom 依赖、JitPack 发布与 buildSrc 版本号工具全部随根构建替换剥离——本 fork 不发 JitPack。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,社区已审钉法)
JDK21(Eclipse Temurin,org.gradle.java.home 钉进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证)
Gradle8.14.1(2.2.21 旧线配套;上游 wrapper 8.14.3 降半版)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.numberkeyboard
export JAVA_HOME="/path/to/jdk-21"    # Windows 机用 gradle.properties 的 org.gradle.java.home 钉版
java -version
1.3 源码依赖度盘点
模块/源集文件依赖形态处置
commonMain/data+listener+expect5纯 Kotlin,零第三方依赖原样编译(jvm + 双 ohos)
commonMain/engine(新增)2纯 Kotlin,从 Composable 1:1 抽取ohos 核心进编译
composeMain2androidx.compose.foundation/material3 全家桶隔离,不接任何目标
ohosMain(新增)1expect actualohos 编译
androidMain / iosMain2java.text / Foundation actual留盘不编(目标已剥)
desktopMain1java.text actual服务 jvm("desktop")
sample/ + buildSrc/—Compose 示例工程 + 版本号工具留盘不进 settings

expect 声明全库只有 3 处:getDecimalSeparator() / getGroupingSeparator() / isCurrencySymbolInFront()。上游给了 3 个平台的 actual——android 与 desktop 走 java.text.DecimalFormat,iOS 走 NSLocale。鸿蒙 Kotlin/Native 两头都没有,ohos 补第 4 个,固定 en_US 风格:

// lib/src/ohosMain/kotlin/com/davidmiguel/numberkeyboard/expect.ohos.kt
// HarmonyOS Kotlin/Native exposes no java.text / Foundation locale services,
// so the ohos line pins en_US-style separators to stay deterministic. Callers
// that need another locale pass explicit values to NumberKeyboardEngine /
// NumberKeyboardData instead of relying on these defaults.
actual fun getDecimalSeparator(): Char = '.'
actual fun getGroupingSeparator(): Char = ','
actual fun isCurrencySymbolInFront(): Boolean = true

策略抽取是本适配与"整库真编译"类仓库最大的工时差。NumberKeyboardClickedListener 的三段逻辑(数字 / 左辅助 / 右辅助)加 getNumberOfDecimals / formatMaxAmount 两个私有函数,全部内联在上游 NumberKeyboard.kt 里。抽取纪律只有一条:逐行对照,语义 1:1,不顺手优化——连 appended.replace(',', '.') 硬编码逗号转点(不吃自定义分隔符)这个上游怪癖也原样保留,composeMain 里的原文件保持上游原样不动作为对照基线:

// lib/src/commonMain/.../engine/NumberKeyboardEngine.kt(节选)
class NumberKeyboardEngine(
    val maxAllowedAmount: Double = 10_000.0,
    val maxAllowedDecimals: Int = 2,
    val roundUpToMax: Boolean = true,
    val decimalSeparator: Char = getDecimalSeparator(),
    val groupingSeparator: Char = getGroupingSeparator()
) {
    fun onNumberClicked(amount: String, number: Int): String?      // 接受返回新 amount,拒绝返回 null
    fun onLeftAuxButtonClicked(amount: String): String?            // 小数点
    fun onRightAuxButtonClicked(amount: String): String?           // 退格(含整数键盘尾零截断)
}

// lib/src/commonMain/.../engine/KeyboardLayout.kt(节选)
KeyboardLayout.of(format, digits?)   // rows 三行 + zero;乱序格式用 0..9 置换,可注入固定序列
1.4 页面边界
标题区          Number Keyboard / 页名 / 参数行(与上游 sample 逐字对齐)
Tabs 底栏       Integer / Decimal / Biometric / Custom 四页
金额大字区      int 或 currency(52sp 粗体,随 Tab 语义)
状态行          amount=… raw=… int=… double=… currency=…(验收对账行)
键盘区          4×3:前三行来自 layout.rows,第四行 [左辅助, zero, 右辅助]
格式切换        Custom 页顶部 4 段按钮(Normal / Inverted / Scrambled / AlwaysScrambled)

页面启动时先调 layout op 拉 Normal 布局,Custom 页首次进入时按当前格式再拉。

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

2.1 依赖盘点:零第三方依赖是本适配最大的简化

KStore 的成败手是验证 kotlinx ohos 制品线,number-keyboard 不需要这一步:策略抽取后 commonMain 零第三方依赖——没有 Compose、没有 serialization、没有 coroutines,libs.versions.toml 收敛到只剩 Kotlin 插件一行。上游全部 Compose 依赖随 composeMain 隔离与 sample/ 留盘一起消失在 ohos 构建之外。依赖矩阵、制品换线、测试依赖缺口这三个存储类库的经典障碍在本库上天然不存在,适配重心完全落在"策略抽得准不准"上。

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

社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / KMPSwipe / scaffolds 用的)配 Gradle 8.14.1。KStore 走的 2.4.20 线是为了读 kotlinx 制品的 2.4 元数据,本库 commonMain 零依赖,没有这层约束,按已审旧线走最稳。settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        // HarmonyOS Kotlin distribution must come FIRST, otherwise Gradle resolves
        // the official Kotlin plugin and configuration fails with
        // `Unresolved reference: ohosArm64`.
        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_numberkeyboard"

include(":lib")
include(":example:nativeApp")
2.3 编译真实策略

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

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

    jvm("desktop")                            // 交叉验证用
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {}            // 零第三方依赖

        val desktopTest by getting            // 非默认命名源集没有快捷访问器
        desktopTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

实测:

gradlew.bat :lib:desktopTest                                                    # 12 项全绿
gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

上游 data/ / listener/ / expect.kt 一个文件没动,新增只有 engine 两个文件与 ohos actual 一个文件。

2.4 测试的处置:desktopTest 交叉验证 + 全局禁用 ohos 测试

与社区各仓同款纪律:测试统一在 JVM 跑(desktopTest),根构建全局禁用 ohos 测试任务:

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

两个本仓特有的坑都出在 jvm("desktop") 命名目标上:测试目录是 src/desktopTest/kotlin 而不是 src/jvmTest/kotlin(放错就是 NO-SOURCE 静默跳过);脚本里没有 desktopTest 快捷访问器,要 val desktopTest by getting 再挂依赖。详见 8.1。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.davidmiguel.numberkeyboard.engine/**(策略 + 布局,1:1 抽取)
com.davidmiguel.numberkeyboard.data/**(上游 NumberKeyboardData,原样编译)
        ↓
Bridge.kt(@CName × 1,扁平 JSON 进出,无状态)
        ↓ UTF-8 JSON
N-API numberKeyboardBridge()
        ↓
ArkUI Index.ets(四 Tab,amount 状态归 ArkTS)

请求与响应:

请求 : {"op":"press","key":5,"amount":"12","maxAllowedAmount":10000,"maxAllowedDecimals":2,"roundUpToMax":true}
       {"op":"decimal","amount":"12"}
       {"op":"delete","amount":"12.5","maxAllowedDecimals":2}
       {"op":"layout","format":"Scrambled"}
响应 : {"ok":true,"accepted":true,"op":"press","amount":"12.5","rawAmount":"12.5",
        "byte":12,"short":12,"int":12,"long":12,"float":12.5,"double":12.5,"currency":"$ 12.5"}
       {"ok":true,"accepted":false,"op":"press"}
       {"ok":true,"op":"layout","format":"Scrambled","rows":[[7,3,1],[8,4,2],[9,5,6]],"zero":0}
       {"ok":false,"error":"..."}

两个不对称设计:其一,拒绝不是错误——ok 代表桥通、参数合法,accepted:false 代表策略拒绝,正好对应上游监听器"不回调"的静默语义,UI 侧据此保持原状态并给一行提示;其二,布局是数据——键盘三行行序与零键位置由 layout op 以纯数据推给 UI,ArkTS 永远不自己排键位,Scrambled 的洗牌也发生在 Kotlin/Native 里。

3.2 无状态桥:状态归 ArkTS
private fun config(json: MiniJson): Config = Config(
    engine = NumberKeyboardEngine(
        maxAllowedAmount = json.dbl("maxAllowedAmount", 10_000.0),
        maxAllowedDecimals = json.int("maxAllowedDecimals", 2),
        roundUpToMax = json.bool("roundUpToMax", true),
        decimalSeparator = json.char("decimalSeparator", '.'),
        groupingSeparator = json.char("groupingSeparator", ',')
    ),
    currencySymbol = json.str("currencySymbol").ifBlank { "$" }
)

private fun press(json: MiniJson): String {
    val key = json.int("key", -1)
    if (key !in 0..9) return failure("key must be an integer 0..9")
    val cfg = config(json)
    return result("press", cfg, cfg.engine.onNumberClicked(json.str("amount"), key))
}

上游语义本来就是调用方持有 amount(Compose 里是 remember { mutableStateOf("") }),桥完全同构:每次请求带当前 amount 全量配置,桥算完即忘。这样"按了没反应"永远能二分定位——accepted:false 是策略拒绝,状态没刷是 UI 问题;桥也因此可重入、可并发、无会话泄漏。响应字段全部从 NumberKeyboardData 属性直出(构造一次数据类、逐字段拼接),ArkTS 侧不做第二次格式化——那会绕开上游分组逻辑,两端就对不上了。

3.3 错误通道

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

@CName("NumberKeyboardBridge")
fun numberKeyboardBridge(request: String): String = try {
    /* op 分发 */
} catch (t: Throwable) {
    """{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}

非法 JSON、缺 op、key 越界、未知格式、非法乱序置换(digits 不是 0…9 置换),全部走同一个 ok:false 通道,ArkTS 侧统一渲染。

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

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

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让 ArkTS 复制输入策略,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("NumberKeyboardBridge")
fun numberKeyboardBridge(request: String): String   // press / decimal / delete / layout 一口进
C ABI 符号ArkTS 方法覆盖内核
NumberKeyboardBridgenumberKeyboardBridge(req)NumberKeyboardEngine 三按键 + KeyboardLayout 四格式 + NumberKeyboardData 八读数

HarmonyOS Kotlin 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS 模块(libohosnumberkeyboard.so 里 llvm-nm 得到全局 T 符号,但 ArkTS import 看不到),必须再包一层 NAPI。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"numberKeyboardBridge", nullptr, NapiNumberKeyboardBridge, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosnumberkeyboard_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。API 26 的 NAPI 头把 napi_get_value_string_utf8 缓冲区标成非 const,先量长度再开 std::vector<char>。libohosnumberkeyboard_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。

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

add_library(numberkeyboard_napi SHARED napi_init.cpp)
target_link_libraries(numberkeyboard_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosnumberkeyboard)

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

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

第 5 阶段:ArkUI 宿主

5.1 键盘行序来自桥
aboutToAppear(): void {
  this.fetchNormalLayout();   // 布局一律来自 layout op,Normal/Inverted 也不例外
}

private fetchLayout(format: string, tab: string, apply: (r: LayoutResult) => void): void {
  const req: BridgeRequest = { op: 'layout', format: format };
  const parsed = JSON.parse(numberKeyboardBridge(JSON.stringify(req))) as LayoutResult;
  if (!parsed.ok || parsed.rows === undefined || parsed.rows.length !== 3) {
    this.setStatus(tab, `失败: ${parsed.error ?? 'layout 响应缺少 rows'}`);
    return;
  }
  apply(parsed);
}

键盘三行由 layout.rows 渲染,第四行 [左辅助, zero, 右辅助]——证明摆位逻辑跑在 Kotlin/Native 里。AlwaysScrambled 在每次有效按键被接受后重拉 layout,实现"每键重排";Scrambled 在切入该格式时拉一次并存住。

5.2 页面交互

四 Tab 对应上游 sample 四页:按键统一 OutlineButton 风格(白底、灰边 #D1D5DB、圆角 8vp、weight(1) 高 48vp),Custom 页圆形键(borderRadius '50%' + aspectRatio(1))。数字 / 小数点 / 退格按 tabIndex(Tab 序号)分发到各页的请求构造器;每页大字显示 int 或 currency,状态行显示 amount=… raw=… int=… double=… currency=… 供与响应 JSON 逐字段对账。

5.3 拒绝反馈与错误边界

accepted:false 时状态行提示"已拒绝(超上限/超小数位)"——上游是静默拒绝,静默在验收里没法对账,业务侧同理建议给反馈。所有桥调用包 try/catch,Native 抛错(理论不该发生)显示 失败: … 并进 hilog,页面不崩。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosnumberkeyboard)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/
│       └── Bridge.kt                         @CName + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.numberkeyboard、versionName 5.0.2-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/libohosnumberkeyboard_api.h
        ├── src/main/cpp/types/libnumberkeyboard_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 四 Tab 验收页面
6.2 构建、签名和安装
gradlew.bat :lib:desktopTest                 # 12 项交叉验证
gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录,那是 Gradle 工程)

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组且 product 引用 "default",勾选自动签名后由 IDE 回填完整配置)。

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.numberkeyboard -m entry
hdc shell hilog | findstr NumberKeyboard

四、完整代码对照

4.1 整体架构

composeMain/**(上游 Compose UI,保留对照,ohos 不编)
commonMain/engine/**(NumberKeyboardEngine + KeyboardLayout,1:1 抽取)
    │ onNumberClicked / onLeftAuxButtonClicked / onRightAuxButtonClicked / of(format)
commonMain/data/**(上游 NumberKeyboardData 原样编译)
    │ rawAmount / byte / short / int / long / float / double / currency
Bridge.kt + MiniJson
    │ NumberKeyboardBridge
libnumberkeyboard_napi.so
    │ numberKeyboardBridge
Index.ets
    │ JSON parse + @State 更新(amount 状态归 ArkTS)
    ▼
ArkUI 真机四 Tab 页面

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_numberkeyboard、sample 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 单行钉版
build.gradle.kts(根)版本 5.0.2-ohos.1、全局禁用 Ohos*Test* 任务
lib/build.gradle.kts纯 KMP:jvm(“desktop”) + ohosArm64 + ohosX64,commonMain 零依赖
lib/src/commonMain/.../engine/NumberKeyboardEngine.kt新增:策略 1:1 抽取
lib/src/commonMain/.../engine/KeyboardLayout.kt新增:四格式行序 + 乱序置换
lib/src/ohosMain/.../expect.ohos.kt新增:en_US 风格分隔符 actual
lib/src/desktopTest/.../NumberKeyboardEngineTest.kt新增:12 项交叉验证
lib/src/composeMain/**上游 Compose UI 原样移入(git rename,零改动)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.ktC 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鸿蒙适配主文档

4.3 关键 API 对照

层次API作用
引擎NumberKeyboardEngine(config)按配置构造策略(上限 / 小数位 / 回弹 / 分隔符)
引擎onNumberClicked / onLeftAuxButtonClicked / onRightAuxButtonClicked三按键,接受返回新 amount,拒绝返回 null
布局KeyboardLayout.of(format, digits?)四格式行序 + 零键位,digits 可注入固定置换
数据NumberKeyboardData八读数 + currency 千分位分组(上游原样)
NativeNumberKeyboardBridgeC ABI JSON 导出(单入口四 op)
N-APInumberKeyboardBridge(req)向 ArkTS 暴露能力
ArkTSdispatchDigit / dispatchDecimalKey / dispatchDelete触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const res = JSON.parse(numberKeyboardBridge(JSON.stringify({
  op: 'press', key: 5, amount: this.amount,
  maxAllowedAmount: 8888.888, maxAllowedDecimals: 3, currencySymbol: '€'
})));
// res.accepted === true 时 this.amount = res.amount;res.accepted === false 时状态不变
val engine = NumberKeyboardEngine(maxAllowedAmount = 8888.888, maxAllowedDecimals = 3)
val updated = engine.onNumberClicked(amount, 5)      // null 即拒绝
val data = NumberKeyboardData(updated, engine.decimalSeparator,
                             engine.groupingSeparator, "€")

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;金额格式化只发生在 Kotlin 侧。

五、关键决策说明

决策 1:策略 1:1 抽取,而不是 ArkTS 重写或桥内藏状态

让 ArkTS 重写接受/拒绝规则,"按了没反应"就没有共同裁判,两端行为必然漂移;把策略藏进有状态桥,又会制造第二份 amount 真相。1:1 抽取成 NumberKeyboardEngine 后,Compose 侧与桥侧跑同一份算法,JVM 测试逐条锁定行为——连 replace(',', '.') 的上游怪癖都保留,因为抽取物的唯一使命是做上游语义的镜像,"顺手优化"等于引入回归。

决策 2:Compose UI 隔离 composeMain,ArkUI 重做验收页

上游 Compose BOM 1.8.2 / Kotlin 2.2.0 与社区 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21 对不齐,Composable 强行编进 ohos 等于再打一套 Compose 发行版。composeMain 不是 Kotlin 源集、不接任何目标,上游文件以 git rename 零改动保留——既是对照基线,也是以后 compose.ui ohos 制品对齐时接回的现成材料。

决策 3:根构建替换(cmp_qrose 模式),sample 与平台源集留盘

上游根构建(AGP、Compose Multiplatform 插件、compose-bom、JitPack 发布)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。sample/ Compose 示例工程与 androidMain / iosMain actual 留盘不进 settings.gradle.kts——它们是以后给上游提 PR 的对照物,删除就找不回来了。

决策 4:桥无状态 + 响应字段 NumberKeyboardData 直出

每次请求带全量配置与当前 amount,桥算完即忘——与 Compose 调用方持有 amount 完全同构,可重入、可并发、无会话泄漏。响应里的八读数与 currency 从上游数据类属性直出,ArkTS 只展示不加工:千分位分组、货币符前后缀全部是上游实现,两端展示天然一致。

决策 5:desktopTest 交叉验证 + locale 显式化

策略逻辑在 commonMain,JVM 与 ohos 跑同一份代码;12 项 desktopTest 是"策略没漂"的闸门。ohos 侧 KN 没有 java.text / NSLocale,actual 固定 en_US 风格分隔符——不是丢功能,上游本来就允许显式传分隔符(Custom 页就是这么做的),桥协议里这三个值同样是请求字段,需要其他 locale 由调用方显式传入,没有隐藏的全局状态。

六、测试与验证

6.1 测试环境

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

6.2 JVM 交叉验证

gradlew.bat :lib:desktopTest

12 项用例全部通过(NumberKeyboardEngineTest,全部为本轮新增——上游没有测试,抽取物的行为锁只能自己建):

用例钉住什么
leadingZeroIsNotAppended空串按 0 拒绝、"0" 按数字替换首位零
decimalsAreCapped小数位超限拒绝、恰好卡线接受
exceedingMaxRoundsUpToMaxWhenConfigured超上限回弹到 formatMaxAmount 拼装值(10.00)
exceedingMaxIsRejectedWhenRoundUpDisabledroundUpToMax=false 时静默拒绝(Biometric 语义)
roundUpSnapsToMaxAmount回弹值 10000.00 的拼装格式
decimalSeparatorIsAppendedOnce空串补 0.、重复小数点拒绝
backspaceWalksBackToEmpty退格到空串、空串退格拒绝
backspaceTrimsTrailingZerosForIntegerKeyboards整数键盘 12.00 尾零截断
customSeparatorsAreHonoured自定义分隔符下全部规则生效
layoutsMatchUpstreamRowsNormal / Inverted 行序、乱序固定置换
scrambledLayoutRejectsNonPermutations非法置换直接抛参数异常
shuffledDigitsCoverAllTenDigits随机洗牌覆盖 0…9 全集

抽取即对照 + 测试锁行为,两层合起来才是"策略 100% 复用"的证据:能按键出数字不算验收——那只证明键能按,不证明拒绝语义和上游一致。

6.3 HAP 验证

:lib:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/<abi>/
llvm-nm -D: NumberKeyboardBridge 为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libnumberkeyboard_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully

debugShared 未 strip 的 KN so arm64 约 3.7MB、x86_64 约 3.5MB(含 Kotlin/Native 运行时),各 ABI 目录配 libc++_shared.so 约 1.8MB;HAP 内含双 ABI 各三件套(libnumberkeyboard_napi.so / libohosnumberkeyboard.so / libc++_shared.so)。

6.4 功能用例

  1. 启动后 aboutToAppear 拉 Normal 布局,键盘行序 123/456/789、零键居中——来自 layout op 的真实返回,不是 ArkTS 写死;
  2. Integer 页空串按 0 无反应(accepted:false,状态行提示已拒绝);
  3. Integer 页从 1 连按到 10000(见真机截图),状态行 amount=10000 raw=10000 int=10000 double=10000 currency=$ 10,000——千分位分组 $ 10,000 是 Kotlin/Native 侧上游 formatAmount 的输出;
  4. Integer 页超上限再按一位,formatMaxAmount 回弹(上游对 0 位小数的拼装原样语义),大字 int 仍显示 10000;
  5. Decimal 页按 1 再按 . 得 1.,连按第二个 . 被拒;输入 1234.5 显示 $ 1,234.5;
  6. Biometric 页按到 9999 后再按任何数字被拒且数值不回弹——roundUpToMax=false 与 Integer 页最大的行为差异;
  7. Custom 页切 Inverted 行序镜像(789/456/123),切 Scrambled 进页重洗一次、期间稳定,切 AlwaysScrambled 每次有效按键后整屏重排,零键位置跟随 zero 字段漂移;
  8. 任何一步 ok:false 在状态行显示 失败: …,全程无崩溃。

6.5 验证结论

输入策略、数据模型、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四个 Tab 展示真实的键盘会话:布局来自 Native、按键接受/拒绝与上游语义一致、金额格式化(千分位分组)由 Kotlin/Native 输出(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/number-keyboard.git
cd number-keyboard
export JAVA_HOME="/path/to/jdk-21"
gradlew.bat :lib:desktopTest                         # 12 项交叉验证(Windows;macOS/Linux 用 ./gradlew)
gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,勾选自动签名后构建运行
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.numberkeyboard -m entry
hdc shell hilog | findstr NumberKeyboard

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配——但这个库的策略长在 Composable 肚子里,第一步不是加 ohosArm64(),而是把监听器逻辑 1:1 抽成 NumberKeyboardEngine;抽取纪律是逐行对照、不顺手优化,composeMain 原文件零改动留作对照基线;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Version catalog 的键名决定 Gradle 访问器:kotlin-multiplatform 对应 libs.plugins.kotlin.multiplatform,从别的仓拷 kotlinMultiplatform 写法会报 Unresolved reference——跨仓拷构建脚本的 alias 名要随 toml 键名改;
  4. jvm("desktop") 命名目标的测试目录是 src/desktopTest/kotlin,不是 src/jvmTest/kotlin——放错目录 desktopTest 报 NO-SOURCE 静默跳过,看起来"构建通过"其实一条测试没跑;
  5. 非默认命名源集没有快捷访问器,脚本里要 val desktopTest by getting 再挂 dependencies,直接写 desktopTest.dependencies 是脚本编译错误;
  6. ArkTS 结构体成员变量不能与内置通用属性重名:@State tabIndex 撞 CustomComponent.tabIndex()(焦点顺序方法),编译期报 10505001 Property 'tabIndex' in type 'Index' is not assignable to the same property in base type 'CustomComponent'——改名 currentTabIndex 解决;同类雷区还有 key / id / position / enabled / visibility;
  7. 根锚定的 gitignore 模式盖不住嵌套目录:example/harmonyApp/entry/oh_modules/(DevEco 同步生成的符号链接目录)要单独追加 gitignore,否则混进提交;.hvigor/、.cxx/、entry/build/ 同理;
  8. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. 引擎行为有疑问时先怀疑测试断言再怀疑引擎:"12,55" 退格一次是 "12,5" 不是 "12,"——首跑失败的断言算错了步数,引擎是上游语义的镜像,改的只能是测试;
  11. 签名材料由 DevEco 自动写入本机配置,证书绝不进入 AtomGit。

8.2 已知问题

  • 鸿蒙侧没有 Compose UI,四 Tab 是 ArkUI 重做,按钮观感与 Material3 有差异;composeMain 保留上游 Composable,等社区 compose.ui ohos 制品与 CMP 工具链对齐后接回,ArkUI 降级为 fallback;
  • ohos 的 locale actual 固定 en_US 风格(. / , / 货币符在前);需要其他 locale 由调用方经桥协议显式传参,KN 侧 i18n 能力补齐后可改 actual,业务无感;
  • 桥不持久化配置:每次请求带全量配置(上限、小数位、分隔符),无会话状态——可重入的代价是请求体略长;
  • AlwaysScrambled 的"每键重排"靠 ArkTS 在按键被接受后重拉 layout:订阅式推送过不了同步 C ABI,这与 updates Flow 不跨桥是同一类边界;
  • float / double 是二进制浮点的字符串化,金额计算业务应使用 rawAmount 或 double 配合 decimal 库,这与上游语义一致,不是鸿蒙特有;
  • 上游 sample 的 Material Icons 未搬运,指纹键以文字占位、退格键以 ⌫ 符号呈现;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.numberkeyboard 匹配的签名。

8.3 未来优化方向

  • 把 NumberKeyboardData 之外的扩展读数(如格式化整数字符串)补进桥响应;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和 uitest 自动化截图脚本(四 Tab 几十条拒绝路径,手点容易漏);
  • 若社区 CMP / compose.ui ohos 制品与上游 Compose 版本对齐,把 composeMain 接回编译目标。

九、总结

9.1 核心难点回顾

上游 number-keyboard(策略内联在 Composable、UI 即全部依赖)
    → 1:1 策略抽取(NumberKeyboardEngine + KeyboardLayout 进 commonMain)
    → 工具链对齐 HarmonyOS Kotlin 2.2.21-1.0.0(Gradle 8.14.1,cmp_qrose 已审钉法)
    → 根构建替换(cmp_qrose 模式,sample 与 android/ios 源集留盘)
    → ohosArm64 / ohosX64 KLIB + en_US actual
    → C ABI(@CName 单入口四 op)+ 无状态桥
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析(amount 状态归 ArkTS)
    → ArkUI 真机四 Tab 验收(布局 / 拒绝 / 回弹 / 乱序重排 / 千分位分组)

9.2 封装层次

  • 策略层:NumberKeyboardEngine 三按键 + KeyboardLayout 四格式(1:1 抽取,JVM 测试锁定);
  • 数据层:上游原版 NumberKeyboardData 八读数与 currency 千分位分组;
  • UI 层:上游 Compose UI 原样保留在 composeMain(不编 ohos)+ ArkUI 四 Tab 验收页;
  • Native 层:生成双 ABI 动态库并通过单个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:持有 amount 状态、页面状态、操作分发、拒绝反馈与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 库的"值钱部分"长在哪,决定适配的第一刀:qrose 的编码器天然在 commonMain,KStore 整库纯 Kotlin 只补 8 行 actual,number-keyboard 的策略长在 Composable 肚子里——必须先做 1:1 抽取再谈编译。抽取物是上游语义的镜像,抽取质量(逐行对照 + JVM 测试锁行为)决定鸿蒙侧是否真的"复用了上游",而不是"看起来像上游";
  2. 工具链线经验不能互相套用:KStore 为读 kotlinx 制品 2.4 元数据走 2.4.20 线留 Gradle 9.x;本库 commonMain 零第三方依赖,没有版本元数据约束,按社区已审旧线 2.2.21-1.0.0 + Gradle 8.14.1 走最稳。选线依据是上游依赖形态,不是"越新越好";
  3. 无状态桥 + 状态归 ArkTS 是交互类库的正确桥形:与 Compose 调用方持有 amount 完全同构,ok 与 accepted 两级响应把"桥坏了"和"策略拒绝"干净二分;交互库的验收重点不在渲染闭环,而在拒绝路径——空串按 0、双小数点、超上限不回弹,每一条都要在对账状态行上看到 accepted:false。

9.4 适配成果

当前适配已完成:

  • number-keyboard 5.0.2(5.0.2-ohos.1)输入策略的双 ABI Kotlin/Native 编译(上游 data/ / listener/ 零改动,新增仅 engine 两文件 + actual 一文件);
  • 输入策略 1:1 抽取与 12 项 JVM 交叉验证全绿(首位零、上限回弹、roundUpToMax=false、尾零截断、自定义分隔符、四格式行序、乱序置换);
  • 单个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 四 Tab 验收页面(Integer / Decimal / Biometric / Custom,布局拉取、拒绝反馈、AlwaysScrambled 每键重排、千分位分组对账);
  • HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证(Integer 页真机截图,currency=$ 10,000 为 Kotlin/Native 输出);
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐