本文记录 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 都 import androidx.compose,社区鸿蒙工具链当前没有对齐的 compose.ui ohos 制品,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)、操作按钮、时长、位置、对齐与偏移、宽度上限、圆角/投影/内边距/间距/图标尺寸、动画时长与曲线、滑动来向。
nexttoastDismiss 的响应字段:队列非空时是下一条的全量 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin,org.gradle.java.home 已写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(2.2.21 线的配套版本)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.composetoast
1.3 源码依赖度盘点
源文件依赖形态处置
ToastData.ktColor / ImageVector / expect currentTimeMillis迁入 composeMain;其中的 ToastPosition 枚举、ToastDefaults 数值、时长默认值由新内核按原值承接
ToastManager.ktViewModel / StateFlow / @Composable迁入 composeMain;队列进出与自动提升语义由 ToastQueue 镜像
ToastHost.ktPopup / AnimatedVisibility / material3迁入 composeMain;对齐/偏移/宽度上限/动画参数作为纯数据进入 spec
ToastIcons.ktImageVector.Builder 路径 DSL迁入 composeMain;四图标坐标逐位转录为 SVG path(ToastIconPaths)
DialogToastSupport.kt@Composable迁入 composeMain;对话框包装策略由 needsDialogToastWrapper 平台语义承接
Platform.kt + 五平台 actualexpect/actual随 commonMain 迁入 composeMain;鸿蒙语义("HarmonyOS" / true)落在桥接层
ToastDataTest.ktCompose 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,上游历史完整保留):

  1. 上游六个 Compose 文件整体 git mv 进 toast/src/composeMain/kotlin/com/yhz/composetoast/。composeMain 不是 Kotlin 源集,不会被任何目标编译,纯粹作为"上游源码原样留存 + 未来 CMP 工具链对齐后的接回位";
  2. 原测试随迁 composeTest,零改动;
  3. 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 的函数名与 Compose PathBuilder 子集一一对应(moveTo / curveToRelative / reflectiveCurveToRelative / arcToRelative / verticalLineToRelative…),四个图标的指令序列与上游 ToastIcons 逐行对照、坐标逐位一致,输出 ArkUI Path2D 可直接填充的 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、动画 300ms FastOutSlowIn 全部来自上游卡片与 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 方法覆盖内核
