开源鸿蒙平台 CMP 三方库「ComposeToast」适配全流程
本文记录
ComposeToast接入 OpenHarmony 的完整过程,覆盖纯 Compose UI 库的适配路线选型、模型层下沉、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
ComposeToast是基于 Compose Multiplatform 的 Toast 库:应用根部包一个ProvideToastManager,业务代码在任意位置调Toast.showSuccess / showError / showWarning / showInfo / show一行弹 Toast,四类型预设配色、操作按钮、FIFO 队列、Popup 三档定位、slide+fade 动画、自动消失全部由库托管。本次适配与 KStore 那类"真编译整库"的路线不同:上游整套 API 都 importandroidx.compose,社区鸿蒙工具链当前没有对齐的compose.uiohos 制品,Compose 渲染层无法编进 ohos——因此把类型模型、默认时长、默认配色、图标路径数据、队列语义这些不碰渲染树的内核下沉到commonMain编成动态库,Compose 源码原样隔离进composeMain,ArkUI 作为新的渲染后端消费桥接层下发的渲染规格(spec JSON)。
项目地址: AtomGit/oh-tpc/ComposeToast.git
上游仓库: ocnyang/ComposeToast
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
ComposeToast 为 Compose Multiplatform 应用提供"非侵入式 Toast"这一件事的完整答案:ProvideToastManager 一行接管全局,Toast 全局对象从任何位置(Composable、ViewModel、普通类)一行触发;ToastManager 内部是 StateFlow 队列,同一时刻只有一条 current、其余 FIFO 等待、展示到时自动消失;ToastHost 用 Popup 把卡片按 TOP / CENTER / BOTTOM 三档渲染(BOTTOM 整体上移 100dp 避开底部按钮、TOP 下移 16dp);ToastDefaults 给四类型预设配色(Success 0xF64CAF50 / Error 0xF6F44336 / Warning 0xF6FF9800 / Info 回落 Material 主题色);ToastIcons 内置四个 1024×1024 矢量图标;ActionData 支持操作按钮;DialogToastSupport 处理 Android/iOS 上 Dialog 独立窗口盖住页内 Toast 的问题。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套 Toast 能力,但上游构建(Kotlin 2.2.20 + Compose Multiplatform 1.9.1 + AGP)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示提示气泡,却无法证明上游的类型模型、配色默认值、图标矢量数据、队列语义真正在 OpenHarmony ARM64 设备上完成编译、链接和执行——那只是"又一个系统弹窗调用",不是库适配。适配需要同时验证模型内核、颜色回退链、矢量图标数据、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游用 Kotlin 2.2.20 + Compose Multiplatform 1.9.1 + AGP(com.android.kotlin.multiplatform.library)+ Compose 编译器插件,鸿蒙工具链与 AGP/Compose 插件无法共存于一个 Gradle 构建。 |
| 渲染层不可编译 | 上游 commonMain 六个源文件全部 import androidx.compose(Color、ImageVector、Popup、@Composable),社区 Maven 上没有与上游对齐的 compose.ui ohos 制品,整个渲染面无法进入 ohos 目标。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 时序语义跨不了桥 | 自动消失是 viewModelScope.delay 的协程时序,C ABI 只能同步一来一回。 |
| 矢量数据换格式 | ImageVector 的路径 DSL(curveToRelative / arcToRelative…)在鸿蒙没有对应物,需要无损转录成 ArkUI 可消费的格式。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:上游模型内核的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。渲染完全交给 ArkUI——上游本来就是"数据模型与渲染分离"的 Compose 惯用法,ArkTS 只是多了一个渲染后端。
1.2 库提供的能力
Demo 提供四个 Tab 的真实 Toast 会话,对应桥接的全部入口:
- 类型预设:
success/error/warning/info四个按钮,颜色、图标、时长全部由 KN 侧按类型决定(2000 / 3000 / 2500 / 2000 ms),消息、时长覆盖、位置(TOP / CENTER / BOTTOM)可选; - 自定义样式:
custom类型,背景/文字/图标三色色板(含"跟随主题"= 不传字段)、图标选择(含none强制无图标)、“撤销”/"重试"操作按钮、深色主题覆盖(theme三字段); - 队列管理:连发 3 / 5 条(第 2 条起 KN 侧排队,响应
queued=true)、提前消失当前、清空全部、队列深度实时展示; - 对话框与关于:
CustomDialog内弹 Toast(验证needsDialogToastWrapper=true的对话框作用域宿主策略)、toastMeta版本握手(版本号、平台语义、默认参数全部来自真实 Native 调用)。
响应包含一次 Toast 的完整渲染规格:
| 字段 | 作用 |
|---|---|
ok / error | 操作是否成功,失败时 error 带原文。 |
show / queued | 立即展示(附全量 spec)或已入队等待(附 id 与队列深度)。 |
toast | 全量渲染规格:id、类型、消息、图标种类与 SVG path、三色(已按回退链解析成 #AARRGGBB)、操作按钮、时长、位置、对齐与偏移、宽度上限、圆角/投影/内边距/间距/图标尺寸、动画时长与曲线、滑动来向。 |
next | toastDismiss 的响应字段:队列非空时是下一条的全量 spec,渲染端换内容重新计时。 |
queueDepth / platform / needsDialogToastWrapper | 队列深度与平台语义回显。 |
内核侧完整能力(类型默认值、颜色回退链、位置映射、图标路径、队列进出规则)全部在 :toast 的 commonMain 里原样编译进 .so,JVM 侧保留 13 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的内核逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 Compose 源码六个文件原样迁移进 composeMain 零改动保留;新增内核是六个纯 Kotlin 文件,其中图标坐标、类型默认值、位置偏移全部按上游数值逐位转录,不做任何"优化"。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);:composeApp / :iosApp 留盘不进构建;AGP、Compose 插件、发布配置随根构建替换剥离——本 fork 不发 Maven Central。 |
| 桥接稳定 | 4 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 定时纪律 | 自动消失的定时器不进 KN 侧:ArkUI 宿主按 durationMs setTimeout 后回调 dismiss,KN 侧零线程、零协程。 |
| UI 完整 | 单页四 Tab 覆盖全部桥接入口:类型预设、自定义样式、队列管理、对话框与版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游渲染层(
ToastHost的 Popup、material3 卡片、AnimatedVisibility)留在composeMain等社区 CMP 工具链对齐后接回;当前鸿蒙渲染是 ArkUI 按内核下发的 spec 复刻同一套视觉与动效。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 compose.ui 无 ohos 制品、确定交付物
第 2 阶段:目标与拆分打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、源码集拆分(composeMain 隔离)
第 3 阶段:桥协议建模 ── spec 渲染规格、回退链在 KN 侧闭合、show/queued 两态响应、错误通道
第 4 阶段:原生桥接 ── @CName × 4 C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 四 Tab 验收页、Path2D 矢量图标、队列交接与定时器纪律
第 6 阶段:示例与验证 ── JVM 交叉验证、HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
toast/src/commonMain/ 新内核:ToastType / ToastPosition / ToastIconPaths / ToastSpec / ToastSpecBuilder / ToastQueue
toast/src/composeMain/ 上游六个 Compose 源文件原样迁移(非注册源集,ohos 不编,等 CMP 工具链对齐后接回)
toast/src/composeTest/ 上游原测试(依赖 Compose Color)随迁,零改动
toast/src/androidMain|desktopMain|iosMain|jsMain|wasmJsMain/ 上游平台 actual 留盘,目标未声明即不参与编译
composeApp/ iosApp/ 上游 Demo,留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkUI 四 Tab 页面
doc/ 本文与效果图
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md / upstream_CN.md 上游原文归档
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :toast 与 :example:nativeApp,rootProject.name = "ohos_ComposeToast"。上游的 AGP、Compose Multiplatform 插件、Compose 编译器、maven-publish / signing 发布段全部随根构建替换剥离。
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(2.2.21 线的配套版本) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.composetoast |
1.3 源码依赖度盘点
| 源文件 | 依赖形态 | 处置 |
|---|---|---|
ToastData.kt | Color / ImageVector / expect currentTimeMillis | 迁入 composeMain;其中的 ToastPosition 枚举、ToastDefaults 数值、时长默认值由新内核按原值承接 |
ToastManager.kt | ViewModel / StateFlow / @Composable | 迁入 composeMain;队列进出与自动提升语义由 ToastQueue 镜像 |
ToastHost.kt | Popup / AnimatedVisibility / material3 | 迁入 composeMain;对齐/偏移/宽度上限/动画参数作为纯数据进入 spec |
ToastIcons.kt | ImageVector.Builder 路径 DSL | 迁入 composeMain;四图标坐标逐位转录为 SVG path(ToastIconPaths) |
DialogToastSupport.kt | @Composable | 迁入 composeMain;对话框包装策略由 needsDialogToastWrapper 平台语义承接 |
Platform.kt + 五平台 actual | expect/actual | 随 commonMain 迁入 composeMain;鸿蒙语义("HarmonyOS" / true)落在桥接层 |
ToastDataTest.kt | Compose Color | 随迁 composeTest,零改动 |
盘点结论:上游没有一行可整体编译进 ohos 的纯 Kotlin——这正是纯 UI 库与 KStore 类库的分水岭。适配面不是"补 actual",而是"把模型从渲染里手洗出来"。
1.4 页面边界
标题区 COMPOSETOAST · OPENHARMONY / 副标题(CMP Toast 库 · Kotlin/Native ohosArm64/ohosX64)
Tab1 类型预设 消息输入、时长(默认/2000/5000)、位置三档、四类型按钮、状态行
Tab2 自定义样式 背景三组色板(含跟随主题)、图标选择、撤销/重试操作按钮开关、深色主题覆盖、show 按钮
Tab3 队列管理 连发 3/5 条、提前消失当前、清空全部、current id 与队列深度
Tab4 对话框与关于 CustomDialog 内 Toast 场景、toastMeta 版本握手、durations/colors 默认值、仓库与社区链接
Toast 覆盖层 三张全屏 Stack 按 spec.alignment 三选一渲染,hitTestBehavior 穿透
第 2 阶段:目标与拆分打通
2.1 制品线验证(路线选型的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有与上游对齐的 Compose ohos 制品。结论:没有。社区 CMP 1.9.2 工具链面向的是它自己配套的 Compose 版本线,上游 Compose Multiplatform 1.9.1 的 compose.ui / compose.foundation / compose.material3 ohos 变体不存在,androidx.compose.ui.graphics.vector.ImageVector 在 ohos 目标上无法解析。
这决定了适配路线:KStore 那类"上游 commonMain 本来就是纯 Kotlin"的库可以真编译整库;ComposeToast 上游 commonMain 六个文件全部 import androidx.compose,渲染面整体进不来。能进 ohos 的是模型层——类型默认值、颜色回退链、位置映射、图标矢量数据、队列规则。社区已过审的 AestheticDialogs(Model/Token/配色引擎下沉)与 ktoast(blendARGB/ToastDuration 下沉)走的正是这条路。
2.2 工具链钉版(为什么是 2.2.21-1.0.0)
ohosArm64() / ohosX64() 只存在于 HarmonyOS Kotlin 插件。settings.gradle.kts 必须先写 eazytec Maven,再写 gradlePluginPortal()。Kotlin 版本钉 2.2.21-1.0.0,Gradle Wrapper 钉 8.14.1,JDK 钉 21——与已过审的 qrose / ktoast 同一条工具链线。上游仓库的 Kotlin 2.2.20 + AGP 8.11.2 + Compose 插件那套在鸿蒙上直接 sync 会在配置期就死。
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_ComposeToast"
include(":toast")
include(":example:nativeApp")
2.3 源码集拆分与内核下沉
toast/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvm()
jvmToolchain(21)
ohosArm64()
ohosX64()
sourceSets {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
拆分手法(git 均识别为 rename,上游历史完整保留):
- 上游六个 Compose 文件整体
git mv进toast/src/composeMain/kotlin/com/yhz/composetoast/。composeMain不是 Kotlin 源集,不会被任何目标编译,纯粹作为"上游源码原样留存 + 未来 CMP 工具链对齐后的接回位"; - 原测试随迁
composeTest,零改动; commonMain写入六个新内核文件,包名与上游一致(com.yhz.composetoast),全部零依赖纯 Kotlin:ToastType.kt:四类型枚举,承载上游ToastManager各 show 签名里的默认时长与ToastDefaults的 ARGB 颜色(Info 三色为 null,语义同上游"回落主题");ToastPosition.kt:位置枚举 + 上游ToastHost的 Popup 映射(alignment 名、y 偏移 BOTTOM -100 / TOP +16 / CENTER 0、滑动来向随 TOP 翻转);ToastIconPaths.kt:SvgPathBuilder的函数名与 ComposePathBuilder子集一一对应(moveTo/curveToRelative/reflectiveCurveToRelative/arcToRelative/verticalLineToRelative…),四个图标的指令序列与上游ToastIcons逐行对照、坐标逐位一致,输出 ArkUIPath2D可直接填充的 SVG path 字符串;填充规则 SVG 默认 nonzero,与上游PathFillType.NonZero一致;ToastSpec.kt/ToastSpecBuilder.kt:请求 → 渲染规格。回退链逐条对齐上游ToastContent:请求值 → 类型默认值 → Material3 基准浅色(surfaceVariant#E7E0EC/ onSurfaceVariant#49454F/ primary#6750A4,可用请求里的theme覆盖);宽度上限对齐minOf(maxWidth - 32.dp, 400.dp);圆角 12dp、投影 8dp、内边距 16/10、间距 12、图标 24、动画 300msFastOutSlowIn全部来自上游卡片与ToastHost的实参;ToastQueue.kt:镜像ToastManager队列语义——入队时无 current 立即展示,否则 FIFO 等待;dismiss 只对 current 生效并自动提升队首;clear 清空两者。id 改为单调计数 Long(上游是"时间戳-随机数"字符串,C ABI 需要可回传的数值 id,唯一性职责不变)。
根构建全局禁用无执行环境的 ohos 测试任务(与 qrose / ktoast 同款):
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
2.4 交叉验证测试的重建
上游原测试依赖 Compose Color,只能随迁 composeTest 留档。交叉验证的闸门由新内核测试承担:commonTest 新增 ToastCoreTest,13 项用例跑在 JVM 与 ohos 编译两端共用的同一份 commonMain 上——类型默认值逐项断言上游数值、Info 主题回退、请求覆盖、custom 类型语义、宽度上限 328/400 两档、深色主题覆盖、操作按钮颜色回退、hex 往返、队列全部进出规则、图标 path 合法性。这份测试后来直接抓住了第 8.1 节的第 4 条坑。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.yhz.composetoast/**(新内核,原值转录上游语义)
↓ ToastSpecBuilder(回退链闭合)/ ToastQueue(单 current + FIFO)
Bridge.kt(@CName × 4,UTF-8 JSON 进出)
↓
N-API toastShow() / toastDismiss() / toastClear() / toastMeta()
↓
ArkUI Index.ets(四 Tab + 三层对齐覆盖层)
请求与响应:
请求 : {"type":"success","message":"...","durationMs":2000,"position":"bottom",
"backgroundColor":"#F64CAF50","textColor":"#FFFFFFFF","iconColor":"#FFFFFFFF",
"icon":"success","actions":[{"label":"撤销","actionColor":"#FF6750A4"}],
"screenWidthDp":360,
"theme":{"surfaceVariant":"#E7E0EC","onSurfaceVariant":"#49454F","primary":"#6750A4"}}
(除 type/message 外全部可选;颜色支持 #RRGGBB / #AARRGGBB;缺省走类型默认或主题)
响应 : {"ok":true,"show":true,"toast":{...全量 spec...},"queueDepth":0,
"platform":"HarmonyOS","needsDialogToastWrapper":true}
{"ok":true,"queued":true,"id":3,"queueDepth":2}
{"ok":false,"error":"..."}
一个关键对称设计:颜色与布局不做双端各自解析。请求里的可空颜色在 KN 侧按"请求值 → 类型默认 → 主题"的回退链一次闭合,响应的 spec 三色永远是解析完成的 #AARRGGBB;ArkUI 拿到即渲染,零回退逻辑。上游的回退语义(toast.backgroundColor ?: MaterialTheme.colorScheme.surfaceVariant)因此只存在一份实现,就在 .so 里。
3.2 spec 为什么全量下发
spec 的 23 个字段不只是颜色——位置到对齐/偏移的映射、宽度上限的数学、圆角/投影/内边距/间距/图标尺寸、动画时长与曲线、滑动来向,全部在 KN 侧定死。这是 AestheticDialogs"spec 驱动渲染"先例的直接沿用:渲染端越薄,两端视觉漂移的可能性越小,未来任何渲染后端(包括 CMP 工具链对齐后的 Compose 接回)消费的都是同一份规格。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ToastShow")
fun toastShow(request: String): String {
return try {
/* 解析 → 构建 spec → 入队 */
} catch (t: Throwable) {
"""{"ok":false,"error":${jsonString(t.message ?: t.toString())}}"""
}
}
非法 JSON、缺 type / message、非法颜色串,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行,HAP 不崩。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libcomposetoast_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoscomposetoast.so)
│ ToastShow / ToastDismiss / ToastClear / ToastMeta
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制一份队列与回退语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("ToastShow") fun toastShow(request: String): String // 构建 spec + 入队,一口进
@CName("ToastDismiss") fun toastDismiss(request: String): String // 消失当前并提升队首
@CName("ToastClear") fun toastClear(request: String): String // 清空,返回清掉条数
@CName("ToastMeta") fun toastMeta(request: String): String // 版本握手 + 默认参数
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
ToastShow | toastShow(req) | ToastSpecBuilder.build(回退链/宽度/动画)+ ToastQueue.enqueue(立即展示或 FIFO 排队) |
ToastDismiss | toastDismiss(req) | ToastQueue.dismiss(只认 current 的 id,队首自动顶上,next 交接) |
ToastClear | toastClear(req) | ToastQueue.clear(current + 等待队列一并清空) |
ToastMeta | toastMeta(req) | 版本握手、平台语义、四类型默认时长与配色(真机"关于"面板数据源) |
与上游的时序对齐:自动消失是 viewModelScope.delay,桥内不持线程——ArkUI 宿主按 spec 的 durationMs setTimeout 后回调 toastDismiss,两条路在"展示 durationMs 后消失并推进队列"这一点上严格等价;KN 侧唯一的全局状态是那条队列,零协程、零锁。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"toastShow", nullptr, NapiToastShow, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"toastDismiss", nullptr, NapiToastDismiss, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"toastClear", nullptr, NapiToastClear, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"toastMeta", nullptr, NapiToastMeta, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoscomposetoast_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoscomposetoast_api.h 由 CAdapter 链接期自动生成,copyKnSo* 任务随 so 一起拷进 entry/libs/<abi>/ 并同步到 cpp/include/,不手写、不拖整份符号表。
4.4 CMake 和类型声明
add_library(ohoscomposetoast SHARED IMPORTED)
set_target_properties(ohoscomposetoast PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(composetoast_napi SHARED napi_init.cpp)
target_link_libraries(composetoast_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoscomposetoast)
OHOS_ARCH 为空或 libohoscomposetoast.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。nativeApp 侧的 copyKnSoArm64 / copyKnSoX64 任务把链接产物与匹配 triple 的 libc++_shared.so 自动拷进 HAP 的 entry/libs/<abi>/,并以 finalizedBy 串起 copyApiHeader,链接完头文件就位。
export const toastShow: (request: string) => string;
export const toastDismiss: (request: string) => string;
export const toastClear: (request: string) => string;
export const toastMeta: (request: string) => string;
第 5 阶段:ArkUI 宿主
5.1 调用协议与队列交接
private callShow(req: ShowRequest): void {
const raw: string = toastShow(JSON.stringify(req));
const resp = JSON.parse(raw) as ShowResp;
if (resp.show === true) {
this.present(specFrom(resp.toast), resp.queueDepth ?? 0); // 渲染 + setTimeout(durationMs)
return;
}
if (resp.queued === true) { /* 本条不渲染,等前一条 dismiss 的 next 交接 */ }
}
private onExpire(id: number): void {
const resp = JSON.parse(toastDismiss(JSON.stringify({ id }))) as DismissResp;
const next = specFrom(resp.next);
if (next !== null) {
animateTo(exitAnim, () => { this.currentToast = next; }); // 换内容,重新计时
this.armTimer(next.durationMs, next.id);
} else {
animateTo(exitAnim, () => { this.currentToast = null; }); // 队列空,清场
}
}
armTimer 先 clearTimer 再 setTimeout,页面销毁时统一清理句柄;操作按钮点击复刻上游 ActionData.onAction + onDismiss——先入队一条反馈 Toast,再 dismiss 当前,反馈条目经 next 交接渲染。
5.2 页面交互与矢量图标
四 Tab 自上而下按 1.4 的页面边界排布。Toast 覆盖层是三张全屏 Stack(Top / Center / Bottom 对齐各一张,按 spec.alignment 三选一渲染),hitTestBehavior(HitTestMode.None) 让空白区域穿透、只有卡片本身可点(点击 = 提前 dismiss);spec.offsetYDp 换算成 margin(BOTTOM 上移 100、TOP 下移 16)。进出场用 TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: ±40 })),曲线 curves.cubicBezierCurve(0.4, 0, 0.2, 1)——这正是上游 FastOutSlowInEasing 的贝塞尔定义,时长取 spec 的 animationDurationMs。
图标渲染是本次适配的复用卖点:ToastIconView 组件内 new Path2D(spec.iconPath),先 scale(iconSize / 1024) 再按 iconColor 填充——四个图标的路径数据来自上游 ToastIcons 的 1:1 转录,鸿蒙上画的和 Android/iOS 上的是同一份几何。onReady 只触发一次,队列交接换内容时靠 @Watch 触发重绘。
5.3 对比度和错误边界
类型预设四按钮按各自品牌色着色(绿 / 红 / 琥珀 / 浅紫);破坏性操作(清空全部)用酒红;ok:false 时错误原文进状态行与 hilog。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行。操作按钮与卡片点击共用一片点击区,ClickEvent 没有 stopPropagation,用 300ms 时间窗守卫过滤冒泡。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoscomposetoast)+ cxxShared + copyKnSo* + copyApiHeader
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 4 + MiniJson(支持嵌套 actions/theme,继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.composetoast、versionName 1.0.0-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohoscomposetoast_api.h
├── src/main/cpp/types/libcomposetoast_napi/{index.d.ts, oh-package.json5}
├── src/main/ets/pages/Index.ets ArkUI 验收页面(四 Tab + 对话框场景)
├── src/main/ets/components/ToastCardView.ets spec 全字段渲染卡片
├── src/main/ets/components/ToastIconView.ets Path2D 矢量图标
└── src/main/ets/model/ToastModels.ets 桥 JSON 契约 interface
6.2 构建、签名和安装
./gradlew :toast:compileKotlinJvm :toast:jvmTest # 13 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 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.composetoast -m entry
四、完整代码对照
4.1 整体架构
toast/src/composeMain/**(上游 Compose 源码,零改动留存,ohos 不编)
│ ToastHost / ToastManager / ToastContent / ToastIcons / DialogToastSupport
toast/src/commonMain/**(新内核,零依赖纯 Kotlin,jvm + ohos 双端编译)
│ ToastType(默认值)/ ToastPosition(映射)/ ToastIconPaths(矢量数据)
│ ToastSpecBuilder(回退链)/ ToastQueue(单 current + FIFO)
Bridge.kt + MiniJson
│ ToastShow / ToastDismiss / ToastClear / ToastMeta
libcomposetoast_napi.so
│ toastShow / toastDismiss / toastClear / toastMeta
Index.ets + ToastCardView + ToastIconView
│ JSON parse + @State 更新 + setTimeout 自动消失
▼
ArkUI 真机页面(四 Tab + 三层对齐覆盖层)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_ComposeToast、:composeApp/:iosApp 移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 单插件钉版 |
build.gradle.kts(根) | 版本 1.0.0-ohos.1、全局禁用 Ohos*Test* 任务 |
toast/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,发布配置剥离(见 Git 历史) |
toast/src/composeMain/** | 上游六个 Compose 源文件原样留存(零改动,git rename) |
toast/src/commonMain/** | 六个新内核文件:类型默认值 / 位置映射 / 图标矢量数据 / spec 回退链 / 队列 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填、copyApiHeader |
nativeApp/.../Bridge.kt | 4 个 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 验收页面(四 Tab + 队列交接 + 定时器纪律) |
harmonyApp/.../ets/components/*.ets | spec 渲染卡片 + Path2D 矢量图标 |
toast/src/commonTest/.../ToastCoreTest.kt | 13 项内核交叉验证 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档 |
4.3 关键 API 对照
| 上游 API(Compose) | 鸿蒙对应物 | 说明 |
|---|---|---|
ToastManager.showToast(...) | toastShow(req) → ToastQueue.enqueue | 无 current 立即展示,否则 FIFO 排队 |
ToastManager.dismissCurrent / showNextToast | toastDismiss({id}) → ToastQueue.dismiss | 只认 current 的 id,next 字段交接队首 |
ToastManager.clear | toastClear() | current + 等待队列一并清空 |
Toast.showSuccess/showError/showWarning/showInfo | 请求 type 字段 + ToastType 默认值 | 时长 2000/3000/2500/2000、配色、图标由类型决定 |
ToastDefaults | ToastType.defaultBackground/Text/Icon | ARGB 与上游 Color(0x...) 逐位一致 |
ToastContent 的 ?: MaterialTheme 回退链 | ToastSpecBuilder 请求值→类型默认→M3 基准浅色 | 回退在 KN 侧闭合,ArkUI 零回退逻辑 |
ToastHost 的 Popup alignment / offset | spec alignment / offsetYDp | BottomCenter/-100、TopCenter/+16、Center/0 |
minOf(maxWidth - 32.dp, 400.dp) | spec maxWidthDp | 数学原样,screenWidthDp 由请求携带 |
slideInVertically + fadeIn 300ms FastOutSlowIn | spec animation* + TransitionEffect | cubicBezier(0.4, 0, 0.2, 1) 即 FastOutSlowIn |
ToastIcons 四个 ImageVector | ToastIconPaths SVG path + Path2D 填充 | 坐标逐位转录,nonzero 与上游一致 |
ActionData / onAction | 请求 actions 数组 + 操作回调 | 点击先入队反馈 Toast 再 dismiss,经 next 交接 |
DialogToastContent(Android/iOS 包装策略) | needsDialogToastWrapper=true + 对话框作用域宿主 | ArkUI CustomDialog 同为独立窗口,保守策略同 iOS |
viewModelScope.delay 自动消失 | ArkUI setTimeout(durationMs) → toastDismiss | KN 侧零线程零协程,时序等价 |
4.4 ArkTS 与 Kotlin 的边界
const resp = JSON.parse(toastShow(JSON.stringify({
type: 'success', message: '操作完成', position: 'bottom'
}))) as ShowResp;
// resp.show === true:spec 三色/图标/动画已解析完毕,卡片直接渲染
// resp.queued === true:本条在 KN 队列等待,由前一条 dismiss 的 next 交接
@CName("ToastShow")
fun toastShow(request: String): String = try {
val root = MiniJson.asObject(MiniJson.parse(request))
val spec = ToastSpecBuilder.build(parseRequest(root), nextToastId())
val result = queue.enqueue(spec)
if (result.shown) showResponse(spec, result) else queuedResponse(spec, result)
} catch (t: Throwable) {
fail(t.message ?: t.toString())
}
两者之间只传输 UTF-8 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:模型下沉而不是真编译整库
KStore 能真编译整库,是因为它的 commonMain 本来就是纯 Kotlin;ComposeToast 上游 commonMain 六个文件全部 import androidx.compose,而社区 Maven 上没有对齐的 compose.ui ohos 制品(2.1 已验证)。路线必须跟着当天的制品可达性走:能下场的只有模型层。下沉不是重写——类型默认值、位置偏移、图标坐标、回退顺序、队列规则全部按上游数值与语义逐位转录,13 项 JVM 交叉验证就是"内核没漂"的闸门。
决策 2:上游 Compose 源码原样进 composeMain,一个字节不改
删除是最差的选项:上游渲染层是"未来社区 CMP 工具链对齐后接回"的本体,也是鸿蒙 spec 与上游视觉是否一致的对照物。composeMain 不是 Kotlin 源集、不参与任何目标编译,只承担留存与对照职责;原测试随迁 composeTest 留档,同样零改动。
决策 3:根构建替换(cmp_qrose 模式)
上游根构建的 AGP、Compose Multiplatform 插件、Compose 编译器、maven-publish / signing 对鸿蒙没有保留价值,整个替换为极简 ohos 构建。:composeApp / :iosApp 留盘不进 settings.gradle.kts。发布配置随根构建剥离,本 fork 不发 Maven Central,原配置在 Git 历史可考。
决策 4:spec 全量下发 + 图标矢量数据 1:1 转录
渲染端越薄,两端视觉漂移越小。spec 把颜色回退、位置映射、宽度数学、布局参数、动画曲线一次定死,ArkUI 拿到即渲染。图标不用系统 SymbolGlyph 近似——那会让鸿蒙用户看到四个"长得差不多但不是上游"的图标;SvgPathBuilder 的函数名与 Compose PathBuilder 一一对应,转录可以逐行 diff,Path2D 按同一份几何填充。
决策 5:定时器留在 ArkTS,KN 零线程零协程
上游自动消失是 viewModelScope.delay;鸿蒙侧若在 KN 里起协程定时,就要处理跨 FFI 的生命周期与线程问题。改由 ArkUI 宿主 setTimeout(durationMs) 后回调 toastDismiss,语义严格等价(展示 durationMs 后消失并推进队列),KN 侧唯一的全局状态就是那条队列——桥的复杂度上限被压到了"同步函数 + 一个队列"。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin,org.gradle.java.home 钉版)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :toast:compileKotlinJvm :toast:jvmTest
13 项用例全部通过。同一份 commonMain 也是 ohosArm64 / ohosX64 .so 里跑的逻辑,这些断言就是桥接契约的验收底线:
| 用例组 | 钉住什么 |
|---|---|
| 类型默认值 | 四类型时长 2000/3000/2500/2000、配色 0xF64CAF50 等逐位等于上游 ToastDefaults、Info 三色为 null |
| 位置解析 | fromId 大小写/空白容错、未知回落 BOTTOM;alignment/offsetYDp/slideFrom 三组映射 |
| 主题回退 | Info 无色时回落 M3 基准浅色 #FFE7E0EC / #FF49454F / #FF6750A4;theme 覆盖生效 |
| 请求覆盖 | 颜色/时长/图标显式传值优先;icon:"none" 强制无图标;custom 类型语义同上游 show() |
| 宽度上限 | 360dp 屏 → 328、1000dp 屏 → 400 封顶,对齐 minOf(maxWidth - 32.dp, 400.dp) |
| 操作按钮 | actionColor 缺省回落主题 primary,显式色生效 |
| hex 往返 | argbHex / parseHex 对带 alpha 的上游色(如 #F64CAF50)严格往返 |
| 队列语义 | 入队展示/排队、错 id 不动队列、dismiss 提升队首、清空计数,全部对齐 ToastManager |
| 图标数据 | 四个 path 以 M 开头含 Z、无 NaN;none/未知种类返回空串 |
6.3 HAP 验证
:toast:compileKotlinJvm / :toast:jvmTest: BUILD SUCCESSFUL(13/13)
:example:nativeApp:linkDebugSharedOhosArm64: BUILD SUCCESSFUL
:example:nativeApp:linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ToastShow / ToastDismiss / ToastClear / ToastMeta 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libcomposetoast_napi.so)
CompileArkTS: finished
PackageHap: finished
install bundle successfully
start ability successfully
debugShared 未 strip 时 libohoscomposetoast.so arm64 约 3.77MB(3,949,616 字节)、x86_64 约 3.47MB(3,635,048 字节,含 Kotlin/Native 运行时),每个 ABI 三件套(libcomposetoast_napi.so / libohoscomposetoast.so / libc++_shared.so)由 copyKnSo* 与 CMake POST_BUILD 双路保证同目录。
6.4 功能用例
真机已按四 Tab 验收清单逐项过检,效果图记录了类型预设链路:
- 启动后 Tab4 出现
toastMeta版本握手:version=1.0.0-ohos.1、platform=HarmonyOS、needsDialogToastWrapper=true(全部来自真实 Native 调用,非静态文字); - 类型预设页输入消息、选 2000ms、BOTTOM 位置,点「成功」:绿色 Toast(
#F64CAF50)从底部滑入,白色对勾图标由Path2D按上游矢量数据渲染,状态行显示show id=3 type=success 2000ms 队列=0; - 到时自动消失(KN 不参与计时,ArkUI setTimeout 与上游
delay时序等价);连续点「错误」「警告」「信息」,各自品牌色与时长逐条生效; - 切 TOP 位置再弹:卡片从顶部滑入且整体下移 16dp;切 BOTTOM 再弹:整体上移 100dp 避开底部按钮;
- 快速连点预设按钮(连发场景):第 2 条起状态行显示
queued,当前条消失后下一条自动顶上(next交接),队列深度实时正确; - 自定义样式页选深灰底/白字/
none图标 + 勾选「撤销」:卡片按自定义三色渲染、无图标、出现操作按钮;点「撤销」先弹反馈 Toast 再 dismiss 当前,反馈条经队列交接渲染; - 打开深色主题开关再弹 custom:三色按
theme覆盖值解析(深底浅字浅紫图标); - Tab3「提前消失当前」「清空全部」分别触发单条 dismiss 与
toastClear,current id 与队列深度归零; - Tab4 打开对话框,对话框内弹 Toast 在对话框子窗口内渲染(
needsDialogToastWrapper=true策略),自动消失后队列交接给页面作用域。
6.5 验证结论
类型模型、颜色回退链、矢量图标数据、队列语义、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的 Toast 会话:版本握手、类型预设、自定义样式、队列进出与对话框场景(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/ComposeToast.git
cd ComposeToast
export JAVA_HOME="/path/to/jdk-21"
./gradlew :toast:compileKotlinJvm :toast:jvmTest # 13 项交叉验证
./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.composetoast -m entry
hdc shell hilog | grep -i composetoast
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP/CMP 适配,必须让上游模型真正编译成
ohosArm64/ohosX64动态库——哪怕是"下沉的内核"而不是整库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin/Native 没有
java.lang.Math:MiniJson 里顺手写的Math.floor在 ohos 编译期直接失败,必须用kotlin.math.floor——commonMain 代码对 JVM 习语的每一点依赖都要过一遍 Native 语法面; - 颜色解析的整数溢出:8 位 hex(如
#F64CAF50,超过Int.MAX_VALUE)用toIntOrNull(16)解析会静默返回 null,必须toLongOrNull(16)再toInt()。这个坑是jvmTest的 hex 往返用例抓住的——交叉验证的价值就在这类"编译期不报错、运行期才错"的地方; - ArkUI 组件状态命名与通用属性冲突:
@State position/@Prop size会和组件基类的.position()/.size()撞名,必须改名(toastPosition/iconSize); Canvas.onReady只触发一次:队列交接换内容时iconPath变了但画布不会重绘,需要@Watch监听@Prop变化主动重画;ClickEvent没有 stopPropagation:Toast 卡片的 onClick 与操作按钮的 onClick 共用一片点击区,用 300ms 时间窗守卫过滤冒泡;- 根构建的
/build/与.idea/*是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的example/harmonyApp/构建产物要单独放目录级.gitignore,否则混进提交; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 根构建全局禁用
Ohos*Test*任务:HarmonyOS Kotlin 的 CAdapter 生成的测试任务没有可用的执行环境,留着会在build时炸; - 签名材料由 DevEco 自动写入
build-profile.json5,提交前注意不要把证书路径带进仓库。
8.2 已知问题
- Compose 渲染层未上鸿蒙:
ToastHost/ material3 卡片 /AnimatedVisibility留在composeMain,当前鸿蒙视觉是 ArkUI 按 spec 复刻;等社区compose.uiohos 制品与上游 Compose 版本对齐后,可把composeMain接回commonMain走真渲染; - 图标渲染走 Canvas 2D 软填充:24dp 规格下与上游
Icon视觉一致,超大尺寸(如 >256vp)不建议直接放大使用; needsDialogToastWrapper=true是保守策略:ArkUICustomDialog是独立子窗口,对话框内的 Toast 需要对话框作用域宿主;跨对话框排队的条目由 KN 队列统一调度,渲染作用域以 current 所在宿主为准;- 自动消失计时在 ArkTS:页面销毁必须清定时器句柄(Demo 已处理);切后台期间的计时与前台动画的细微偏差由宿主承担;
- 全局单队列:上游
ToastManager即单队列模型,鸿蒙侧保持一致;多宿主(页内 + 对话框)共享同一条 KN 队列是上游语义的自然延伸,不是缺陷; - 队列 id 为单调 Long,进程重启后从 1 重新计数(上游 id 同样不跨进程持久);
- android / ios / desktop / js / wasmJs 目标剥离(源文件留在树里),发布、签名配置一并剥离,本 fork 不发 Maven Central。
8.3 未来优化方向
- 按位置分队列(TOP / CENTER / BOTTOM 三宿主并行展示),需先在上游语义上确认多 current 模型;
toastShow扩展自定义图标 path 直传字段,让业务可注入品牌矢量;- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(x86_64)验收矩阵和自动化截图脚本(uitest + hdc)。
九、总结
9.1 核心难点回顾
上游 ComposeToast(commonMain 六文件全依赖 androidx.compose)
→ 验证 compose.ui 无 ohos 制品 → 确定模型下沉路线(AestheticDialogs / ktoast 先例)
→ 工具链钉版 Kotlin 2.2.21-1.0.0(Gradle 8.14.1 / JDK 21)
→ 根构建替换(cmp_qrose 模式)+ 源码集拆分(composeMain 零改动隔离)
→ 六个纯 Kotlin 内核文件(默认值/映射/矢量数据/回退链/队列,原值转录)
→ ohosArm64 / ohosX64 KLIB + 13 项 JVM 交叉验证
→ C ABI(@CName × 4)+ spec 全量下发
→ C++ N-API(DisposeString 纪律)
→ ArkUI 四 Tab 验收(Path2D 矢量图标 / 队列交接 / 定时器纪律 / 对话框场景)
9.2 封装层次
- 内核层:六个零依赖纯 Kotlin 文件,类型默认值、位置映射、图标矢量数据、颜色回退链、队列语义全部原值转录自上游;
- Native 层:双 ABI 动态库通过 4 个 C ABI 输出渲染规格;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、调用协议分支(show / queued / 失败)、定时器纪律、Path2D 矢量渲染与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 路线跟着制品可达性走,不跟着历史结论走:同一个社区、同一个工具链,KStore 的正确答案是"真编译整库 + 8 行 actual",ComposeToast 的正确答案是"模型下沉 + 渲染换后端"——分水岭只在上游 commonMain 有没有一行可以脱离
androidx.compose编译的代码; - 纯 UI 库的适配面不是"补平台代码",而是"把模型从渲染里手洗出来":洗出来的每一项(默认值、偏移、坐标、回退顺序)都必须按上游原值逐位转录,任何"顺手优化"都是漂移;
- 交叉验证是"内核没漂"的唯一可信证明:13 项
jvmTest跑在 ohos.so同一份 commonMain 上,hex 溢出这类"编译期不报错"的坑正是被它拦下的;渲染类库的 Demo 验收重点是"同一输入在两端是否同一视觉",spec 全量下发让这件事可以被逐字段核对。
9.4 适配成果
当前适配已完成:
- ComposeToast
1.0.0(1.0.0-ohos.1)模型内核的双 ABI Kotlin/Native 编译(类型默认值 / 位置映射 / 图标矢量数据 / 回退链 / 队列语义,全部原值转录;上游 Compose 源码零改动留存composeMain); - 4 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号); - DevEco Stage ArkUI 四 Tab 验收页面(类型预设、自定义样式、队列管理、对话框与版本握手);
- 13 项 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)