本文记录 compose-country-code-picker 接入 OpenHarmony 的完整过程,覆盖国家数据依赖盘点、Compose 源码集隔离、搜索语义移植、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

compose-country-code-picker 是基于 Compose Material 3 的 Kotlin Multiplatform 国家区号选择器:一个带搜索框的懒加载国家列表(250 条),支持按国家名或 +86 形式的区号过滤、国旗 emoji 前置展示、初始滚动定位到默认国家(USA),典型用途是手机号输入表单选区号。本次适配与 KStore 那批"真编译整库"路线(社区 Maven 上 kotlinx ohos 制品线已齐)有本质区别:上游的两个数据依赖 com.vanniktech:multiplatform-locale 与 com.doist.x:normalize 都没有 ohos 制品,UI 又是 100% Compose 1.12 API——因此走的是社区早期验证过的内核提取路线(qrose / WheelPicker / FlexibleBottomSheet 同款):把值得保留的语义(国家表 + 三段式搜索规则 + 初始定位)移植为纯 Kotlin commonMain 编成 .so,上游 Compose 源码一字不改隔离进 src/composeMain,ArkUI 页面作为验收宿主。其中最关键的一步是国家数据的同源内置:250 条数据直接从上游所依赖的 multiplatform-locale:0.11.0 JVM 制品导出,与上游语义零漂移。

项目地址: AtomGit/oh-tpc/compose-country-code-picker.git

上游仓库: eygraber/compose-country-code-picker

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

compose-country-code-picker 为 Compose Multiplatform 应用提供"选国家区号"这一件事的完整答案:一行 CountryCodePicker(onClick = { country -> ... }) 得到一个可搜索的国家列表。搜索语义是精心设计的纯逻辑——defaultSearch lambda 区分三种输入:+ 前缀按完整区号匹配(+86 → 中国)、纯数字按去 + 区号前缀匹配(1 → 整个北美区号段)、其他按显示名匹配且先原文后去重音折叠(cote → Côte d’Ivoire)。列表项是「国旗 emoji + 国名 (区号)」,initialCountry 参数控制首屏滚动位置(默认 Country.USA),countries / search 参数开放定制。上游把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套区号选择能力,但上游构建(Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 / Compose 1.12.2)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示一个列表,却无法证明搜索语义与国家数据在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证国家数据表、搜索引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 + com.eygraber.conventions 0.0.104 插件体系,与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵multiplatform-locale 只发布 android/jvm/ios klib(无 ohosArm64 变体可消费);doistx-normalize 按 jvm/apple/js/linux 平台分发实现,同样没有 ohos 制品。
Compose 版本断层上游 UI 用的是 Compose 1.12 / Kotlin 2.4 的 Material 3 API(TextField(state = TextFieldState)),社区鸿蒙 CMP 是 1.9.2,强行对齐等于再发一套 Compose。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:国家数据表的 ohos 编译、搜索语义的逐条移植、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。数据的单一真相源完全放在 Kotlin 侧,ArkTS 只负责输入、调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的区号选择会话,对应桥接的全部入口:

  • 握手(info):返回 fork 版本、上游版本、国家总数、默认国家——来自真实 Native 调用而非静态文字;
  • 全表(countries):启动时拉取全部 250 条国家数据渲染列表;
  • 搜索(search):搜索框每敲一个字走一次引擎 op,+86 / 86 / china / cote 行为与上游一致;
  • 初始定位(initial):按上游默认(USA)解析初始下标,列表一次滚到位;
  • 选中回调(onClick):点选国家后顶部横幅回填「国旗 + 国名 (区号) + ISO code」,对应上游 onClick(country) 契约。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op 回显 / querysearch 回显本次搜索词;count 报告命中条数。
