开源鸿蒙平台 CMP 三方库「pigment」适配全流程
本文记录
pigment接入 OpenHarmony 的完整过程,覆盖取色器库盘点、Compose 鸿蒙制品线选型、整库真实编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
pigment是 serbelga 开发的 Compose Multiplatform 色板取色器库:ColorPicker.FlowRow(流式多行色板)与ColorPicker.LazyRow(横向懒加载色板)两种布局,ColorPickerItem(color, enabled)色块模型,Small / Medium / Large 三档尺寸,勾选对比色由Color.luminance()实时决定(暗底白勾、亮底黑勾),支持禁用项与Color.Unspecified"非颜色"项。本次适配与社区早期路线(qrose 的"Compose 源码隔离到composeMain不编译")有本质区别:社区 Maven 上已经出现完整的 Compose 鸿蒙制品线(org.jetbrains.compose的 runtime / ui / foundation / material3 / components-resources1.12.0-0.1.0-07,全部带ohosArm64/ohosX64klib),配合鸿蒙 Kotlin2.4.20-0.1.0-07自带的 compose 编译器插件,不需要隔离——上游commonMain的两个文件(690 行的ColorPicker.kt含全部@Composable组件,零改动)被原样编译成 OpenHarmony KLIB,一个带选中指示、禁用语义、未指定颜色项的完整取色器组件库编译进真机.so,ArkUI 页面作为验收宿主,所有尺寸、亮度与对比色数值由真库实时计算。项目地址: AtomGit/oh-tpc/pigment.git
上游仓库: serbelga/pigment
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
pigment 为 Compose Multiplatform 应用提供"从预设色板里挑一个颜色"这一件事的完整答案:ColorPicker.FlowRow 一个调用出流式色板(maxItemsInEachRow / maxLines 控制换行),ColorPicker.LazyRow 出横向滚动色板(带 LazyListState / flingBehavior / reverseLayout 全套懒加载参数),Map 泛型重载把任意业务键映射到色块;选中态由 ColorIndicatorColors.checkColor(color) 按 color.luminance() 决定勾选图标用白还是黑,边框宽度由 ColorIndicatorBorderWidth.borderWidth(color, selected) 按"选中 2dp / 极亮极暗 1dp / 其余无框"三段分支决定;ColorPickerItem.enabled = false 的色块不可点,Color.Unspecified 渲染为"非颜色"重置项。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套色板交互,但上游构建(Kotlin 2.4.10 / Gradle 9.8.0 / AGP 9.4.1)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示色块,却无法证明公共组件库在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证组件模型、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.10 / Gradle 9.8.0 / AGP 9.4.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 读不了 2.4 元数据。 |
| 依赖矩阵 | 组件依赖 compose.ui / compose.foundation / material3(ripple)/ components-resources(矢量图标与字符串),每一个都要有 ohos 变体才能解析。 |
| 代码生成缺失 | 上游源码引用 compose 插件生成的 Res 资源访问器(Res.drawable.ic_check_24px 等),org.jetbrains.compose Gradle 插件不在鸿蒙线上,生成物不存在。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 内部 API 不跨模块 | 尺寸映射(36/48/56dp)、checkColor / borderWidth 分支是上游 internal / private 成员,桥模块读不到。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:组件库整体的 ohos 编译、根构建替换(cmp_qrose 模式)、手写 Res 访问器补位和平面 JSON 桥接层。组件语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和重绘视觉。
与社区已入库的两个取色器并存不冲突:
| compose-pipette | kmp-kolor-picker | pigment | |
|---|---|---|---|
| 取色方式 | 圆盘 / 方形 / 圆环连续 HSV 拖动 | HSV 光谱画布 | 预设色板点选 |
| 输入 | 手势坐标 → HSV | 手势坐标 → HSV | List<ColorPickerItem> / Map<T, ColorPickerItem> |
| 布局 | 单组件 | 单组件 | FlowRow 流式 + LazyRow 懒加载 |
| 特有语义 | thumb 对比色 | 光谱渐变 | 禁用项、非颜色项、三档尺寸、勾选对比色分支 |
1.2 库提供的能力
Demo 提供一个真实的色板选择会话,对应桥接的全部入口:
- 默认值核验(defaults):
ColorPickerDefaults.Shape === CircleShape用对象同一性核验默认形状,colorIndicatorColors()与colorIndicatorColors(White, Black)的数据类相等性核验默认对比色,真机页面渲染"真库校验通过"; - 尺寸模型(sizes):Small / Medium / Large 三档真实枚举项,返回指示器 36 / 48 / 56dp、间距 4dp、内边距 2dp;
- 单色模型(item):一个颜色过真库——构造真实
ColorPickerItem(color, enabled),Color.luminance()/toArgb()实时计算,返回对比色、边框宽度与分支; - 整板模型(items):色板列表 + 选中项 + 禁用索引 + 尺寸一揽子下发,ArkTS 按
items[]重绘整板,选中判定与组合件内colorItem.color == selectedColor同语义; - 版本握手(version):
PigmentVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op | 回显本次操作。 |
luminance / isDark | 真库 Color.luminance() 结果与明暗判定。 |
checkColorHex / checkColorIsOnDark | 勾选图标颜色(< 0.5 取 onDark,否则 onLight)。 |
borderWidthDp / borderBranch | 边框宽度(未指定时为 null)与三段分支名。 |
indicatorSizeDp / itemSpacingDp / innerPaddingDp | 复刻自上游私有常量的布局度量。 |
组件侧完整能力(四个公共组合件、ColorPickerSize、ColorIndicatorColors / ColorIndicatorBorderWidth、ColorPickerDefaults、私有渲染内部件 ColorItem / ColorIndicator / ColorUnspecifiedIndicator)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留编译级交叉验证(上游本无 commonMain 测试,快照测试是 Android 专属 Paparazzi 模块,留盘不进构建),证明 ohos .so 里链接的组件代码与上游零漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | pigment 的全部 commonMain 文件(ColorPicker.kt 690 行 + ColorPickerItem.kt)原样编译,不隔离、不改写;仅新增 1 个 Res 访问器 shim 源文件(全仓唯一新增 Kotlin 源文件)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);build-logic(Spotless 约定插件)、:pigment:samples、catalog 四端 Demo、Paparazzi 快照测试留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 版本主体与上游完全一致,只把仓库换到 ohos 制品线(换线不换版);jvm 交叉验证走上游同款 1.11.1 线。 |
| UI 完整 | 三页签覆盖全部桥接入口:FlowRow 色板、LazyRow 色板、默认值 / 尺寸 / 亮度探针验收页。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 侧暂无社区 Compose 线的组合宿主,组合件编译并链接进
.so但不在真机组合;ArkTS Demo 用真库返回的模型数值重绘视觉——与 qrose / compose-pipette 审核通过的"真库模型 + ArkTS 重绘"同一条路。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 Compose ohos 制品线(material3 / components-resources 可达性)、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、双线依赖、skiko 排除、Res shim 补位
第 3 阶段:桥协议建模 ── 色板载荷、item/items JSON 契约、真库直调与复刻公式的边界、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── FlowRow / LazyRow 色板重绘、尺寸切换、模型验收页、dispatch 日志
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
pigment/ 上游组件库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
pigment/src/commonMain/.../resources/ 新增:Res 访问器 shim(全仓唯一新增源文件)
pigment/samples/ 上游 @sample 文档示例,留盘不进 settings
pigment/screenshot-tests/ 上游 Paparazzi 快照测试(本就未进 settings),留盘
build-logic/ 上游 Spotless 约定插件 composite build,留盘不进构建
catalog/ 上游四端 Demo(android/desktop/shared/web),留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :pigment 与 :example:nativeApp,rootProject.name = "ohos_pigment"。上游的 AGP / Dokka / Compose 插件别名、vanniktech 发布、Spotless 约定全部随根构建替换剥离——本 fork 不发 Maven Central。被替换的上游文件逐一保全为 *.upstream(settings.gradle.kts.upstream、build.gradle.kts.upstream、gradle.properties.upstream、gradle/libs.versions.toml.upstream、pigment/build.gradle.kts.upstream),上游 README 存为 README.upstream.md。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向本机 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 9.6.1(自上游 9.8.0 对齐到社区同款 wrapper;2.4.20 线支持 Gradle 9.x) |
| Compose | ohos 线 1.12.0-0.1.0-07(runtime / ui / foundation / material3 / components-resources);jvm 交叉验证线上游同款 1.11.1 + material3 1.9.0 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.pigment |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
pigment | 2(ColorPicker.kt 690 行 / ColorPickerItem.kt) | compose.ui + foundation(FlowRow/LazyRow)+ material3(ripple)+ components-resources(Res 访问器) | 整体编译(jvm + 双 ohos,源码零改动) |
pigment:samples | 7 | compose foundation/ui + :pigment,文档 @sample 专用 | 留盘不进构建 |
pigment:screenshot-tests | 3 | Paparazzi + TestParameterInjector(Android 专属) | 留盘不进构建(上游本就未 include) |
expect/actual 声明全库为 零—— pigment 没有任何平台源码集,commonMain 就是全部实现。挡在面前的是两件小事:其一,源码引用 compose 插件生成的 Res 资源访问器(Res.drawable.ic_check_24px、Res.string.color_off 等),代码生成不在鸿蒙线上;其二,桥需要的尺寸/对比色/边框分支是上游 internal / private 成员,跨模块不可见。前者用 shim 补位(第 2.4 节),后者按 compose-pipette 审核先例逐字复刻(第 3.2 节)。
1.4 页面边界
标题区 Pigment · OpenHarmony / 版本握手行(0.7.0-ohos.1)
Tab 切换 取色器 / 横向 / 模型
取色器页签 18 色 Material 调色板 + 非颜色项、禁用项淡化、小/中/大尺寸切换、选中信息行
横向页签 31 色 HSL 光谱 List 横滑、首个可见索引读数、非颜色项置首
模型页签 版本握手、默认值核验表(真库校验通过)、三档尺寸表、灰阶亮度探针、dispatch 日志
日志区 最近 Native 调用的原始 JSON 响应留痕
页面启动时先调 pigmentVersion() 做握手,再按当前尺寸 pigmentDispatch({op:'items',...}) 拉整板模型。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 Compose 制品。用 .module 元数据逐个验证:
org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07 ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.ui:ui:1.12.0-0.1.0-07 ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.foundation:foundation:1.12.0-0.1.0-07 ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.material3:material3:1.12.0-0.1.0-07 ohosArm64 / ohosX64 klib ✓
org.jetbrains.compose.components:components-resources:1.12.0-0.1.0-07 ohosArm64 / ohosX64 klib ✓
三个关键结论:其一,material3 与 components-resources 都在 ohos 线上(inklet / compose-font-icons / SpriteView-KMP 先例),pigment 的三件依赖全部可达,不需要 qrose 式的 composeMain 隔离;其二,ohos 线不发布 *-desktop 子制品,jvm() 交叉验证必须从 Maven Central 解析上游同款 1.11.1 线——依赖按目标线声明(“line switch, not a version switch”);其三,鸿蒙 Kotlin 2.4.20-0.1.0-07 自带 org.jetbrains.kotlin.plugin.compose 编译器插件同版本制品,@Composable 变换在 ohos 目标上直接可用。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / kmp-kolor-picker 用的)。但上游 pigment 是 Kotlin 2.4.10 构建的 2.4 代元数据,Kotlin 2.2.21 只能读到 2.3.0,编译期直接报:
Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.4.0, expected version is 2.3.0 or less.
工具链版本跟着上游走:Kotlin 钉 2.4.20-0.1.0-07(KStore / compose-pipette / channel-event-bus 先例)。源码级兼容成本为零;Gradle 对齐到社区同款 9.6.1(2.4.20 线支持 Gradle 9.x),不像 2.2.21 旧线必须降 8.14.1。
settings.gradle.kts 把 eazytec 仓库写在最前:
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_pigment"
include(":pigment")
include(":example:nativeApp")
2.3 编译真实组件库
pigment/build.gradle.kts 收敛为纯 KMP 模块,Compose 依赖按目标线声明:
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.compose.compiler)
}
kotlin {
applyDefaultHierarchyTemplate()
jvm() // 交叉验证用
ohosArm64() // the adaptation
ohosX64()
sourceSets {
ohosMain.dependencies {
api(libs.compose.runtime) // 1.12.0-0.1.0-07
api(libs.compose.ui)
api(libs.compose.foundation)
api(libs.compose.material3)
api("org.jetbrains.compose.components:components-resources:${libs.versions.compose.get()}") {
exclude(group = "org.jetbrains.skiko", module = "skiko-ohosarm64-api")
exclude(group = "org.jetbrains.skiko", module = "skiko-ohosx64-api")
exclude(group = "org.jetbrains.skiko", module = "skiko-ohosarm64-fusionrenderer")
exclude(group = "org.jetbrains.skiko", module = "skiko-ohosx64-fusionrenderer")
}
}
jvmMain.dependencies {
api(libs.compose.runtime.jvm) // 上游同款 1.11.1 线
api(libs.compose.ui.jvm)
api(libs.compose.foundation.jvm)
api(libs.compose.material3.jvm) // material3 独立版本线 1.9.0
api(libs.compose.resources.jvm)
}
}
}
skiko 排除是硬要求:components-resources 会传递 skiko-ohos{arm64,x64}-api 与 -fusionrenderer,而 compose.ui 带完整的 org.jetbrains.skiko:skiko klib——两者都携带 Skia 实现,KLIB 加载器(unique_name 相同)和链接器(Skia 符号重复)先后拒绝。完整 klib 覆盖前者,排除之(SpriteView-KMP / compose-font-icons 同款处置)。
实测:
./gradlew :pigment:compileKotlinJvm # BUILD SUCCESSFUL(上游源码 + 上游 1.11.1 线)
./gradlew :pigment:compileKotlinOhosArm64 # BUILD SUCCESSFUL(上游源码 + ohos 1.12.0-0.1.0-07 线)
./gradlew :pigment:compileKotlinOhosX64 # BUILD SUCCESSFUL
690 行 ColorPicker.kt 一个字符没动,含全部 @Composable、FlowRow / LazyRow、material3 ripple()、vectorResource 调用。
2.4 Res shim 补位(pigment 特有的一步)
上游 ColorPicker.kt 引用代码生成的资源访问器:
import dev.sergiobelda.pigment.resources.Res
import dev.sergiobelda.pigment.resources.color_off
import dev.sergiobelda.pigment.resources.ic_check_24px
import dev.sergiobelda.pigment.resources.ic_format_color_reset_24px
import dev.sergiobelda.pigment.resources.selected
这些访问器由 org.jetbrains.compose Gradle 插件按 compose.resources { packageOfResClass = "dev.sergiobelda.pigment.resources" } 生成——插件不在鸿蒙线上。处置是手写一个与生成物同包名、同名字、同接收者形状的 shim(全仓唯一新增源文件 pigment/src/commonMain/kotlin/dev/sergiobelda/pigment/resources/Res.kt):
@file:OptIn(InternalResourceApi::class)
package dev.sergiobelda.pigment.resources
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.InternalResourceApi
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.StringResource
public object Res {
public val drawable: DrawableRes = DrawableRes
public val string: StringRes = StringRes
}
public object DrawableRes
public object StringRes
public val DrawableRes.ic_check_24px: DrawableResource
get() = DrawableResource(
"drawable/ic_check_24px",
setOf(ResourceItem(setOf(), "drawable/ic_check_24px.xml", -1L, -1L)),
)
public val StringRes.color_off: StringResource
get() = StringResource(
"string/color_off",
"color_off",
setOf(ResourceItem(setOf(), "values/strings.xml", -1L, -1L)),
)
// ic_format_color_reset_24px / selected 同构
三个实现细节:其一,构造器全部显式传参(DrawableResource(id, items) 与 StringResource(id, key, items) 的 items 无默认值,ResourceItem 四参 (qualifiers, path, offset, size) 逐个写全);其二,构造器挂在 InternalResourceApi opt-in 后面——插件自己的生成物走的就是这道门,shim 显式 opt-in 即可;其三,物理资产(composeResources/drawable/ 下的矢量 XML 与 values/strings.xml)原样留在树里供上游构建使用,ohos 侧组合件只编译不组合,访问器仅需要编译期可解析。上游源码因此零改动——这与 compose-font-icons 的 headless facade 是同一个先例:适配面落在新增文件,而不是修改上游。
第 3 阶段:桥协议建模
3.1 JSON 契约
dev.sergiobelda.pigment.colorpicker/**(上游组件库,原样编译)
↓ ColorPickerItem / ColorPickerSize / ColorPickerDefaults / Color 数学
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API pigmentVersion() / pigmentDispatch()
↓
ArkUI Index.ets(三页签)
请求与响应:
请求 : {"op":"version"}
{"op":"defaults"}
{"op":"sizes"}
{"op":"item","hex":"#607D8B","selected":"true","enabled":"true"}
{"op":"items","colors":"#F44336,#E91E63,...,unspecified","selected":"E91E63",
"size":"Medium","disabled":"4,9,17"}
响应 : {"ok":true,"op":"item","item":{...单色模型...}}
{"ok":true,"op":"items","size":"Medium","indicatorSizeDp":48.0,"items":[...]}
{"ok":false,"op":"...","error":"..."}
两个设计:colors 用逗号分隔的 hex 列表(扁平 JSON 解析器不递归数组),字面量 unspecified 或空项映射到 Color.Unspecified 非颜色项——与上游 ColorPickerItem(Color.Unspecified) 同语义;selected 归一化为 6 位 RGB 比对(UNSPECIFIED 字面量选中非颜色项),与组合件内 colorItem.color == selectedColor 的选中判定对齐。布尔值统一走 "true" / "false" 字符串。
3.2 真库直调与复刻公式的边界
桥内每一个色值都过真库类型:
val colorItem = ColorPickerItem(color = color, enabled = enabled) // 真实数据类实例
val luminance = colorItem.color.luminance() // 真实 compose ui 数学
val argb = colorItem.color.toArgb()
val shapeIsCircle = ColorPickerDefaults.Shape === CircleShape // 对象同一性
val defaultsVerified = ColorPickerDefaults.colorIndicatorColors() ==
ColorPickerDefaults.colorIndicatorColors(
onDarkColor = Color.White, onLightColor = Color.Black) // 数据类相等性
val size = ColorPickerSize.valueOf(sizeName) // 真实枚举项
上游 internal / private 的四组公式在桥内逐字复刻(compose-pipette 审核先例,复刻处逐行注明出处):
| 复刻公式 | 上游出处 | 内容 |
|---|---|---|
indicatorSizeDp / itemSpacingDp / innerPaddingDp | ColorPickerSize.indicatorSize()/itemSpacing() + 私有常量 | Small 36 / Medium 48 / Large 56;间距 4;内边距 2 |
checkColor | ColorIndicatorColors.checkColor | luminance() < 0.5 → onDark(#FFFFFF)否则 onLight(#000000) |
borderWidth | ColorIndicatorBorderWidth.borderWidth | 选中 → 2dp;luminance() !in 0.1..0.9 → 1dp(neutral);其余 Dp.Unspecified(响应里 null) |
| 非颜色项常量 | ColorUnspecifiedIndicator 私有色值 | 白底、黑图标(选中时 0.2 alpha)、黑勾 |
亮度数值本身永远来自真库 Color.luminance()——复刻的只是分支形状,不是数学。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("PigmentDispatch")
fun pigmentDispatch(request: String): String = try {
val json = MiniJson.parse(request)
when (json.str("op")) {
"version" -> versionResponse()
"defaults" -> defaultsResponse()
"sizes" -> sizesResponse()
"item" -> itemResponse(json)
"items" -> itemsResponse(json)
else -> err(json.str("op").ifBlank { "(missing)" }, "unsupported op")
}
} catch (t: Throwable) {
err("dispatch", t.message ?: t.toString())
}
非法 hex、缺 op、不支持的 op、坏尺寸名,全部走同一个 ok:false 通道,ArkTS 侧统一渲染错误横幅。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libpigment_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohospigment.so)
│ PigmentDispatch / PigmentVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制组件语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("PigmentVersion")
fun pigmentVersion(): String = "0.7.0-ohos.1" // 版本握手
@CName("PigmentDispatch")
fun pigmentDispatch(request: String): String // 五个 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
PigmentDispatch | pigmentDispatch(req) | ColorPickerItem 构造 + luminance + Defaults 核验 + 尺寸/分支复刻 |
PigmentVersion | pigmentVersion() | fork 版本握手(真机版本行的数据来源) |
桥模块不 apply compose 编译器插件——它只触碰库的非组合面(数据类、枚举、Color 数学),没有插件变换的 lambda 需要链接(compose-pipette 同款纪律)。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"pigmentDispatch", nullptr, NapiPigmentDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"pigmentVersion", nullptr, NapiPigmentVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohospigment_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohospigment_api.h 直接取 Kotlin/Native 生成的 C Adapter 头(含 ExportedSymbols 结构与两个业务符号),不手写缩减版。
4.4 CMake 和类型声明
add_library(ohospigment SHARED IMPORTED)
set_target_properties(ohospigment PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(pigment_napi SHARED napi_init.cpp)
target_link_libraries(pigment_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohospigment)
OHOS_ARCH 为空或 libohospigment.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const pigmentDispatch: (request: string) => string;
export const pigmentVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 dispatch 模式
import { pigmentDispatch, pigmentVersion } from 'libpigment_napi.so';
private dispatch(req: DispatchRequest): PigmentResponse {
const raw: string = pigmentDispatch(JSON.stringify(req));
return JSON.parse(raw) as PigmentResponse; // typed interface,禁 any
}
取色器页签在尺寸切换、选中项变化时重新 {"op":"items",...} 拉整板;每格的勾选颜色、边框宽度、分支名直接取 items[i].checkColorHex / borderWidthDp / borderBranch,ArkTS 不自行计算任何颜色数学。
5.2 页面交互
取色器页签:18 色 Material 500 调色板加末位非颜色项(斜杠圆圈图标),第 4 / 9 / 17 项禁用(淡化且不可点),小 / 中 / 大三枚尺寸片(36 / 48 / 56dp,点击重新 dispatch),选中项按 checkColorHex 画白勾或黑勾、按 borderWidthDp 画边框,borderBranch = neutral 的极亮极暗项常显 1dp 中性边框,并复刻上游"左半透明灰 #BDBDBD"细节;底部信息行显示"已选 hex · 亮度 · 对比色 · 边框分支"。横向页签:31 色 HSL 光谱(ArkTS 侧 hslToHex 生成色号,颜色数学仍在真库)横向 List 滚动,非颜色项置首,onScrollIndex 回读首个可见索引。模型页签:版本握手行、默认值核验表(shapeVerified / defaultsVerified 渲染为"真库校验通过 ✓")、三档尺寸表、12 级灰阶亮度探针——亮度跨 0.5 时对比色白黑翻转、跨出 0.1–0.9 时边框分支在 neutral / unselected 间切换,肉眼可见;底部 dispatch 日志留痕最近 Native 调用的原始 JSON。
5.3 对比度和错误边界
色块本身即色彩主体,文字一律深色底/浅色底双套;ok:false 时错误原文进横幅与日志,Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohospigment)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.pigment、versionName 0.7.0-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/libohospigment_api.h
├── src/main/cpp/types/libpigment_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 三页签验收页面
6.2 构建、签名和安装
./gradlew :pigment:compileKotlinJvm # 交叉验证(上游 1.11.1 线)
./gradlew :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 保持空数组)。
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.pigment -m entry
四、完整代码对照
4.1 整体架构
dev.sergiobelda.pigment.colorpicker/**(上游组件库原样编译)
│ ColorPicker × 4 / ColorPickerItem / ColorPickerSize
│ ColorIndicatorColors / ColorIndicatorBorderWidth / ColorPickerDefaults
dev.sergiobelda.pigment.resources.Res(手写 shim,唯一新增源文件)
│ Res.drawable.ic_check_24px / Res.string.color_off …
Bridge.kt + MiniJson
│ PigmentDispatch / PigmentVersion
libpigment_napi.so
│ pigmentDispatch / pigmentVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(三页签)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_pigment、上游模块移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + ohos Compose 线 1.12.0-0.1.0-07 + jvm 线 1.11.1/material3 1.9.0 双线钉版 |
build.gradle.kts(根) | 版本 0.7.0-ohos.1、全局禁用 Ohos*Test* 任务 |
pigment/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,依赖按目标线声明,skiko 排除 |
pigment/src/commonMain/.../resources/Res.kt | 唯一新增源文件(Res 访问器 shim,InternalResourceApi opt-in) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 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 | 鸿蒙适配主文档(20 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 组件 | ColorPicker.FlowRow(colors, selectedColor, …) | 流式色板组合件(maxItemsInEachRow / maxLines) |
| 组件 | ColorPicker.LazyRow(colors, selectedColor, …) | 横向懒加载色板(LazyListState / flingBehavior) |
| 模型 | ColorPickerItem(color, enabled) | 色块模型(桥内真实构造) |
| 模型 | ColorPickerSize.Small/Medium/Large | 三档尺寸(36/48/56dp,桥复刻映射) |
| 模型 | ColorPickerDefaults | 默认形状 / 尺寸 / 对比色 / 边框宽度(桥内同一性与相等性核验) |
| 数学 | Color.luminance() / Color.toArgb() | compose ui 实时计算(桥内真实调用) |
| Native | PigmentDispatch / PigmentVersion | C ABI JSON 导出 |
| N-API | pigmentDispatch(req) / pigmentVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(op) | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(pigmentDispatch(JSON.stringify({
op: 'items',
colors: ['#F44336', '#E91E63', '#607D8B', 'unspecified'].join(','),
selected: '607D8B',
size: 'Medium',
disabled: '2'
}))) as ItemsResponse;
// res.items[2].checkColorHex === '#FFFFFF'(亮度 0.19 < 0.5,onDark)
// res.items[2].borderBranch === 'selected'(2dp 边框)
val colorItem = ColorPickerItem(Color(0xFF607D8B), enabled = true)
val luminance = colorItem.color.luminance() // 0.190…
val checkColor = if (luminance < 0.5f) Color.White else Color.Black
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:整库编译而不是 composeMain 隔离
qrose 适配时社区 Compose 线尚不存在,Painter 只能隔离到 src/composeMain 不编译、真机走 KN 光栅化。本次先验证制品线(runtime / ui / foundation / material3 / components-resources 的 ohos klib 全部存在),障碍已消失——再隔离等于把组件库雪藏,反而丢掉"上游原件进真机 .so"的证明力。真编译让四个公共组合件、选中分支、禁用语义、非颜色项全部是上游原件。
决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 对齐 9.6.1
上游本来就是 Kotlin 2.4.10,旧钉法 2.2.21 读不了 2.4 元数据;2.4.20 与之同代零降级。Gradle 方面 2.4.20 线支持 9.x,wrapper 对齐到社区同款 9.6.1(compose-pipette / inklet 同款),只有 2.2.21 旧线才必须降 8.14.1,两条工具链线的经验不能互相套用。
决策 3:Res shim 而不是改上游源码
代码生成不可用时有三条路:改上游源码删掉 Res 引用(引入漂移,违背零改动原则)、把资源引用挪进 expect/actual(改动面更大)、手写与生成物同形状的访问器 shim(只增不改)。选第三条——全仓唯一新增源文件落在 resources/ 包,上游 colorpicker/ 目录一字不动,上游构建(插件在场时生成物与 shim 同包同名会冲突的场景)因 build-logic 不进构建而不受影响。
决策 4:双线依赖(ohos 线 / jvm 交叉验证线)
ohos 线不发布 *-desktop 子制品,jvm 目标从同一条线解析必然失败;jvm 线(Maven Central 1.11.1 + material3 1.9.0)没有 ohos klib。处置是依赖声明在 ohosMain / jvmMain 各自的 source set 里(commonMain 保持线无关)——“line switch, not a version switch”,交叉验证编译的正是上游当年构建用的同款制品体。
决策 5:真库模型 + 复刻公式的边界
桥能拿到的是公共面:ColorPickerItem 构造、Color.luminance()、ColorPickerDefaults 的对象同一性与数据类相等性、ColorPickerSize 枚举。尺寸映射与三段分支是 internal / private,跨模块不可见——按 compose-pipette 审核先例逐字复刻并逐行注明出处,亮度数值本身永远来自真库。复刻的是分支形状,不是数学。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21、Gradle 9.6.1、DevEco Studio 5.1.0 Release(product 5.1.0(18))、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :pigment:compileKotlinJvm
上游无 commonMain 测试(快照测试是 Paparazzi / Android 专属模块,本就未进上游 settings),交叉验证为编译级:上游两个源文件在 Maven Central 的上游同款线(compose 1.11.1 + material3 1.9.0)上原样编译通过——同一份源码,两条制品线(1.11.1 与 1.12.0-0.1.0-07)都能吃下,是"源码零漂移"的直接证据。ohos 测试任务由根构建全局禁用(Ohos*Test* → enabled = false),ohos 侧只承担编译与链接。
6.3 HAP 验证
:pigment:compileKotlinJvm: BUILD SUCCESSFUL
:pigment:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(3m36s)
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/{arm64-v8a,x86_64}
llvm-nm -D libohospigment.so: PigmentDispatch / PigmentVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libpigment_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully
KN so arm64 约 27.4 MB(28,728,984 字节,debugShared 未 strip,含 Kotlin/Native 运行时与 Compose klib)、x86_64 约 26.2 MB;libc++_shared.so arm64 / x86_64 分别约 1.8 MB / 1.7 MB,与 NAPI so 一起构成每个 ABI 的三件套。
6.4 功能用例
- 启动后版本握手行显示
0.7.0-ohos.1(来自pigmentVersion()真实 Native 调用,非静态文字); - 取色器页签渲染 18 色 Material 调色板加非颜色项,第 4 / 9 / 17 项淡化禁用不可点(真机截图见 7.1);
- 点选深色蓝灰
#607D8B(亮度约 0.19 < 0.5):勾选图标取 onDark 白色、边框分支selected(2dp)——对比色分支随亮度实时翻转; - 点选极亮 / 极暗色块:未选中态显示 1dp 中性边框(
neutral分支);中间亮度色块无边框(unselected,Dp.Unspecified); - 小 / 中 / 大尺寸片切换:色块直径随
indicatorSizeDp36 / 48 / 56 变化,间距与内边距同步按桥返回值调整; - 非颜色项选中:白底 + 斜杠圆圈图标 + 黑勾(
iconAlpha 0.2); - 横向页签 31 色光谱滚动选择,首个可见索引随滚动回读;
- 模型页签默认值核验表显示"真库校验通过 ✓"(
shapeVerified/defaultsVerified均为真); - 灰阶探针拖到纯黑 / 纯白端:边框分支跳回
neutral;跨 0.5 亮度:对比色白黑互换; - 每步操作在 dispatch 日志留痕原始 JSON 响应。
6.5 验证结论
组件库、Compose 运行时、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,取色器页签展示了真实的色板选择会话:版本握手、选中对比色分支、三段边框分支、尺寸模型与禁用 / 非颜色语义(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/pigment.git
cd pigment
export JAVA_HOME="/path/to/jdk-21"
./gradlew :pigment:compileKotlinJvm # 交叉验证(上游 1.11.1 线)
./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.pigment -m entry
hdc shell hilog | grep Pigment
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:pigment真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin KDoc 注释里写
composeResources/drawable/*.xml这种通配路径会踩 Kotlin 块注释嵌套规则——*.xml里的/*开启了一层未闭合的嵌套注释,报"Unclosed comment"且位置指向文件尾,写文档注释时通配符要拆开表述; - compose-resources 的
DrawableResource(id, items)/StringResource(id, key, items)构造器items无默认值,ResourceItem(qualifiers, path, offset, size)四参——手写 shim 时先用 javap 对着 jar 确认签名,不要凭记忆省参数; ResourceItem构造器挂在InternalResourceApiopt-in 后面(报"This is internal API of the Compose gradle plugin"),shim 加@file:OptIn(InternalResourceApi::class)即可,这与插件生成物走的是同一道门;- ohos 线不发布
*-desktop子制品:jvm 交叉验证目标必须单独走 Maven Central 上游线(1.11.1 + material3 1.9.0),依赖声明在各自 source set,commonMain 保持线无关; components-resources传递的skiko-ohos{arm64,x64}-api/-fusionrenderer与compose.ui带的完整 skiko klib 重复(KLIB unique_name 冲突 + 链接期 Skia 符号重复),exclude 前者;- Gradle wrapper 从上游 9.8.0 对齐到 9.6.1:鸿蒙 Kotlin 2.4.20 线的社区验证矩阵在 9.6.x,不赌 9.8;
- 根构建
.gitignore的/build是根锚定模式,盖不住example/nativeApp/build/与example/harmonyApp/entry/libs/,要单独追加,否则 so 与中间产物混进提交; - ArkTS 只能
import 'libpigment_napi.so',绝不能 importlibohospigment.so——KN CAdapter 不把@CName业务符号导出给 ArkTS; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();签名材料由 DevEco 自动写入build-profile.json5,提交前保护,证书绝不进入 AtomGit。
8.2 已知问题
- OHOS 侧暂无社区 Compose 线的组合宿主:组合件编译并链接进
.so但不在真机组合,视觉由 ArkUI 按真库返回值重绘(qrose / compose-pipette 审核路线的既定边界); - 桥内复刻的四组公式(尺寸映射、checkColor / borderWidth 分支、非颜色项常量)必须与上游同步——上游改这些
internal/private成员时桥要跟着改(README.OpenHarmony_CN.md 已列入维护清单); - compose-resources 物理资产(矢量 XML / strings)不进 ohos 包,
Res访问器仅编译期可解析,vectorResource/stringResource不在真机执行路径上; - 上游无
commonMain测试,JVM 交叉验证是编译级而非用例级(Paparazzi 快照测试是 Android 专属,留盘不进构建); - android / ios / js 目标与 build-logic / samples / catalog 留盘不进构建,发布、dokka、Spotless 一并剥离,本 fork 不发 Maven Central;
- 非颜色项响应里
iconColorHex为 RGB(alpha 由iconAlpha字段单独承载),ArkTS 侧要组合使用。
8.3 未来优化方向
- 等社区 compose.ui ohos 线出现组合宿主(
ComposeArkUIViewController)后,把 ArkUI 重绘换回真组合件直接渲染ColorPicker.FlowRow/LazyRow——终态是"整库编译"升级为"整库运行",届时桥退化为模型导出; - 社区 compose Gradle 插件上线 ohos 线后,用真代码生成替换手写
Resshim; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 pigment commonMain(2 个文件,690 行 ColorPicker.kt 含全部组合件)
→ 验证 ohos Compose 制品线(runtime / ui / foundation / material3 / components-resources)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
→ 根构建替换(cmp_qrose 模式,*.upstream 保全)
→ 双线依赖 + skiko 排除 + Res shim(唯一新增源文件)
→ ohosArm64 / ohosX64 KLIB,源码零改动
→ C ABI(@CName × 2)+ 真库模型直调 + 四组复刻公式
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机三页签验收(色板选择 / 光谱横滑 / 模型核验)
9.2 封装层次
- 组件层:上游原版四个
ColorPicker组合件、ColorPickerItem/ColorPickerSize模型、ColorPickerDefaults默认值; - 模型层:真库
Color.luminance()/toArgb()实时计算 + 复刻的尺寸与分支公式; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、JSON 解析、色板重绘、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:Compose ohos 线补齐 material3 / components-resources 后,UI 组件库的正确答案就是"整库真编译 + 一个资源 shim"——pigment 全库零 expect/actual、零平台源码集,适配面只剩代码生成缺口一处;路线选择必须基于当天的制品可达性,而不是 qrose 时代"Compose 上不了鸿蒙"的历史结论;
- 工具链版本跟着元数据走:上游 Kotlin 2.4.10 决定了必须用 2.4.20-0.1.0-07 新线,Gradle 矩阵按线区分(2.4.20 线 9.6.1,2.2.21 线 8.14.1),不能混用两条线的经验;
- "真库 / 复刻"的边界要写进文档:桥内每一个数值要么来自真库调用(亮度、默认值核验、数据类构造),要么逐行注明复刻出处(尺寸、分支)——审核时这张对照表就是漂移风险的验收面;UI 组件库的 Demo 验收重点落在"视觉随真库数值变化"而不是像素级复刻。
9.4 适配成果
当前适配已完成:
- pigment
0.7.0(Apache-2.0)真实组件库的双 ABI Kotlin/Native 编译(上游commonMain文件零改动,仅 1 个Resshim 新增源文件); - HarmonyOS Compose 制品线在取色器库上的完整落地(runtime / ui / foundation / material3
1.12.0-0.1.0-07+ components-resources 带 skiko 排除); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 三页签验收页面(色板选择、光谱横滑、默认值核验 / 亮度探针、dispatch 日志);
- 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)