开源鸿蒙平台 CMP 三方库「compose-country-code-picker」适配全流程
本文记录
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 同款):把值得保留的语义(国家表 + 三段式搜索规则 + 初始定位)移植为纯 KotlincommonMain编成.so,上游 Compose 源码一字不改隔离进src/composeMain,ArkUI 页面作为验收宿主。其中最关键的一步是国家数据的同源内置:250 条数据直接从上游所依赖的multiplatform-locale:0.11.0JVM 制品导出,与上游语义零漂移。
项目地址: 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 回显 / query | search 回显本次搜索词;count 报告命中条数。 |
rows | 国家行数据(\n 行分隔、` |
index / total | initial 返回初始下标与全表规模。 |
引擎侧完整能力(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 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 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 product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.countrycodepicker |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 源文件 | 行数 | 依赖形态 | 处置 |
|---|---|---|---|
CountryCodePicker.kt | 334 | 100% Compose Material 3(含私有 defaultSearch lambda) | 拆出搜索逻辑进 commonMain,其余整体隔离进 composeMain |
SearchIcon.kt | 64 | ImageVector.Builder 手绘 search 图标(androidx.compose.ui.graphics) | 整体隔离进 composeMain |
上游外部依赖的处置是这个适配的核心决策点:
| 依赖 | 上游角色 | ohos 制品 | 处置 |
|---|---|---|---|
com.vanniktech:multiplatform-locale:0.11.0 | Country 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 方法 | 覆盖内核 |
|---|---|---|
CountryCodePickerDispatch | countryCodePickerDispatch(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.properties | JDK 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.kt | searchCountries():上游 defaultSearch 逐条移植 |
library/src/jvmTest/.../CountryCodePickerSearchTest.kt | 14 项交叉验证 |
library/src/composeMain/... | 上游 CountryCodePicker.kt + SearchIcon.kt(零改动,rename 相似度 100%) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、MiniJson、行文本编码、错误通道 |
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 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(18 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | CountryRegistry.all | 250 国内置表,顺序同上游 Country.entries |
| 引擎 | CountryRegistry.fromCode(code) | 对应上游 Country.Companion.fromOrNull |
| 引擎 | CountryRegistry.indexOf(code) | 初始滚动下标(null→US、未知→0,上游 coerceAtLeast 语义) |
| 引擎 | searchCountries(term, countries) | 上游 defaultSearch 三段式匹配(区号带 + / 纯数字 / 名字 + 折叠名) |
| 数据 | CountryInfo.normalizedName | 上游运行时 NFD 折叠的预计算形态 |
| Native | CountryCodePickerDispatch | C ABI JSON 导出(唯一符号) |
| N-API | countryCodePickerDispatch(req) | 向 ArkTS 暴露能力 |
| ArkTS | search(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 相同的源上):
| 用例 | 钉住什么 |
|---|---|
registryHasTheFullTable | 250 条、code 无重复、AF 开头(与上游 enum 同序) |
lookupHelpersBehaveLikeUpstreamCompanion | fromCode 大小写、未知码 null |
initialIndexDefaultsToUsaAndCoercesUnknownCodes | null→US 下标、未知码→0(上游 coerceAtLeast 语义) |
blankTermReturnsEveryCountry | 空白 → 全表 |
plusPrefixMatchesCallingCodeWithPlus | +86 → 只有 CN |
bareDigitsMatchCallingCodeWithoutPlus | 86 → 只有 CN |
digitOneMatchesTheWholeNanpBloc | 1 → US、CA(整个北美区号段) |
nameSearchIsCaseInsensitive | CHINA → CN、HK、MO(CLDR contains 语义,与上游英文环境一致) |
nameSearchMatchesThroughStrippedAccents | cote → CI(折叠名命中) |
accentedTermStillMatchesTheRawDisplayName | Cô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 功能用例
- 启动后页头出现
0.5.2-ohos.1 · 250 countries from the Kotlin engine(来自infoop 的真实 Native 调用,非静态文字); - 全表拉取后列表渲染 250 国,首屏定位到
initialop 返回的美国下标(上游Country.USA默认语义); - 搜索框输入
86,列表收敛到唯一一条🇨🇳 China (+86),计数行变1 / 250 countries——纯数字按区号前缀匹配; - 输入
+86结果同样只有中国——+前缀按完整区号匹配; - 输入
1返回整个北美区号段(US、CA 等多条); - 输入
china/CHINA忽略大小写命中,且Hong Kong SAR China、Macao SAR China一并命中(与上游 contains 语义一致); - 输入
cote命中Côte d'Ivoire——预计算折叠名在起作用; - 清空搜索框恢复 250 条;
- 点选国家后顶部横幅回填「国旗 + 国名 (区号) + 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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:library真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Gradle 9.8.1 的上游 wrapper 与 Kotlin 2.2.21 线不兼容,必须降 8.14.1——工具链线与 Gradle 版本的配对关系不能跨线套用(2.4.20 线可以留 9.x);
- Windows 上导出数据时
System.out重定向会把 emoji/重音写成?(US-ASCII 编码器),必须OutputStreamWriter(UTF_8)直写文件并用 hexdump 验证字节(f0 9f 87 a8); - Java
IntStream.map只接受 int→int,返回 String 的 lambda 编译即错,改codePoints().toArray()加循环; - CLDR 英文国家名含逗号(“Bolivia, Plurinational State of”),行文本编码里 callingCodes 必须是行尾列,前 4 列切开后 remainder 整体归区号——分隔符冲突在结构上不可能发生;
- ArkTS 严格模式拒绝
JSON.parse派生的嵌套对象进类型化上下文,250 国数据用\n/|行文本平铺传递; - ohos 无测试 runner:根构建全局禁用
Ohos*Test*任务,交叉验证统一走jvmTest; - Git Bash 直接
./gradlew.bat报JAVA_HOME is not set——wrapper 启动阶段就需要 JVM,org.gradle.java.home救不了,先export JAVA_HOME=...; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so / KN so / libc++)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 签名材料由 DevEco 自动写入
build-profile.json5,提交前用git update-index --skip-worktree保护,证书绝不进入 AtomGit。
8.2 已知问题
- 内置表英文名是 CLDR English(与上游 JVM/Android 平台英文环境一致),不随系统语言本地化——上游靠平台 ICU actual,ohos 侧待补
@ohos.i18nactual; - 上游的槽位定制 API(
countryItemLeadingContent/countryItemHeadlineContent/ListItemColors)不在桥上:那是 Compose 组合层定制点,ohos 侧由 ArkTS 开发者直接改 ArkUI 布局,内核不感知; - 上游
Country是 250 值 enum,本内核用CountryInfo数据类——桥上只传数据不传类型,fromLocaleOrNull等伴生函数对应CountryRegistry.fromCode; - 每敲一个字全量走一次
searchop:上游有"续打增量过滤"优化(只在上一次结果集里再滤),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 封装层次
- 引擎层:
CountryRegistry250 国内置表、searchCountries()三段式匹配、normalizedName预计算折叠; - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 符号输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理搜索框状态、行文本解析、列表渲染、选中横幅与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 数据类依赖适配的正解是"同源内置"而不是"手抄一份":250 条国家数据直接从上游所依赖库的同版本 JVM 制品、走它自己的 ICU 路径导出,配合
jvmTest锁规模与顺序——数据零漂移是可证明的,而手抄永远说不清; - 运行时依赖能预计算就预计算:上游为搜索折跟着一个 UAX #15 库 + memo 缓存,ohos 上两者都不可得,但"折叠名"本质是静态数据——生成期算一次存成列,运行时依赖归零,这比移植一个 normalizer 进
.so便宜得多; - 工具链配对关系跟着依赖走、且不能跨线套用:本库内核零 kotlinx 依赖,钉 2.2.21 旧线、Gradle 降 8.14.1;KStore 依赖 2.4 元数据,升 2.4.20 线、Gradle 留 9.x——两个方向相反的决定都是对的,依据是各自依赖的制品可达性。
9.4 适配成果
当前适配已完成:
compose-country-code-picker0.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。
参考文档
更多推荐


所有评论(0)