rows国家行数据(\n 行分隔、`
index / totalinitial 返回初始下标与全表规模。

引擎侧完整能力(250 国数据、三段式搜索、去重音折叠名、默认国家定位)全部在 commonMain 里随编译进入 .so,JVM 侧保留 14 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的搜索逻辑没有漂移。

1.3 实现适配

维度要求
代码复用上游 defaultSearch 的匹配语义逐条移植(区号带 + 匹配、区号去 + 前缀匹配、名字忽略大小写 + 折叠名匹配);国家数据从上游所依赖的 multiplatform-locale:0.11.0 JVM 制品同源导出内置;上游两个 Compose 源文件零改动隔离。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);eygraber conventions / AGP / detekt / Maven Central 发布全部剥离;上游 CI 文件留盘不进构建。
桥接稳定使用 1 个 C ABI 符号和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律工具链钉 HarmonyOS Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(2.2.21 旧线只认 Gradle 8.x);两个无 ohos 制品的数据依赖以内置数据表替代,版本与上游完全一致(0.11.0 / 1.4.0 语义)。
UI 完整单页覆盖全部桥接入口:握手行、搜索框、命中计数、国家列表、选中横幅。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游 CountryCodePicker() 是 100% Material 3 组合(LazyColumn + TextField + ListItem),本 fork 把它隔离在 src/composeMain;ArkTS Demo 复刻的是选择器的交互语义,每一个字节的国家数据和每一次匹配决策都来自 Kotlin 侧。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认数据依赖无 ohos 制品、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 钉版
第 3 阶段:桥协议建模     ── 行数据平铺契约、search/info/countries/initial 四 op、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 搜索框、国家列表、初始定位、选中横幅和握手行
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
library/                              上游库模块(进 ohos 构建,jvm + ohosArm64 + ohosX64)
library/src/commonMain/               新增内核:CountryInfo / Countries.kt(250 国内置表)/ Search 引擎
library/src/jvmTest/                  新增:14 项 JVM 交叉验证
library/src/composeMain/              上游 CountryCodePicker.kt + SearchIcon.kt(零改动隔离,不编译)
example/nativeApp/                    Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/                   DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
.github/ Dangerfile.df.kts detekt.yml 上游 CI 文件,留盘不进构建

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_compose_country_code_picker"。上游的 com.eygraber.conventions 五件套插件(kotlin-multiplatform / android-kmp-library / compose-jetbrains / detekt2 / publish-maven-central)、AGP classpath、develocity 构建扫描、renovate 全部随根构建替换剥离——本 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 9.8.1 必须降级;2.2.21 旧线只认 Gradle 8.x)
上游工具链(被替换)Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 / Compose JetBrains 1.12.2
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.countrycodepicker
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
源文件行数依赖形态处置
CountryCodePicker.kt334100% Compose Material 3(含私有 defaultSearch lambda)拆出搜索逻辑进 commonMain,其余整体隔离进 composeMain
SearchIcon.kt64ImageVector.Builder 手绘 search 图标(androidx.compose.ui.graphics)整体隔离进 composeMain

上游外部依赖的处置是这个适配的核心决策点:

依赖上游角色ohos 制品处置
com.vanniktech:multiplatform-locale:0.11.0Country enum(250 国 code/code3/emoji/区号)+ 平台 ICU 本地化名无(仅 android/jvm/ios klib)数据同源内置:从其 JVM 制品导出 250 行 Kotlin 数据表
com.doist.x:normalize:1.4.0搜索名 NFD 折叠(UAX #15)无(jvm/apple/js/linux 平台分发)预计算成数据列:生成数据表时折叠一次,运行时零依赖
compose-material3 1.9.0 + compose-runtime 1.12.2全部 UI无(社区线是 CMP 1.9.2)上游 UI 隔离,ArkTS 复刻

数据导出的具体做法见 2.3。expect 声明全库为 0——上游没有 expect/actual,本 fork 也没有新增平台源码集,内核是纯 common Kotlin。

1.4 页面边界
标题区          CountryCodePicker / 握手行(0.5.2-ohos.1 · 250 countries from the Kotlin engine)
选中横幅        国旗 emoji + 国名 (区号) + ISO code + Clear 按钮(对应上游 onClick 契约)
搜索框          单行输入(placeholder 提示 name / +86 / 86 三种搜索形态)
计数行          命中数 / 全表规模(如 1 / 250 countries)
国家列表        List + ForEach:国旗 emoji 前置 + 国名 (首个区号) 标题行,多区号国家副行展开
                首屏 initialIndex 定位到 USA(上游 rememberLazyListState 语义)

页面启动时先调 info op 做握手,再 countries 拉全表,最后 initial 解析初始下标。

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

2.1 依赖制品核查(本适配的路线分岔点)

先查社区 Maven(https://maven.eazytec-cloud.com)与 Maven Central 上有没有两个数据依赖的 ohos 变体。用 .module 元数据逐个验证:

com.vanniktech:multiplatform-locale:0.11.0
  variants: androidApi-published / jvmApi-published / iosArm64Api-published ...(无 ohos)
com.doist.x:normalize:1.4.0
  variants: jvm / apple / js / linux 平台分发(无 ohos)

结论:kotlinx 那条 ohos 制品线(KStore 适配时验证过的 coroutines / serialization / kotlinx-io)救不了这两个库——它们不是 kotlinx 系。fork 上游依赖加 ohos 目标是另一个库的适配工程量(要补 250 国 CLDR 本地化 actual),不属于本库范围。路线就此定为内核提取 + 数据同源内置:与 qrose(编码器留 commonMain)、WheelPicker(内置历法数据表替代 java.time)同一族先例。

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

本库内核不依赖任何 kotlinx ohos 制品,不需要迁就 2.4 元数据——直接钉社区批准的旧线 HarmonyOS Kotlin 2.2.21-1.0.0。代价是 Gradle 必须从上游的 9.8.1 降到 8.14.1:2.2.21 线的 Kotlin 插件在 Gradle 9.x 上加载即失败,这是与 KStore(2.4.20 线留 9.x)相反方向的一次降级,两条工具链线的经验不能互相套用。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        // HarmonyOS Kotlin distribution must come BEFORE gradlePluginPortal/google,
        // otherwise the ohosArm64()/ohosX64() factory functions and the ohosMain
        // source set are reported as unresolved references.
        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_compose_country_code_picker"

include(":library")
include(":example:nativeApp")

gradle.properties 钉 JDK 与版本:org.gradle.java.home=C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot、version=0.5.2-ohos.1。

2.3 国家数据同源导出(本适配的成败手)

数据必须与上游 Country.entries 同源,不能手抄。做法是用一段 30 行的 Java 程序消费上游同版本制品:

1. 从 Maven Central 拉取 com.vanniktech:multiplatform-locale-jvm:0.11.0(与上游 toml 同版本)
2. 遍历 Country.values():
   - code / code3 / emoji / callingCodes 直接取自 enum(保持上游顺序)
   - 英文名走 new Locale("", code).getDisplayCountry(Locale.ENGLISH)
     ——与该库 JVM actual 的 ICU 路径完全一致(Territory.displayName())
   - 折叠名 Normalizer.normalize(name, NFD).replaceAll("\\p{M}", "")
     ——正是上游 defaultSearch 运行时算的东西,预计算成 norm 列
3. OutputStreamWriter(UTF_8) 直写文件(System.out 重定向在 Windows 会把 emoji 写成 "?")
4. 生成 Countries.kt:一行一国,头部保留 Apache 2.0 署名与 GENERATED 标注

生成结果:250 行,与上游 enum 逐条对应(CountryInfo("CN", "CHN", "🇨🇳", "China", "China", listOf("+86")) 形态)。normalizedName 列的存在让运行时不再需要任何 UAX #15 实现——上游的运行时折叠 + memo 缓存,变成数据生成期的一次性计算。

2.4 编译内核

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

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

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)

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

    sourceSets {
        commonMain.dependencies {}
        jvmTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

内核三个源文件:CountryInfo.kt(数据类 + CountryRegistry 表/查码/初始下标)、Countries.kt(生成数据)、CountryCodePickerSearch.kt(searchCountries(),上游 defaultSearch 的逐条移植)。实测:

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

上游测试的处置:上游 commonMain 本来就没有测试文件,因此本 fork 新写 14 项 jvmTest 作为交叉验证(见 6.2)。ohos 侧测试无 runner,根构建全局禁用:

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

第 3 阶段:桥协议建模

3.1 JSON 契约
com.eygraber.compose.country.code.picker/**(内核:国家表 + 搜索引擎)
        ↓ CountryRegistry / searchCountries
Bridge.kt(@CName × 1,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API countryCodePickerDispatch()
        ↓
ArkUI Index.ets(单页列表)

请求与响应:

请求 : {"op":"info"}
       {"op":"countries"}
       {"op":"search","query":"+86"}
       {"op":"initial","code":"US"}      // code 可省,默认 US
响应 : {"ok":true,"count":250,"rows":"CN|CHN|🇨🇳|China|+86\nUS|USA|🇺🇸|United States|+1\n..."}
       {"ok":true,"index":233,"total":250}
       {"ok":false,"error":"..."}

一个不对称设计:请求是扁平标量 JSON(MiniJson 只解析顶层对象、无嵌套无数组);响应的 rows 是两层分隔符平铺的行文本——'\n' 分行、'|' 分字段、多区号 ',' 相连。不做嵌套对象数组的原因见 3.2。

3.2 载荷形态为什么是行文本而不是嵌套 JSON

ArkTS 严格模式对 JSON.parse 派生的嵌套对象在类型化上下文里有已知限制(FlexibleBottomSheet 适配时记录过:the ArkTS runtime rejects JSON.parse-derived objects in typed contexts)。250 条国家数据如果走嵌套数组,每一条都要经过 as CountryRow[] 这层不可靠的边界。行文本方案下 ArkTS 侧自己 split('\n') / split('|'),类型从字符串字面量构造,完全绕开该限制——这是 WheelPicker('a|b|c' 竖线串)与 tanstack-table(平铺字段)两个已审仓库验证过的同款做法。

字段顺序有一个刻意设计:code|code3|emoji|displayName|callingCodes,callingCodes 永远是行尾列。原因是 CLDR 英文国家名可能含逗号(“Bolivia, Plurinational State of”),若区号不在行尾,名字里的逗号会和区号分隔符冲突。ArkTS 侧按前 4 个 | 切开、remainder 整体归 callingCodes,冲突在结构上不可能发生。

3.3 错误通道

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

@CName("CountryCodePickerDispatch")
fun countryCodePickerDispatch(request: String): String {
    return try {
        val json = MiniJson.parse(request)
        when (json.str("op")) {
            "info" -> info()
            "countries" -> countries()
            "search" -> search(json)
            "initial" -> initial(json)
            else -> err("unknown op '${json.str("op")}'")
        }
    } catch (t: Throwable) {
        err(t.message ?: t::class.simpleName ?: "error")
    }
}

非法 JSON、缺 query、不支持的 op、空 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染到错误横幅。

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

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

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

4.2 Kotlin/Native 导出函数
@CName("CountryCodePickerDispatch")
fun countryCodePickerDispatch(request: String): String   // info / countries / search / initial 一口进
C ABI 符号ArkTS 方法覆盖内核
CountryCodePickerDispatchcountryCodePickerDispatch(req)CountryRegistry.all 全表 + searchCountries() 三段式搜索 + indexOf() 初始定位 + 握手

引擎无状态:query 由 ArkTS 持有,每次请求自带全部上下文——选择器没有拖拽帧/跨调用状态机,不需要 FlexibleSheet 那种会话 id 模式。

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

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

为什么必须包一层 N-API so:HarmonyOS Kotlin 的 CAdapter 只给 ArkTS 挂 stdlib 类型模块,不把 @CName 业务函数挂到 ArkTS module——llvm-nm -D 能看到 T CountryCodePickerDispatch,但 ArkTS import 'libohoscountrycodepicker.so' 后函数仍是 undefined。libcountrycodepicker_napi.so 才是 ArkTS 实际 import 的 N-API 面。

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

add_library(countrycodepicker_napi SHARED napi_init.cpp)
target_link_libraries(countrycodepicker_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohoscountrycodepicker)

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

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

第 5 阶段:ArkUI 宿主

5.1 启动注入和搜索触发
aboutToAppear(): void {
  const info = JSON.parse(countryCodePickerDispatch('{"op":"info"}')) as InfoResp;   // 握手
  this.forkVersion = info.fork;
  this.totalCount = info.countries;
  const all = JSON.parse(countryCodePickerDispatch('{"op":"countries"}')) as RowsResp;
  this.rows = parseRows(all.rows);
  const init = JSON.parse(countryCodePickerDispatch('{"op":"initial"}')) as InitialResp;
  this.initialIndex = init.index;   // List({ initialIndex }) 对应上游 rememberLazyListState
}

private search(term: string): void {
  const req = `{"op":"search","query":"${escapeJson(term)}"}`;
  const resp = JSON.parse(countryCodePickerDispatch(req)) as RowsResp;
  this.rows = parseRows(resp.rows);
  this.matchCount = resp.count;    // @State 刷新列表与计数行
}

搜索框 TextInput.onChange 每次变化都走 search op——空白词也发请求,由引擎返回全表(上游在组合层守卫空串,本 fork 把守卫下沉进内核,规则单一真相源)。escapeJson 对 \、"、换行做转义后拼进请求 JSON,MiniJson 的引号状态机保证串内逗号安全。

5.2 页面交互

单页自上而下:标题与握手行(fork 版本 + 国家规模,来自真实 Native 调用)、选中横幅(国旗 emoji + China (+86) + ISO code 灰字 + Clear 按钮)、搜索框(placeholder 提示三种搜索形态)、命中计数行(1 / 250 countries)、国家列表。列表项复刻上游 ListItem 结构:DefaultCountryItemLeadingContent 对应国旗 emoji 前置(40vp 定宽),DefaultCountryItemHeadlineContent 对应 国名 (首个区号) 标题行,多区号国家(如 +1 段内多区号)副行灰字展开全量区号。ForEach 的 key 用 row.code(行数据不可变,不存在漏字段刷新问题)。

5.3 对比度和错误边界

搜索框与列表遵循系统默认主题;错误横幅深红字(#b00020),ok:false 的原文、Native 抛错(理论不该发生)统一被 try/catch 收进同一展示位。选中态用横幅而非弹窗,保证 Clear 路径一眼可见。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohoscountrycodepicker)+ cxxShared + copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 1 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.countrycodepicker、versionName 0.5.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/libohoscountrycodepicker_api.h
        ├── src/main/cpp/types/libcountrycodepicker_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面
6.2 构建、签名和安装
.\gradlew.bat :library:jvmTest                                             # 14 项交叉验证
.\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 中勾选自动生成,证书写入本机 ~/.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.countrycodepicker -m entry

四、完整代码对照

4.1 整体架构

com.eygraber.compose.country.code.picker/**(内核,commonMain)
    │ CountryInfo / Countries.kt(250 国内置表)/ searchCountries()
Bridge.kt + MiniJson
    │ CountryCodePickerDispatch(info / countries / search / initial)
libcountrycodepicker_napi.so
    │ countryCodePickerDispatch
Index.ets
    │ split rows + JSON.parse + @State 更新
    ▼
ArkUI 真机页面(搜索框 + 国家列表 + 选中横幅)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_compose_country_code_picker
gradle/libs.versions.toml仅 kotlin = "2.2.21-1.0.0" 两行目录
build.gradle.kts(根)版本 0.5.2-ohos.1、全局禁用 Ohos*Test* 任务
gradle.propertiesJDK 21 钉版、kotlin.native.* 开关
library/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64 + jvmTest
library/.../CountryInfo.kt数据类 + CountryRegistry(表 / fromCode / indexOf)
library/.../Countries.kt生成的 250 行国家表(Apache 2.0 署名,勿手改)
library/.../CountryCodePickerSearch.ktsearchCountries():上游 defaultSearch 逐条移植
library/src/jvmTest/.../CountryCodePickerSearchTest.kt14 项交叉验证
library/src/composeMain/...上游 CountryCodePicker.kt + SearchIcon.kt(零改动,rename 相似度 100%)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt1 个 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 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(18 节)

4.3 关键 API 对照

层次API作用
引擎CountryRegistry.all250 国内置表,顺序同上游 Country.entries
引擎CountryRegistry.fromCode(code)对应上游 Country.Companion.fromOrNull
引擎CountryRegistry.indexOf(code)初始滚动下标(null→US、未知→0,上游 coerceAtLeast 语义)
引擎searchCountries(term, countries)上游 defaultSearch 三段式匹配(区号带 + / 纯数字 / 名字 + 折叠名)
数据CountryInfo.normalizedName上游运行时 NFD 折叠的预计算形态
NativeCountryCodePickerDispatchC ABI JSON 导出(唯一符号)
N-APIcountryCodePickerDispatch(req)向 ArkTS 暴露能力
ArkTSsearch(term)触发引擎搜索并刷新列表与计数

4.4 ArkTS 与 Kotlin 的边界

// ArkTS:搜索 "86" —— 只发起与渲染,不做任何匹配决策
const req = `{"op":"search","query":"${escapeJson('86')}"}`;
const resp = JSON.parse(countryCodePickerDispatch(req)) as RowsResp;
// resp.rows === "CN|CHN|🇨🇳|China|+86",resp.count === 1
// Kotlin:三段式匹配在上游语义下逐条判定
val trimmedSearchTerm = searchTerm.trim()
val isPlusPrefix = trimmedSearchTerm[0] == '+'
val isCallingCode = isPlusPrefix || trimmedSearchTerm.toString().toIntOrNull() != null
return countries.filter { country ->
    if (isCallingCode) {
        country.callingCodes.any { callingCode ->
            if (isPlusPrefix) callingCode.startsWith(trimmedSearchTerm)
            else callingCode.removePrefix("+").startsWith(trimmedSearchTerm)
        }
    } else {
        val isDisplayNameMatch = country.displayName.contains(searchTerm, ignoreCase = true)
        isDisplayNameMatch || country.normalizedName.contains(searchTerm, ignoreCase = true)
    }
}

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

五、关键决策说明

决策 1:国家数据同源内置而不是 fork multiplatform-locale

给 multiplatform-locale 补 ohos 目标是另一个库的适配工程(还要补 250 国各语言 CLDR actual)。本库只需要它的数据:用 30 行 Java 消费其 0.11.0 JVM 制品、走它自己 JVM actual 的 ICU 路径导出英文名——生成表与上游在英文环境下逐条一致,jvmTest 锁死 250 条规模与顺序。先例是 WheelPicker 内置历法数据表替代 java.time。

决策 2:折叠名预计算而不是移植 UAX #15

上游搜索的去重音折叠靠 doistx-normalize 的运行时 NFD + \p{M} 剥离 + memo 缓存。ohos 上既无该制品也无系统 UAX #15 API。但搜索只需要"名字折叠"这一个用途——在数据生成期折叠一次存成 normalizedName 列,运行时一行 contains,整个 normalizer 连同缓存都不进 .so。行为等价:cote 命中 Côte d’Ivoire 与上游一致(jvmTest 有对应用例)。

决策 3:工具钉 2.2.21-1.0.0 旧线,Gradle 降 8.14.1

内核不依赖 kotlinx ohos 制品线,无需迁就 2.4 元数据,直接钉社区批准的 2.2.21-1.0.0(qrose / FlexibleBottomSheet / WheelPicker 同款)。代价是 Gradle 从上游 9.8.1 降到 8.14.1——2.2.21 旧线只认 Gradle 8.x。与 KStore(依赖逼着升 2.4.20、Gradle 留 9.x)恰好是两个方向,印证"工具链版本跟着依赖走"。

决策 4:根构建替换(cmp_qrose 模式),composeMain 隔离

上游根构建(eygraber conventions 五插件 / AGP / detekt / dokka / Maven Central 发布 / develocity)对鸿蒙没有保留价值,整体替换。上游两个 Compose 源文件挪进 src/composeMain 零改动保留——git 提交记录里它们是 rename(相似度 100%),等社区 CMP 与上游 Compose 版本对齐后可直接搬回 commonMain。

决策 5:jvmTest 交叉验证 + 无状态桥

上游没有测试文件,本 fork 新写 14 项 jvmTest(数据完整性 + 三段式搜索 + 初始定位)跑在与 ohos .so 相同的 commonMain 源上,作为"内核没漂"的闸门。桥无状态(每次请求自带全部上下文):选择器没有跨调用状态机,query 由 ArkTS 持有,最省 FFI 往返。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 11(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。注意 Git Bash 下需先 export JAVA_HOME 再跑 gradlew.bat(wrapper 启动阶段就要 JVM,gradle.properties 里的 org.gradle.java.home 只管 daemon)。

6.2 JVM 交叉验证

.\gradlew.bat :library:jvmTest

14 项用例全部通过(本 fork 新增,跑在与 ohos .so 相同的源上):

用例钉住什么
registryHasTheFullTable250 条、code 无重复、AF 开头(与上游 enum 同序)
lookupHelpersBehaveLikeUpstreamCompanionfromCode 大小写、未知码 null
initialIndexDefaultsToUsaAndCoercesUnknownCodesnull→US 下标、未知码→0(上游 coerceAtLeast 语义)
blankTermReturnsEveryCountry空白 → 全表
plusPrefixMatchesCallingCodeWithPlus+86 → 只有 CN
bareDigitsMatchCallingCodeWithoutPlus86 → 只有 CN
digitOneMatchesTheWholeNanpBloc1 → US、CA(整个北美区号段)
nameSearchIsCaseInsensitiveCHINA → CN、HK、MO(CLDR contains 语义,与上游英文环境一致)
nameSearchMatchesThroughStrippedAccentscote → CI(折叠名命中)
accentedTermStillMatchesTheRawDisplayNameCôte → CI(原名命中)
unknownTermReturnsEmptyList乱串 → 空
emojiFlagsAreRegionalIndicatorPairsOfTheCode每条 emoji 反推回 code
everyRowCarriesAtLeastOneCallingCode区号完整、全部带 + 前缀
searchAcceptsASubsetList引擎支持子集列表(对应上游 countries 参数)

6.3 so 与 HAP 验证

:library:compileKotlinJvm / compileKotlinOhosArm64 / compileKotlinOhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
llvm-nm -D: CountryCodePickerDispatch / libohoscountrycodepicker_symbols 均为全局 T 符号(双 ABI)
BuildNativeWithCmake: finished(双 ABI 编出 libcountrycodepicker_napi.so)
PackageHap: finished
install bundle successfully
start ability successfully

libohoscountrycodepicker.so arm64 约 2.59 MB、x86_64 约 2.16 MB(debugShared,含 Kotlin/Native 运行时),配对 triple 的 libc++_shared.so 分别约 1.85 MB / 1.73 MB。

6.4 功能用例

  1. 启动后页头出现 0.5.2-ohos.1 · 250 countries from the Kotlin engine(来自 info op 的真实 Native 调用,非静态文字);
  2. 全表拉取后列表渲染 250 国,首屏定位到 initial op 返回的美国下标(上游 Country.USA 默认语义);
  3. 搜索框输入 86,列表收敛到唯一一条 🇨🇳 China (+86),计数行变 1 / 250 countries——纯数字按区号前缀匹配;
  4. 输入 +86 结果同样只有中国——+ 前缀按完整区号匹配;
  5. 输入 1 返回整个北美区号段(US、CA 等多条);
  6. 输入 china / CHINA 忽略大小写命中,且 Hong Kong SAR China、Macao SAR China 一并命中(与上游 contains 语义一致);
  7. 输入 cote 命中 Côte d'Ivoire——预计算折叠名在起作用;
  8. 清空搜索框恢复 250 条;
  9. 点选国家后顶部横幅回填「国旗 + 国名 (区号) + ISO code」,Clear 按钮可清除——对应上游 onClick(country) 契约。

6.5 验证结论

国家数据表、搜索引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的区号选择会话:版本握手、全表渲染、三段式搜索、初始定位与选中回填闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-country-code-picker.git
cd compose-country-code-picker
export JAVA_HOME="/path/to/jdk-21"                                                 # Windows Git Bash 需显式导出
./gradlew.bat :library:compileKotlinOhosArm64 :library:compileKotlinOhosX64        # klib
./gradlew.bat :library:jvmTest                                                     # 14 项交叉验证
./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.countrycodepicker -m entry
hdc shell hilog | grep CountryCodePicker

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :library 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Gradle 9.8.1 的上游 wrapper 与 Kotlin 2.2.21 线不兼容,必须降 8.14.1——工具链线与 Gradle 版本的配对关系不能跨线套用(2.4.20 线可以留 9.x);
  4. Windows 上导出数据时 System.out 重定向会把 emoji/重音写成 ?(US-ASCII 编码器),必须 OutputStreamWriter(UTF_8) 直写文件并用 hexdump 验证字节(f0 9f 87 a8);
  5. Java IntStream.map 只接受 int→int,返回 String 的 lambda 编译即错,改 codePoints().toArray() 加循环;
  6. CLDR 英文国家名含逗号(“Bolivia, Plurinational State of”),行文本编码里 callingCodes 必须是行尾列,前 4 列切开后 remainder 整体归区号——分隔符冲突在结构上不可能发生;
  7. ArkTS 严格模式拒绝 JSON.parse 派生的嵌套对象进类型化上下文,250 国数据用 \n/| 行文本平铺传递;
  8. ohos 无测试 runner:根构建全局禁用 Ohos*Test* 任务,交叉验证统一走 jvmTest;
  9. Git Bash 直接 ./gradlew.bat 报 JAVA_HOME is not set——wrapper 启动阶段就需要 JVM,org.gradle.java.home 救不了,先 export JAVA_HOME=...;
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前用 git update-index --skip-worktree 保护,证书绝不进入 AtomGit。

8.2 已知问题

  • 内置表英文名是 CLDR English(与上游 JVM/Android 平台英文环境一致),不随系统语言本地化——上游靠平台 ICU actual,ohos 侧待补 @ohos.i18n actual;
  • 上游的槽位定制 API(countryItemLeadingContent / countryItemHeadlineContent / ListItemColors)不在桥上:那是 Compose 组合层定制点,ohos 侧由 ArkTS 开发者直接改 ArkUI 布局,内核不感知;
  • 上游 Country 是 250 值 enum,本内核用 CountryInfo 数据类——桥上只传数据不传类型,fromLocaleOrNull 等伴生函数对应 CountryRegistry.fromCode;
  • 每敲一个字全量走一次 search op:上游有"续打增量过滤"优化(只在上一次结果集里再滤),250 条全表 filter 本身微秒级,未搬;要搬可按 tanstack-table 的 state 回显模式把上次结果集传回引擎;
  • android / ios 目标剥离(上游 Compose 源文件留在 composeMain),发布、detekt、danger CI 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.countrycodepicker 匹配的签名。

8.3 未来优化方向

  • cinterop @ohos.i18n 取系统语言国家显示名,替代内置英文名,恢复上游 displayName() 的本地化语义;
  • 把续打增量过滤搬进桥协议(上次结果集回显),列表规模扩大时省 FFI 往返;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 library(100% Compose + 两个无 ohos 制品的数据依赖)
    → 确认 multiplatform-locale / normalize 无 ohos 制品(路线分岔:内核提取)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(旧线降级)
    → 根构建替换(cmp_qrose 模式)+ 上游 Compose 隔离 composeMain(rename 100%)
    → 国家数据同源导出(multiplatform-locale 0.11.0 JVM 制品,250 行,折叠名预计算)
    → 搜索语义逐条移植(三段式匹配 + 初始定位)
    → ohosArm64 / ohosX64 KLIB + 双 ABI sharedLib
    → C ABI(@CName × 1)+ 行文本平铺载荷
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机单页验收(握手 / 全表 / 三段式搜索 / 初始定位 / 选中回填)

9.2 封装层次

  • 引擎层:CountryRegistry 250 国内置表、searchCountries() 三段式匹配、normalizedName 预计算折叠;
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 符号输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理搜索框状态、行文本解析、列表渲染、选中横幅与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 数据类依赖适配的正解是"同源内置"而不是"手抄一份":250 条国家数据直接从上游所依赖库的同版本 JVM 制品、走它自己的 ICU 路径导出,配合 jvmTest 锁规模与顺序——数据零漂移是可证明的,而手抄永远说不清;
  2. 运行时依赖能预计算就预计算:上游为搜索折跟着一个 UAX #15 库 + memo 缓存,ohos 上两者都不可得,但"折叠名"本质是静态数据——生成期算一次存成列,运行时依赖归零,这比移植一个 normalizer 进 .so 便宜得多;
  3. 工具链配对关系跟着依赖走、且不能跨线套用:本库内核零 kotlinx 依赖,钉 2.2.21 旧线、Gradle 降 8.14.1;KStore 依赖 2.4 元数据,升 2.4.20 线、Gradle 留 9.x——两个方向相反的决定都是对的,依据是各自依赖的制品可达性。

9.4 适配成果

当前适配已完成:

  • compose-country-code-picker 0.5.2 的双 ABI Kotlin/Native 编译(上游 Compose 源文件零改动隔离,git 记录为 rename 相似度 100%);
  • 250 国数据的同源内置与搜索语义逐条移植(+86 / 86 / 名字忽略大小写 / 折叠名四条规则全部对齐上游并有测试锁定);
  • 1 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证双 ABI 全局 T 符号);
  • DevEco Stage ArkUI 单页验收页面(握手、全表、三段式搜索、初始定位、选中回填);
  • 14 项 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

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

更多推荐