Vico 三方库鸿蒙适配 KMP_CMP 适配从 0 到 1 实战
本文记录 Vico 图表库接入 OpenHarmony 的完整过程,覆盖工程盘点、
ohosArm64示例架构、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI Canvas 展示、HAP 签名和真机验收。与“把图表数据重写一份 ArkTS”不同,本次适配复用 Kotlin 侧的图表模型和验收逻辑,让数据经过真实的 Kotlin/Native 产物传给 ArkUI。这样可以验证共享代码确实在 OpenHarmony 设备上运行,而不是只验证一套页面副本。
本项目的核心是 KMP/CMP 图表库,示例 UI 使用 ArkTS Stage 页面和 ArkUI Canvas;工程分层与原生桥接保持清晰,页面采用独立的图表仪表盘布局,展示折线、柱状、组合、饼图、环形和蜡烛六种场景。
项目地址: AtomGit/oh-tpc/ohos_vico
开发工具: DevEco Studio
一、背景
1.1 为什么 KMP/CMP 项目需要 OpenHarmony 目标
Vico 是一个面向 Compose Multiplatform 的多平台图表库。它的核心价值不在
某一个页面,而在于图表模型、数据范围、坐标轴、图层和交互状态可以在共享
代码中组织,然后交给不同平台的渲染层显示。
OpenHarmony 应用不能直接把 JVM 或 Android Compose 产物安装到 ARM64 设备。
如果只把页面重新写成 ArkTS,虽然可以画出几条线,却无法验证 Kotlin 共享
模型是否真的在鸿蒙设备上运行,也容易在数据范围、刷新和格式化上出现第二套
实现。适配需要解决下面几个问题:
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 示例工程默认没有 ohosArm64(),无法生成 OpenHarmony KLIB 和 ARM64 动态库。 |
| 工具链不一致 | Kotlin/Native、OpenHarmony LLVM、Native SDK 和 Gradle 插件必须使用匹配版本。 |
| 渲染层不同 | Compose 图表层不能直接作为 ArkUI Stage 页面使用,需要一个 OpenHarmony 宿主渲染层。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,必须经过 C ABI、C++ N-API 和 JSON。 |
| 消费验证不足 | 根工程编译通过,不代表独立的 example 工程能链接动态库和打包 HAP。 |
| 交付链路复杂 | 原生库、CMake、HAP、签名、设备安装和 ARM64 ELF 依赖都需要单独检查。 |
因此,本次实现把适配边界放在三个地方:Kotlin/Native 目标配置、原生桥接
层和 ArkUI 展示层。图表数据和验收逻辑仍由 Kotlin 维护,ArkTS 只负责页面
状态、Canvas 绘制和用户操作。
1.2 库提供的能力
Vico 的公共能力包括图表模型、数据范围、坐标轴、图层、标记、格式化和交互
状态。本次示例没有把这些能力重新实现成一套 ArkTS 图表库,而是选择最容易在
设备上验证、又能体现渲染差异的六类场景:
- 折线图:七个数据点、连线、重点点和水平坐标轴。
- 柱状图:按数值范围绘制柱子,突出最高值,并展示目标线概念。
- 组合图:同一张图中同时绘制柱状序列和折线序列。
- 饼图:渠道占比、切片标签和图例,数据总和必须为 100%。
- 环形图:平台占比、中心空洞、百分比标签和图例。
- 蜡烛图:开盘、收盘、最高、最低和涨跌颜色。
页面另外提供刷新和缩放。刷新不是在 ArkTS 中随机生成数据,而是把刷新编号
传给 Kotlin/Native,由共享模型根据编号产生新的确定性数据。缩放则修改
ArkUI 状态并重新调用 Canvas 绘制逻辑。
1.3 实现目标
| 维度 | 要求 |
|---|---|
| 代码复用 | 图表定义、数值范围、百分比校验和刷新逻辑由 Kotlin 共享。 |
| 平台目标 | 为示例模块增加 ohosArm64,生成 libvico_sample.so。 |
| 桥接稳定 | 使用少量 C ABI 函数和 JSON,避免把 Kotlin 对象直接暴露给 ArkTS。 |
| UI 完整 | 页面要能切换六种图表、刷新数据、调整缩放并显示能力标签。 |
| 可测试 | JVM 测试、Native 链接、ArkUI/HAP 构建和真机交互分别验收。 |
| 签名安全 | 仓库只保留未签名工程,证书、profile 和密码由开发者手动配置。 |
| 仓库规范 | README、文章、效果图和项目地址统一使用 AtomGit。 |
说明: 本次交付提供源码适配和独立 OpenHarmony 示例,不新增一个替代
Vico 公共 API 的 ArkTS 图表库。这样可以保持上游 API 和平台实现边界清晰。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 Vico 模块、公共能力和示例边界
第 2 阶段:目标与依赖打通 ── 加入 ohosArm64、仓库和独立消费工程
第 3 阶段:数据与序列化 ── 建立图表模型、JSON 契约和共享验收
第 4 阶段:原生桥接 ── Kotlin/Native C ABI、C++ N-API 和内存释放
第 5 阶段:能力封装 ── ArkUI Canvas、六种图表、刷新和缩放
第 6 阶段:示例与验证 ── HAP 构建、签名、设备安装和真机验收
每个阶段都使用真实产物作为下一阶段输入:shared 先验证模型,nativeApp
再把模型链接为 ARM64 动态库,ohosApp 通过 CMake 和 N-API 加载动态库,
最后由 DevEco 负责 HAP 打包和签名。
三、逐步实现过程
第 1 阶段:项目初始化
1.1 盘点原库源码和公共 API
根工程保留 Vico 原有的多平台模块和 Compose 代码。OpenHarmony 示例放在
example/ 下,避免把 DevEco 工具链、ArkTS 文件和证书配置混入库模块:
vico/ 核心图表库模块
vico/compose/ Compose Multiplatform 图表实现
example/shared/ OpenHarmony 示例共享模型和 JVM 测试
example/nativeApp/ ohosArm64 Kotlin/Native 动态库
example/ohosApp/ DevEco Stage 应用和 ArkUI 页面
scripts/ 构建、签名工程和 HAP 辅助脚本
docs/openharmony/ 验收记录和效果图
example 是一个独立的 Gradle 工程。它不会把 DevEco 工程当成 Kotlin 子模块,
因此可以分别执行 Gradle 和 Hvigor,也可以将 ohosApp 复制到另一个目录完成
手动签名。
1.2 固定工具链和版本矩阵
本次示例使用 Kotlin Multiplatform 2.2.21-1.0.0、Gradle Wrapper、JDK 21、
OpenHarmony API 20 ARM64 Native SDK 和 arm64-v8a HAP ABI。根 Vico 模块继续
保留原有平台矩阵,OpenHarmony 的 Kotlin/Native 依赖由 example/settings.gradle.kts
中的社区 Maven 仓库解析。
example/ohosApp
↓ CMake + N-API
example/nativeApp/libvico_sample.so
↓ project dependency
example/shared
↓ shared Kotlin chart model
Vico chart concepts
这里的 shared 不是 ArkUI 页面数据的临时缓存,而是明确的跨平台模型层。
它定义 ChartKind、ChartPoint 和 ChartDefinition,并负责生成六种场景。
nativeApp 只负责把模型转换成 C ABI 可返回的 JSON,ohosApp 只负责读取
JSON 并绘图。
1.3 创建适配示例目录
参考 diff 工程的主要交互是编辑两段文本并比较差异。Vico 的核心能力是图表
图层和数据绘制,如果沿用文本输入框布局,会掩盖图表能力,也不能体现线、柱、
饼、环形和蜡烛等不同模型。因此页面选择了一个图表仪表盘:上方切换图表,
中间展示当前图表,底部展示缩放和能力标签。
第 2 阶段:目标与依赖打通
2.1 加入 ohosArm64 目标
共享模块保留 JVM 测试,同时增加 ohosArm64():
plugins {
kotlin("multiplatform")
}
kotlin {
jvm()
jvmToolchain(21)
ohosArm64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
JVM 目标让图表定义可以在不连接设备的情况下运行测试,ohosArm64 则让同一
份 commonMain 代码进入 Kotlin/Native 动态库。
2.2 focused build 的作用
Native 模块构建一个 shared library,并用 linker map 限制导出的符号:
kotlin {
jvmToolchain(21)
ohosArm64 {
binaries.sharedLib {
baseName = "vico_sample"
linkerOpts(
"--entry=0",
"--version-script=${project.file("src/ohosArm64Main/linker/shared-library.map")}",
)
linkerOpts("-lace_napi.z", "-luv", "-lhilog_ndk.z")
}
}
}
OpenHarmony 动态库不是直接由 ArkUI 加载的 JavaScript 包,而是通过 CMake
链接到 libentry.so。因此构建时需要同时处理 Kotlin/Native 链接器参数和
OpenHarmony NDK 库。
2.3 配置插件仓库和依赖仓库
Native 编译产物位于 Gradle 的 build/bin/ohosArm64/debugShared。为了让
DevEco 工程能按固定路径找到文件,prepareOhos 会复制:
libvico_sample.so → entry/libs/arm64-v8a/
libvico_sample_api.h → entry/src/main/cpp/include/
动态库和生成头文件属于构建产物,.gitignore 会排除它们。仓库保留复制任务,
这样每台开发机都可以从源码重新生成与本机工具链匹配的文件。
2.4 通过发布坐标消费库
example/settings.gradle.kts 同时配置 mavenLocal()、OpenHarmony 社区
Maven、Maven Central 和 Gradle Plugin Portal:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
}
}
OpenHarmony 示例的 Gradle 脚本使用 JDK 21。执行脚本前先确认:
export JAVA_HOME="/path/to/jdk-21"
java -version
第 3 阶段:数据与序列化
3.1 为什么需要统一 JSON 契约
ArkTS、C++ 和 Kotlin/Native 之间不能直接共享 Kotlin 对象。为了让边界稳定,
示例使用一个 JSON 响应描述图表模型:
VicoChartEngine
↓ ChartDefinition
Kotlin/Native JSON encoder
↓ UTF-8 buffer
C++ N-API string
↓
ArkTS JSON.parse
↓
Canvas renderer
这样图表定义始终在 Kotlin common 逻辑中生成,ArkTS 不需要了解 Kotlin data
class 的内部结构,也不会重新计算范围和百分比。JSON 字段可以随着图表能力
扩展,而 C ABI 入口仍然保持稳定。
3.2 ChartDefinition 设计
页面需要的不只是点列表,还需要标题、范围、摘要和能力标签:
public data class ChartDefinition(
val id: String,
val title: String,
val subtitle: String,
val kind: ChartKind,
val unit: String,
val points: List<ChartPoint>,
val minValue: Double,
val maxValue: Double,
val highlightedIndex: Int,
val summary: String,
val features: List<String>,
)
minValue 和 maxValue 在 Kotlin 中统一计算。ArkTS 只把值映射到 Canvas
绘图区;饼图和环形图固定使用 0 到 100 的百分比范围,避免不同刷新数据出现
两套坐标规则。
3.3 ChartPoint 和六种场景模型
每个点至少有标签和值,组合图和蜡烛图还可以通过 secondary 携带第二个数值:
public data class ChartPoint(
val label: String,
val value: Double,
val secondary: Double? = null,
)
VicoChartEngine.chart(index, refresh) 通过索引选择场景:
public fun chart(index: Int, refresh: Int = 0): ChartDefinition = when (index.mod(6)) {
0 -> lineChart(refresh)
1 -> columnChart(refresh)
2 -> comboChart(refresh)
3 -> pieChart(refresh)
4 -> donutChart(refresh)
else -> candlestickChart(refresh)
}
折线、柱状、饼图和环形图只需要 value,组合图把转化率放在 secondary,
蜡烛图把开盘价放在 secondary、收盘价放在 value。刷新编号参与固定公式,
不使用随机数和网络,因此测试可以准确比较刷新前后的模型。
3.4 错误响应和长度限制
Native 层返回的对象包含以下字段:
{
"id": "weekly-active-users",
"title": "Weekly active users",
"subtitle": "Line layer with points and marker focus",
"kind": "LINE",
"unit": "k users",
"minValue": 35.7,
"maxValue": 96.6,
"highlightedIndex": 6,
"summary": "84k today · +100% this week",
"features": ["line", "points", "marker", "horizontal axis"],
"points": [
{"label": "Mon", "value": 42},
{"label": "Sun", "value": 84}
]
}
secondary 是可选字段。Kotlin/Native 计算异常会转换成带 error 的 JSON,
C++ 层创建 ArkTS 字符串后立即调用 VicoSampleFree,避免 Native 堆内存一直
由 JavaScript 持有。索引和刷新参数在 N-API 层检查数字类型,避免无效输入
直接进入图表模型。
第 4 阶段:原生桥接(技术难点)
4.1 问题:Kotlin/Native 对象不能直接交给 ArkTS
Kotlin/Native 的 ChartDefinition、ChartPoint、List 和异常对象属于
Kotlin 运行时对象。ArkTS 通过 N-API 接收到的是 JavaScript 值,C++ 不能把
Kotlin 对象地址直接当成 JavaScript 对象使用。
最终采用三层桥接:
ArkTS
│ JSON string
▼
C++ N-API entry
│ const char*
▼
Kotlin/Native C ABI
│ shared chart model
▼
UTF-8 JSON + explicit free
4.2 方案对比
| 方案 | 优点 | 缺点 | 选用 |
|---|---|---|---|
| 直接导出 Kotlin 对象 | 代码少 | ABI、生命周期和类型不可控 | ❌ |
| 导出基础字段数组 | 不需要 JSON | 字段扩展和错误处理困难 | ❌ |
| C ABI + JSON | 边界清晰、易扩展、易调试 | 有一次序列化开销 | ✅ |
| 在 ArkTS 重写图表模型 | 页面调用简单 | 逻辑重复,结果可能与 Kotlin 不一致 | ❌ |
4.3 Kotlin/Native 导出函数
NativeBridge.kt 导出四个函数:
@CName("VicoSampleCatalog")
public fun catalogNative(): CPointer<ByteVar> = ...
@CName("VicoSampleChart")
public fun chartNative(index: Int, refresh: Int): CPointer<ByteVar> = ...
@CName("VicoSampleRunChecks")
public fun checksNative(): CPointer<ByteVar> = ...
@CName("VicoSampleFree")
public fun freeNative(pointer: CPointer<ByteVar>?) { ... }
返回值使用 nativeHeap.allocArray<ByteVar> 分配,并以 0 结尾,满足 C 字符串
约定。所有返回字符串都必须由 VicoSampleFree 释放。
4.4 C++ N-API 方法分发
C++ 模块注册三个 ArkTS 方法:
napi_property_descriptor methods[] = {
{"getCatalog", nullptr, Catalog, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"getChart", nullptr, Chart, nullptr, nullptr, nullptr,
napi_default, nullptr},
{"runChecks", nullptr, RunChecks, nullptr, nullptr, nullptr,
napi_default, nullptr},
};
getChart 检查参数数量和数字类型;三个方法都遵循“调用 Native → 生成 ArkTS
字符串 → 释放 Native 缓冲区”的顺序。CMake 将 Kotlin/Native 动态库作为
imported library:
add_library(vico_sample SHARED IMPORTED)
set_target_properties(vico_sample PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libvico_sample.so")
add_library(entry SHARED napi_init.cpp)
target_include_directories(entry PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(entry PRIVATE vico_sample libace_napi.z.so)
4.5 N-API 生命周期
ArkTS getChart(index, refresh)
│
▼
ReadNumber + argument check
│
▼
VicoSampleChart(index, refresh)
│
▼
napi_create_string_utf8(...)
│
▼
VicoSampleFree(nativeBuffer)
│
▼
return JS string
C++ 负责把 Native 缓冲区转换成 ArkTS 字符串,并在返回前释放缓冲区。页面调用者
不需要知道 Kotlin/Native 的堆实现,也不会因为刷新次数增加而积累 Native 内存。
第 5 阶段:能力封装
5.1 六种图表能力封装
共享层先把 Vico 图表概念封装成六种可验证模型:
| 模型 | 页面标题 | 表现的能力 |
|---|---|---|
LINE | Weekly active users | 点、连线、重点标记、水平坐标轴 |
COLUMN | Orders by day | 柱子、最高值、目标线、数值标签 |
COMBO | Revenue and conversion | 柱状序列、折线序列、双序列、图例 |
PIE | Acquisition mix | 扇区、切片标签、图例、100% 总和 |
DONUT | Platform share | 环形扇区、中心空洞、百分比、图例 |
CANDLESTICK | Market movement | 开收盘、最高最低、涨跌颜色 |
模型还提供摘要文本和 features,让页面可以在不理解 Kotlin 业务代码的情况下
展示当前场景的能力说明。
5.2 ArkTS 页面分区
Index.ets 只有一个 Stage 页面,按垂直方向分成:
标题区 Vico for OpenHarmony + 7/7 checks
选择区 Line / Columns / Combo / Pie / Donut / Candles
图表卡片 标题、说明、摘要、刷新按钮和 Canvas
缩放区 Zoom 数值、减号和加号
能力区 当前图表的 features 标签
这个布局避免了参考 diff 工程中的双文本编辑器交互,同时让每一种图层都能在
同一个页面里快速切换。
5.3 ArkTS 状态和错误状态
页面状态只有当前图表、索引、刷新编号、自检数量和缩放值:
@State private chart: ChartModel = EMPTY_CHART;
@State private chartIndex: number = 0;
@State private refresh: number = 0;
@State private checksPassed: number = 0;
@State private checksTotal: number = 0;
@State private zoom: number = 1;
loadChart 捕获 JSON 解析和 Native 调用异常并显示错误标题,不会保留上一张
图表的旧数据。刷新时只递增 refresh,缩放时限制在 0.8 到 2.0,并重新
调用 drawChart。
5.4 页面交互预设
页面提供六个图表按钮、图表卡片刷新按钮和缩放加减按钮:
- 切换按钮更新
chartIndex,并重新请求对应ChartDefinition; - 刷新按钮递增 revision,验证 Kotlin/Native 返回了新数据;
- 加减按钮改变横向坐标比例,验证 Canvas 重绘;
- 自检状态显示
runChecks()的通过数; - 能力标签从当前模型的
features直接生成。
第 6 阶段:示例与验证
6.1 example 工程结构
example/
├── build.gradle.kts
├── settings.gradle.kts
├── shared/
│ ├── build.gradle.kts
│ └── src/commonMain + src/commonTest/
├── nativeApp/
│ ├── build.gradle.kts
│ └── src/ohosArm64Main/
│ ├── kotlin/.../NativeBridge.kt
│ └── linker/shared-library.map
└── ohosApp/
└── entry/
├── src/main/ets/pages/Index.ets
└── src/main/cpp/napi_init.cpp
三个模块分别承担共享模型、Kotlin/Native 动态库和 DevEco Stage 页面职责。
6.2 原生模块注册
entry/src/main/cpp/napi_init.cpp 注册名为 entry 的 N-API 模块:
static napi_module vicoModule = {
1, 0, nullptr, Init, "entry", nullptr, {0}
};
extern "C" __attribute__((constructor))
void RegisterVicoModule() {
napi_module_register(&vicoModule);
}
注册的方法与 ArkTS 类型声明一致:
export const getCatalog: () => string;
export const getChart: (index: number, refresh: number) => string;
export const runChecks: () => string;
6.3 Native 动态库准备
先在仓库根目录执行:
export JAVA_HOME="/path/to/jdk-21"
./scripts/build-openharmony.sh
或者只执行示例任务:
(cd example && ./gradlew :shared:jvmTest :nativeApp:prepareOhos)
成功后应存在:
example/ohosApp/entry/libs/arm64-v8a/libvico_sample.so
example/ohosApp/entry/src/main/cpp/include/libvico_sample_api.h
6.4 构建与安装
准备签名工程:
python3 scripts/prepare-signing-project.py "$HOME/vico_ohos_signing"
在 DevEco Studio 中手动配置 API 20 ARM64 签名后构建 HAP:
./scripts/build-hap.sh "$HOME/vico_ohos_signing"
安装并启动:
hdc list targets
hdc -t <target-id> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <target-id> shell aa start -a EntryAbility -b com.patrykandpatrick.vico.ohos.sample
最后按六种图表、刷新、缩放和 7/7 checks 验收页面。
四、完整代码对照
4.1 整体架构
VicoChartEngine
├─ ChartKind / ChartPoint / ChartDefinition
├─ catalog()
├─ chart(index, refresh)
└─ runVicoChecks()
↓ JSON
NativeBridge.kt
├─ VicoSampleCatalog
├─ VicoSampleChart
├─ VicoSampleRunChecks
└─ VicoSampleFree
↓ C ABI
napi_init.cpp
├─ getCatalog()
├─ getChart(index, refresh)
└─ runChecks()
↓ ArkTS
Index.ets
├─ JSON.parse
├─ Canvas drawChart
├─ refreshChart
└─ adjustZoom
4.2 文件清单
| 文件 | 作用 |
|---|---|
example/shared/src/commonMain/.../VicoChartEngine.kt | 六种图表模型、范围计算、刷新和自检。 |
example/shared/src/commonTest/.../VicoChartEngineTest.kt | JVM 侧图表层、百分比和组合序列测试。 |
example/nativeApp/.../NativeBridge.kt | Kotlin/Native C ABI、JSON 和内存释放。 |
example/nativeApp/.../shared-library.map | 限制导出的 Native 符号。 |
example/ohosApp/entry/src/main/cpp/napi_init.cpp | C++ N-API 方法和模块注册。 |
example/ohosApp/entry/src/main/cpp/CMakeLists.txt | imported library 和 N-API 链接。 |
example/ohosApp/entry/src/main/ets/pages/Index.ets | ArkUI 状态、Canvas 和交互。 |
scripts/build-openharmony.sh | 根测试、示例测试和 Native 准备。 |
scripts/prepare-signing-project.py | 复制无签名材料的 DevEco 工程。 |
scripts/build-hap.sh | 调用 Hvigor 构建 HAP。 |
scripts/check-native-deps.py | 检查 ARM64 ELF 强依赖。 |
docs/openharmony/images/vico-openharmony-line.jpg | 真机 Line 图表效果图。 |
4.3 关键 API 对照
VicoSampleCatalog → 所有六种图表定义
VicoSampleChart → 一个图表定义
VicoSampleRunChecks → 七项共享验收检查
VicoSampleFree → 释放 Native 字符串
ArkTS 侧只依赖三个方法,避免把 Native 指针、Kotlin 对象和内存管理细节带入
页面逻辑。
4.4 ArkTS 与 TypeScript / Kotlin 的语法差异(本次实际踩到的)
| 能力 | Kotlin/Native | ArkUI |
|---|---|---|
| 图表类型 | 生成 ChartKind | 根据 kind 选择绘制分支 |
| 数据点 | 生成 ChartPoint | 映射到 Canvas 坐标 |
| 数值范围 | 生成 minValue / maxValue | 使用统一坐标换算 |
| 摘要文本 | 生成 summary | 显示在图表卡片 |
| 能力说明 | 生成 features | 渲染标签 |
| 刷新 | 根据 revision 生成新数据 | 递增 refresh 状态 |
| 缩放 | 不持有 UI 缩放状态 | 修改 zoom 并重绘 |
五、关键决策说明
决策 1:把 ohosArm64 加入示例的公共构建约定
示例模块加入 ohosArm64(),让同一份共享模型进入 Kotlin/Native 动态库。Vico
的 Compose 渲染实现和 ArkUI Stage 的 Canvas 生命周期不同,因此让 Kotlin 负责
跨平台模型,让 ArkUI 负责 OpenHarmony 原生绘制,在不改变 Vico 公共 API 的情况
下验证真实的 KMP/CMP 数据链路。
决策 2:独立消费者必须通过 Maven 坐标
example/shared、example/nativeApp 和 example/ohosApp 按独立工程组织,
先验证共享模型,再准备动态库,最后由 DevEco 打包。这样可以同时覆盖 Gradle
变体、KLIB、CMake、N-API 和 HAP 链路,避免根工程通过但下游无法消费。
决策 3:JSON 作为跨语言数据契约
如果在 ArkTS 中生成数据,Native 层只剩一个空壳,无法证明 Kotlin/Native
代码被设备真正调用。现在由 VicoChartEngine 生成标题、点、范围、摘要和
能力,ArkTS 只做 JSON 解析和绘制。
决策 4:桥接层只开放四个 C ABI 入口
图表定义字段会随场景增加而变化,JSON 可以向后兼容可选字段。C ABI 只保留
索引、刷新编号和字符串返回,不需要为每一个图表属性扩展 N-API 方法签名。
决策 5:页面按照库能力重新设计
参考 diff 工程的文本编辑交互不适合图表库,仪表盘把六种图表放在同一个选择区,
用户可以在一次运行中对比不同图层,并在每种图表下看到对应能力标签。刷新与缩放
状态留在 ArkTS 页面层,不改变共享模型的原始数据。
决策 6:把库验证和设备验证分开
随机数会让 JVM 测试、截图和设备复现变得困难,因此刷新编号参与固定计算公式,
每次刷新能产生变化,但同样的输入仍然得到同样的输出。JVM 测试验证模型和数值,
Kotlin/Native 编译验证目标,Hvigor 验证 HAP,真机验证 N-API、Canvas 和触摸操作。
证书、profile 和密码属于开发机材料,不应放入 AtomGit;开发者在 DevEco Studio
中手动签名,签名工程与源码工程分离。
六、测试与验证
6.1 测试环境
本次真机验收使用:
| 项目 | 值 |
|---|---|
| 设备 | HUAWEI Mate 60 Pro |
| 设备系统 | HarmonyOS 6.1.1 (24) |
| ABI | ARM64 |
| DevEco | DevEco Studio 6 系列 |
| HAP 包名 | com.patrykandpatrick.vico.ohos.sample |
| 入口 | EntryAbility |
| 示例版本 | 3.3.1-ohos.1 |
6.2 静态检查与单元测试
共享测试覆盖:
(cd example && ./gradlew :shared:jvmTest)
Native 动态库准备任务为:
(cd example && ./gradlew :nativeApp:prepareOhos)
检查点包括六种图表数量、组合图第二序列、饼图/环形图合计 100、蜡烛图开收盘
值和刷新前后数据变化。
6.3 原生桥接和 HAP 验证
DevEco 构建阶段需要确认:
- CMake 能找到
libvico_sample.so; - Ninja 能生成
libentry.so; - ArkTS 编译可以解析
libentry.so的类型声明; - HAP 包含
libs/arm64-v8a下的两个动态库; - 签名由当前开发机的 DevEco 配置完成。
可选的 Native 依赖检查:
mkdir -p /tmp/vico-hap
unzip -o entry-default-signed.hap 'libs/*' -d /tmp/vico-hap
python3 scripts/check-native-deps.py \
"$DEVECO_SDK_HOME/default/openharmony/native" \
/tmp/vico-hap/libs/arm64-v8a
6.4 功能验证用例
用例 1:默认页面和自检
启动应用后,页面显示 Vico for OpenHarmony、7/7 checks、六个图表按钮和
默认 Line 卡片。标题为 Weekly active users,页面底部显示 Zoom 1.0×。
用例 2:六种图表切换
按顺序点击 Line、Columns、Combo、Pie、Donut、Candles:
| 按钮 | 页面标题 | 主要验证点 |
|---|---|---|
| Line | Weekly active users | 点、连线、重点标记和坐标轴 |
| Columns | Orders by day | 柱子、最高值和目标线说明 |
| Combo | Revenue and conversion | 柱状序列和折线第二序列 |
| Pie | Acquisition mix | 切片、图例和 100% 占比 |
| Donut | Platform share | 中心空洞、图例和百分比 |
| Candles | Market movement | 开收盘实体、最高最低影线 |
每次切换都会更新卡片标题、摘要、Canvas 和能力标签。
用例 3:刷新数据
点击图表卡片右侧的刷新按钮,ArkTS 将 refresh 加一并重新调用
getChart(index, refresh)。验收时可观察摘要或点位变化,同时选中的图表类型
不改变。
用例 4:缩放和横向重绘
点击加号和减号,zoom 以 0.1 为步长变化,边界为 0.8× 到 2.0×。页面
文字和 Canvas 都会更新。缩放状态保留在 ArkTS 页面,不会改变 Kotlin 模型的
原始数据。
用例 5:Native 异常边界
Native getChart 对缺少索引或非数字参数抛出 N-API 类型错误,Kotlin 图表
计算异常转换为 JSON 错误字段。页面不会因为一次错误调用而持有未释放的 Native
缓冲区。
用例 6:库自检和错误处理
启动时执行 runChecks(),首页显示 7/7 checks。对缺少索引、非数字参数和
Native 计算异常进行验证,确认 N-API 返回类型错误或 JSON 错误对象,并且每个
Native 返回缓冲区都在创建 ArkTS 字符串后释放。
6.5 验证结论
本次验收结果:
sharedJVM 验收通过;nativeAppARM64 动态库生成成功;- ArkUI/Hvigor HAP 构建成功;
- 签名 HAP 安装成功并启动
EntryAbility; - 六种图表切换成功;
- 刷新能够改变 Native 数据;
- 缩放值可以从
1.0×调整到其他值并恢复; - 首页自检显示
7/7 checks。
七、运行效果
7.1 获取运行截图
下面是 HUAWEI Mate 60 Pro 上运行的 Line 场景截图。页面包含标题、自检状态、
六种图表按钮、折线图、刷新按钮、缩放区和能力标签。

7.2 界面文本快照
| 截图区域 | 对应实现 |
|---|---|
Vico for OpenHarmony | ArkUI 页面标题和平台说明。 |
7/7 checks | runChecks() 返回的共享模型自检数量。 |
| Line / Columns / Combo | 第一行图表选择按钮。 |
| Pie / Donut / Candles | 第二行图表选择按钮。 |
| Weekly active users | ChartDefinition.title。 |
| Line layer with points and marker focus | ChartDefinition.subtitle。 |
| 黄色折线和白色圆点 | Canvas 的 line、point 和 highlightedIndex 绘制。 |
↻ | refreshChart(),触发 Native 新数据。 |
Zoom 0.8× / 1.0× | ArkTS zoom 状态和横向坐标缩放。 |
| line / points / marker / horizontal axis | 当前图表的 features 标签。 |
其他五种场景
- Columns:蓝色柱子表示每天订单,最高柱子对应模型中的重点值。
- Combo:蓝色柱子和黄色折线同时出现,分别表达两个序列。
- Pie:彩色扇区和渠道标签表达 100% 获客占比。
- Donut:彩色环形扇区和中心空洞表达平台份额。
- Candles:绿色/粉色实体和影线表达上涨、下跌、最高和最低。
7.3 验证命令速查
# 根工程测试和 OpenHarmony 原生库准备
./scripts/build-openharmony.sh
# 单独运行共享测试和 Native 复制任务
(cd example && ./gradlew :shared:jvmTest :nativeApp:prepareOhos)
# 准备不带个人证书的 DevEco 工程
python3 scripts/prepare-signing-project.py "$HOME/vico_ohos_signing"
# 在已配置签名的工程中构建 HAP
./scripts/build-hap.sh "$HOME/vico_ohos_signing"
# 安装和启动
hdc -t <target-id> install -r entry-default-signed.hap
hdc -t <target-id> shell aa start -a EntryAbility -b com.patrykandpatrick.vico.ohos.sample
八、遗留问题与改进方向
8.1 踩坑复盘
- Native 文件必须先复制:如果没有执行
prepareOhos,CMake 的 imported
library 路径存在但文件不存在,Ninja 会直接失败。 - JDK 版本必须匹配:OpenHarmony 示例脚本使用 JDK 21,不能用不兼容的
JDK 版本绕过 Kotlin DSL 或 Native 编译器检查。 - 签名配置不能进源码:本机 DevEco 生成的 profile 可能带有绝对路径和
密码,必须使用独立签名工程。 - 多设备 hdc 需要指定目标:同时连接真机和其他设备时,安装命令要加
-t,否则 hdc 会提示需要确认设备。 - Canvas 和模型范围要一致:饼图按百分比绘制,笛卡尔图按模型范围绘制,
不能在 ArkTS 中用另一套默认范围。
8.2 已知问题
- 当前示例使用 Canvas 绘制基础图层,尚未实现完整 Compose Vico 的手势、动画、
滚动和复杂标记系统。 - 饼图和环形图展示图例和比例,切片点击命中测试仍是后续扩展点。
- Native 模型以 JSON 返回,数据量很小时足够直观;大数据集需要进一步评估
编码开销和增量更新策略。 - HAP 需要开发者在 DevEco Studio 中完成签名,仓库不会提供可直接发布的证书。
8.3 未来优化方向
- 将 Canvas 绘图拆成可复用的 Line、Column、Pie 和 Candle 组件。
- 为图表增加触摸命中、长按标记和十字线状态,并通过 ArkUI 手势传回模型。
- 在保持 JSON 契约稳定的前提下增加窗口化数据和增量刷新。
- 为 OpenHarmony 目标建立独立 CI,自动执行 JVM、Native、HAP 和依赖审计。
- 如果后续发布 OpenHarmony 变体,再为消费者提供明确的 Maven 坐标和版本策略。
九、总结
9.1 核心难点回顾
Vico OpenHarmony 适配的难点不是把按钮画出来,而是让同一份 Kotlin 图表
模型真正经过 Kotlin/Native、C ABI 和 N-API 到达设备上的 ArkUI Canvas:
共享模型 → Kotlin/Native → C ABI → C++ N-API → ArkTS JSON → Canvas
每一层都有清晰的输入和输出,出现问题时可以分别检查模型、动态库、符号、
HAP 或页面状态。
9.2 封装层次
Vico root modules
└── example
├── shared 图表模型、范围、刷新和 JVM 检查
├── nativeApp ohosArm64 动态库、C ABI、内存释放
└── ohosApp Stage、N-API、Canvas、按钮和 HAP
9.3 三条经验
- 先固定数据契约,再写 UI。
ChartDefinition让六种图表共享一套桥接
方式,ArkTS 不需要猜测 Kotlin 返回值。 - 把工具链问题和业务问题分开。 JVM、Native、CMake、Hvigor 和真机分层
验证,能迅速定位是依赖、符号、签名还是绘图问题。 - 签名工程必须隔离。 开源仓库提交源码和脚本,开发者在 DevEco 中手动
完成签名,既满足真机运行,也避免泄露本机密钥。
9.4 适配成果
- OpenHarmony 示例具备
ohosArm64Kotlin/Native 构建链路; - Kotlin 共享层提供六种图表模型和七项自检;
- Native 层通过四个 C ABI 函数向 ArkTS 提供 JSON;
- ArkUI 页面拥有独立的图表仪表盘 UI;
- 真机可以切换六种图表、刷新数据和调整缩放;
- 中文、英文 README、本文、效果图和仓库地址统一使用 AtomGit;
- 签名材料、HAP 和原生构建产物不进入源码仓库。
参考文档
更多推荐



所有评论(0)