ToastShowtoastShow(req)ToastSpecBuilder.build(回退链/宽度/动画)+ ToastQueue.enqueue(立即展示或 FIFO 排队)
ToastDismisstoastDismiss(req)ToastQueue.dismiss(只认 current 的 id,队首自动顶上,next 交接)
ToastCleartoastClear(req)ToastQueue.clear(current + 等待队列一并清空)
ToastMetatoastMeta(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.tomlKotlin 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.kt4 个 C ABI、MiniJson(嵌套解析)、错误通道、平台语义
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 验收页面(四 Tab + 队列交接 + 定时器纪律)
harmonyApp/.../ets/components/*.etsspec 渲染卡片 + Path2D 矢量图标
toast/src/commonTest/.../ToastCoreTest.kt13 项内核交叉验证
README.OpenHarmony_CN.md鸿蒙适配主文档

4.3 关键 API 对照

上游 API(Compose)鸿蒙对应物说明
ToastManager.showToast(...)toastShow(req) → ToastQueue.enqueue无 current 立即展示,否则 FIFO 排队
ToastManager.dismissCurrent / showNextToasttoastDismiss({id}) → ToastQueue.dismiss只认 current 的 id,next 字段交接队首
ToastManager.cleartoastClear()current + 等待队列一并清空
Toast.showSuccess/showError/showWarning/showInfo请求 type 字段 + ToastType 默认值时长 2000/3000/2500/2000、配色、图标由类型决定
ToastDefaultsToastType.defaultBackground/Text/IconARGB 与上游 Color(0x...) 逐位一致
ToastContent 的 ?: MaterialTheme 回退链ToastSpecBuilder 请求值→类型默认→M3 基准浅色回退在 KN 侧闭合,ArkUI 零回退逻辑
ToastHost 的 Popup alignment / offsetspec alignment / offsetYDpBottomCenter/-100、TopCenter/+16、Center/0
minOf(maxWidth - 32.dp, 400.dp)spec maxWidthDp数学原样,screenWidthDp 由请求携带
slideInVertically + fadeIn 300ms FastOutSlowInspec animation* + TransitionEffectcubicBezier(0.4, 0, 0.2, 1) 即 FastOutSlowIn
ToastIcons 四个 ImageVectorToastIconPaths SVG path + Path2D 填充坐标逐位转录,nonzero 与上游一致
ActionData / onAction请求 actions 数组 + 操作回调点击先入队反馈 Toast 再 dismiss,经 next 交接
DialogToastContent(Android/iOS 包装策略)needsDialogToastWrapper=true + 对话框作用域宿主ArkUI CustomDialog 同为独立窗口,保守策略同 iOS
viewModelScope.delay 自动消失ArkUI setTimeout(durationMs) → toastDismissKN 侧零线程零协程,时序等价

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 验收清单逐项过检,效果图记录了类型预设链路:

  1. 启动后 Tab4 出现 toastMeta 版本握手:version=1.0.0-ohos.1、platform=HarmonyOS、needsDialogToastWrapper=true(全部来自真实 Native 调用,非静态文字);
  2. 类型预设页输入消息、选 2000ms、BOTTOM 位置,点「成功」:绿色 Toast(#F64CAF50)从底部滑入,白色对勾图标由 Path2D 按上游矢量数据渲染,状态行显示 show id=3 type=success 2000ms 队列=0;
  3. 到时自动消失(KN 不参与计时,ArkUI setTimeout 与上游 delay 时序等价);连续点「错误」「警告」「信息」,各自品牌色与时长逐条生效;
  4. 切 TOP 位置再弹:卡片从顶部滑入且整体下移 16dp;切 BOTTOM 再弹:整体上移 100dp 避开底部按钮;
  5. 快速连点预设按钮(连发场景):第 2 条起状态行显示 queued,当前条消失后下一条自动顶上(next 交接),队列深度实时正确;
  6. 自定义样式页选深灰底/白字/none 图标 + 勾选「撤销」:卡片按自定义三色渲染、无图标、出现操作按钮;点「撤销」先弹反馈 Toast 再 dismiss 当前,反馈条经队列交接渲染;
  7. 打开深色主题开关再弹 custom:三色按 theme 覆盖值解析(深底浅字浅紫图标);
  8. Tab3「提前消失当前」「清空全部」分别触发单条 dismiss 与 toastClear,current id 与队列深度归零;
  9. 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP/CMP 适配,必须让上游模型真正编译成 ohosArm64 / ohosX64 动态库——哪怕是"下沉的内核"而不是整库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin/Native 没有 java.lang.Math:MiniJson 里顺手写的 Math.floor 在 ohos 编译期直接失败,必须用 kotlin.math.floor——commonMain 代码对 JVM 习语的每一点依赖都要过一遍 Native 语法面;
  4. 颜色解析的整数溢出:8 位 hex(如 #F64CAF50,超过 Int.MAX_VALUE)用 toIntOrNull(16) 解析会静默返回 null,必须 toLongOrNull(16) 再 toInt()。这个坑是 jvmTest 的 hex 往返用例抓住的——交叉验证的价值就在这类"编译期不报错、运行期才错"的地方;
  5. ArkUI 组件状态命名与通用属性冲突:@State position / @Prop size 会和组件基类的 .position() / .size() 撞名,必须改名(toastPosition / iconSize);
  6. Canvas.onReady 只触发一次:队列交接换内容时 iconPath 变了但画布不会重绘,需要 @Watch 监听 @Prop 变化主动重画;
  7. ClickEvent 没有 stopPropagation:Toast 卡片的 onClick 与操作按钮的 onClick 共用一片点击区,用 300ms 时间窗守卫过滤冒泡;
  8. 根构建的 /build/ 与 .idea/* 是根锚定模式,盖不住嵌套目录——DevEco 打开工程后生成的 example/harmonyApp/ 构建产物要单独放目录级 .gitignore,否则混进提交;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. 根构建全局禁用 Ohos*Test* 任务:HarmonyOS Kotlin 的 CAdapter 生成的测试任务没有可用的执行环境,留着会在 build 时炸;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,提交前注意不要把证书路径带进仓库。

8.2 已知问题

  • Compose 渲染层未上鸿蒙:ToastHost / material3 卡片 / AnimatedVisibility 留在 composeMain,当前鸿蒙视觉是 ArkUI 按 spec 复刻;等社区 compose.ui ohos 制品与上游 Compose 版本对齐后,可把 composeMain 接回 commonMain 走真渲染;
  • 图标渲染走 Canvas 2D 软填充:24dp 规格下与上游 Icon 视觉一致,超大尺寸(如 >256vp)不建议直接放大使用;
  • needsDialogToastWrapper=true 是保守策略:ArkUI CustomDialog 是独立子窗口,对话框内的 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 三条经验

  1. 路线跟着制品可达性走,不跟着历史结论走:同一个社区、同一个工具链,KStore 的正确答案是"真编译整库 + 8 行 actual",ComposeToast 的正确答案是"模型下沉 + 渲染换后端"——分水岭只在上游 commonMain 有没有一行可以脱离 androidx.compose 编译的代码;
  2. 纯 UI 库的适配面不是"补平台代码",而是"把模型从渲染里手洗出来":洗出来的每一项(默认值、偏移、坐标、回退顺序)都必须按上游原值逐位转录,任何"顺手优化"都是漂移;
  3. 交叉验证是"内核没漂"的唯一可信证明: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。

参考文档

Logo

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

更多推荐