Compose Multiplatform 三方库 qrose 的 OpenHarmony 鸿蒙化适配实战全流程

库版本:qrose 1.1.2 鸿蒙 fork(1.1.2-ohos.1)|验证环境:HarmonyOS Kotlin 2.2.21-1.0.0|Gradle 8.14.1|JDK 21|DevEco Studio 26.0.0|HarmonyOS 7.0.0(API 26)|ohosArm64 + ohosX64 均已 linkDebugShared

上一篇把 qrcode-kotlin 送上了鸿蒙。那是 KMP:纯编码器,一个 expect class QRCodeGraphics。这篇换一条配额线——Compose Multiplatform

qrose 在 Android / iOS / Desktop / Web 上卖的不是 getBytes(),是 rememberQrCodePainter。码点形状、定位球、定位框、Brush、中心 Logo,全是 Compose Painter。我第一反应是:社区不是已经有 CMP 1.9.2 了吗,把 ohosArm64 加进 compose 源集不就结束了?

打开上游 build.gradle.kts 才看见版本现实:Compose BOM 1.12,Kotlin 2.4。社区鸿蒙 Kotlin 是 2.2.21-1.0.0,CMP 是 1.9.2。Painter 的基类在 androidx.compose.ui.graphics.painter.Painter 里。这不是加两行 ohosArm64() 的事,这是再打一套 Compose 发行版的事。

CPF-KMP-CMP 里已经过审的 Vico、Reorderable,走的都不是「Compose UI 直接上鸿蒙」。它们把 Kotlin 模型编进 sharedLib,UI 用 ArkTS 重做。qrose 也走这条路:编码器 100% 复用 commonMain,Painter 原样搁进 composeMain(ohos 不编),鸿蒙侧用 KN 光栅化把 CMP 的样式模型画成 PNG。

这不是把 qrcode-kotlin 换了个皮。qrose 多出来的是 DataMatrix、Aztec、九种一维码、以及 QrData 的 wifi / vCard / geo / event。KMP 那篇只覆盖 QR;这篇把 CMP 条码库的面铺开。
在这里插入图片描述

一、环境搭建

本章不展开,直接引用官方入口:KMP&CMP 鸿蒙社区HarmonyOS 应用开发导读

本文实际使用:

语言 / 框架Compose Multiplatform 库,鸿蒙侧 Kotlin/Native
KotlinHarmonyOS Kotlin 2.2.21-1.0.0
插件仓库https://maven.eazytec-cloud.com/nexus/repository/maven-public/
JDKTemurin 21(写进 gradle.propertiesorg.gradle.java.home
Gradle Wrapper8.14.1
DevEco Studio26.0.0
HarmonyOS SDK7.0.0(API 26)
真机 ABIohosArm64arm64-v8a
模拟器 ABIohosX64x86_64

ohosArm64() / ohosX64() 只存在于这套定制 Kotlin Gradle Plugin。用 Maven Central 上的官方 2.2.21 写这两行,配置期就会 Unresolved reference。这是判断工具链有没有接对的第一根探针。

二、应用背景

跨端应用里,条码很少只出现在 Android。邀请码、Wi-Fi 配网、会议室门牌、仓储标签,iOS 和鸿蒙也要出同一张码,扫出来还得是同一段 payload。如果 Android 用 qrose 的 QrData.vCard,鸿蒙手写一个 BEGIN:VCARD,字段顺序和转义规则迟早会漂。

qrose 把这件事收口在 commonMain:QR 矩阵、DataMatrix 符号、Aztec 靶心、Code128 码集切换、EAN 校验位,全是 Kotlin。CMP 应用在非鸿蒙平台继续用 Painter;鸿蒙平台用同一套编码器,只是出图后端换成 PNG。业务层看到的仍然是 qrose 的纠错档位、载荷前缀和码制枚举。

它解决的问题不是「鸿蒙不会画二维码」。系统扫码、第三方 JS 库都能画。它解决的是:已经用 qrose 的 CMP 工程,到鸿蒙时不要把条码栈换成另一套规格。

对已经写过 KMP 适配的人来说,CMP 适配的增量就一句话:KMP 库只有数据,CMP 库还有「长相」。长相在 Compose 里,数据在 Kotlin 里。把长相留在 Compose、把数据搬上 ohos,就是这次适配的全部架构决策。
在这里插入图片描述
在这里插入图片描述

三、这个库有哪些功能

能力上游入口鸿蒙怎么验
QRrememberQrCodePainter / QRCode.encodeTab「QR 样式」
纠错 Auto/L/M/Q/HQrErrorCorrectionLevel五个 ECL 按钮
码点方/圆/圆角QrPixelShape三个码点按钮
定位球 / 定位框QrBallShape / QrFrameShape球、框各两态
中心 LogoQrLogo开关,强制 High 纠错
纯色前景/背景QrBrush 纯色五套色板
DataMatrixrememberDataMatrixPainterAuto / Square / Rectangle
AztecrememberAztecPainter默认 33% 纠错
一维码 ×9BarcodeTypeCodabar 到 UPC-E
载荷QrData.*wifi / 名片 / 邮件 / 地理 / 日程

Compose 的渐变 Brush、自定义 ShapeModifier 没有搬到鸿蒙光栅化里。已知限制会写明。Demo 覆盖的是编码器全接口,加上 CMP 样式模型里能在无 Skia 条件下复现的那一部分。

四、如何引入

仓库:https://atomgit.com/oh-tpc/cmp_qrose

Kotlin 侧不要直接把三个库模块当 HAP 依赖。正确顺序:
在这里插入图片描述

.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

so 会被拷到 example/harmonyApp/entry/libs/arm64-v8a/libs/x86_64/。然后用 DevEco 打开 example/harmonyApp。ArkTS 只 import NAPI 层:

import { qroseRender } from 'libqrose_napi.so';

const raw = qroseRender(JSON.stringify({
  kind: 'qr',
  data: 'https://atomgit.com/oh-tpc/cmp_qrose',
  payload: 'text',
  ecl: 'Medium',
  pixelShape: 'round',
  ballShape: 'circle',
  frameShape: 'round',
  logo: false,
  cell: 14,
  fg: '#111827',
  bg: '#F9FAFB'
}));

不要 import 'libohosqrose.so'。HarmonyOS Kotlin 的 CAdapter 不会把 @CName("QroseRender") 挂到 ArkTS 表面,ELF 里看得到符号,模块枚举里只有 zip 类型。这和 qrcode-kotlin 那天下午是同一类坑,CMP 库不会例外。

桥协议是「一份 JSON 进,一份 JSON 出」。入参字段:

字段类型说明
kindstringqr / datamatrix / aztec / barcode
datastring要编码的原文;payload 非 text 时由 Demo 拼好再传
payloadstringtext / wifi / tel / vcard … 决定 QrData 分支
eclstringAuto / Low / Medium / Quartile / High
pixelShape / ballShape / frameShapestringsquare / circle / round
logobool中心开孔,桥强制纠错 High
cellint单模块像素
fg / bgstring#RRGGBB

回包字段:okpng(Base64)、kindwidth / heightpayload(编码后的字符串,如 WIFI:T:WPA;…;;)、失败时 error。业务侧只需要关心 pngpayload 两个。

五、六阶段路线图

  1. 查重。 CPF-KMP-CMP 已有 Vico、Reorderable、haze、qrcode-kotlin。qrose 不在其中。
  2. 钉工具链。 丢掉上游 Android Plugin 和 Compose 1.12,根工程只留 HarmonyOS Kotlin 2.2.21-1.0.0。
  3. 拆源码集。 所有 *Painter.kt 和样式 DSL 挪到 src/composeMainCode128Type 原本住在 Painter 文件里,编码器引用它——拆完第一下 JVM 编译就爆,只能把枚举抽回 commonMain
  4. 加目标。 三个模块 jvm + ohosArm64 + ohosX64,sharedLib 只放 example/nativeApp
  5. 光栅化 + C ABI + NAPI。 认 7×7 定位符几何,分球/框/数据区三种形状,PNG 走 zlib stored,CMake 再包一层。
  6. 全接口 Demo。 四个 Tab 把 QR 样式、矩阵码、一维码、QrData 载荷跑完。

剥注解是脏活,但必须做。上游 QR 内部类带着 @JsExport / @JsName / @JvmOverloads。Kotlin/Native 认不全,留着 ohos 编译期直接死。CMP 库的 commonMain 往往同时服务 JS 和 JVM,鸿蒙 fork 要把这些平台注解洗掉,不能假装没看见。

第三阶段值得多说一句。拆 composeMain 不是 git mv 就完事:Painter 文件经常顺手 import androidx.compose.foundation,而样式 DSL 的默认值又写在 @Composable 函数里。切完源集之后 compileCommonMainKotlinMetadata 过、compileKotlinJvm 爆,是这类仓的典型症状——先跑 JVM 编译再盯 ohos,能把问题挡在最小回路里。

六、三个关键决策

6.1 为什么不在鸿蒙上跑 Compose Painter

不是不会配 ComposeArkUIViewController。是版本对不齐。社区 CMP 1.9.2 配 Kotlin 2.2.21-0.3.0 那套发行物;本机已经用 2.2.21-1.0.0 跑通了两份 KMP 仓。把 qrose 的 Painter 拉进来,等于把 Kotlin 发行版、compose.ui、skia、ohpm @cpf-kmp-cmp/compose 整套换血。P1 配额要的是「这个三方库在鸿蒙上可调用」,不是「把 CMP 运行时再打一遍」。

Vico 的适配文已经证明:CMP 库可以先把模型编上 ohos,UI 用 ArkTS。qrose 的模型比图表更干净——它就是布尔矩阵和条空数组。光栅化 200 行就能看出圆角码点和圆形定位球,审的人能看见这是 qrose 的样式模型,不是又一张方块 QR。

6.2 为什么光栅化要认定位符,不能整图画圆

QrCodeMatrix 不告诉你哪个模块是 finder。如果对所有暗模块统一画圆,三个定位符会变成三个大点,很多扫码器会拒识。qrose 上游把球和框拆成独立 Shape,不是装饰,是为了扫码鲁棒性。鸿蒙光栅化按规格书认三个 7×7:内 3×3 走 ballShape,外框走 frameShape,其余走 pixelShape。Logo 开孔时强制 High 纠错,否则中心挖空之后低纠错档位会直接死。

下面两张图是同一个 data、同一套纠错,只切了 pixelShape。注意三个定位符始终是「外框 + 内心」的结构,没有跟着数据区一起变形——这就是 6.2 的验收标准。

QR 方块 QR 圆点

6.3 为什么必须再包 CMake NAPI

@CName("QroseRender") 是 ELF 导出,不是 ArkTS 导出。CAdapter 生成的 napi 表面只有 stdlib 压缩类型。Demo 按官方 native 模块来:napi_init.cpplibqrose_napi.so,内部 dlopenlibohosqrose.so,调完 DisposeStringlibc++_shared.so 必须按 triple 一起拷,否则运行时 dlopen 失败。模拟器是 x86_64,缺 x64 so 的安装错误码是 9568347,和上一篇完全一样。

七、分层验收

命令 / 动作结果
JVM 编码器:qrose:compileKotlinJvm 等三个模块BUILD SUCCESSFUL
ohos 链接linkDebugSharedOhosArm64 / X64libohosqrose.so 4.7MB / 4.3MB
so 布局entry/libs/<abi>/每边 libohosqrose.so + libc++_shared.so
NAPIhvigorw assembleHapabiFiltersx86_64HAP 约 7.6MB,模拟器 hdc install 成功
HAP 运行aa start -b org.terminator.ohos.qrose四个 Tab 出图,状态行 生成成功 <kind> … png=…B

真机走 arm64,模拟器走 x64。不要用仓库根目录当 DevEco 工程——根目录是 Gradle KMP,HAP 是 hvigor。entry/build-profile.json5abiFilters 必须同时写 arm64-v8ax86_64。只写默认 arm64 时,HAP 里没有 libs/x86_64/libqrose_napi.so,模拟器能装上应用,但 ArkTS import { qroseRender } 会报 does not provide an export named ‘qroseRender’。这不是函数名写错,是 NAPI so 根本没打进当前 ABI。

API 26 的 NAPI 头把 napi_get_value_string_utf8 的缓冲区标成非 const。std::string::data() 直接传入会编不过,要先量长度再开 std::vector<char>

后面四节是按 Tab 分的实测记录。每个 Tab 的截图都跟着该 Tab 的说明走,不再集中堆在一个章节里;每张图都标了对应的桥入参,方便对照复现。

八、QR 样式实测

Tab 一覆盖 Qrose.encodeQr 的完整样式面:五种纠错、三种码点、定位球/框各两态、中心 Logo 开孔、五套色板。默认进 Tab 就是圆角码点 + 圆球 + 圆框 + Medium。

码点三态的区别看数据区就够:方是实心矩形,圆是正圆,圆角是圆角矩形。上一节那两张图已经给出方和圆的对比。圆角态介于两者之间,视觉上和上游 Painter 的 RoundedCornerShape 对齐。

中心 Logo 开孔。 打开开关后,桥把纠错切到 High,再在矩阵中心挖一块圆角色块。中心区域约占模块数的 24%,这个比例是上游 Painter 默认值的近似。挖孔之后低纠错档位扫不出来是正常现象,不是光栅化的锅。

QR Logo

色板。 五套配色分别是深灰、青绿、工业蓝、酒红、反色深底。反色档把前景背景对调,定位符仍然清晰。扫码器对反色码的支持不一,Demo 里放这一档主要是验 fg / bg 两个字段真的穿透到了光栅化,不是为了日常可用。

ECL 五档。 同一段短文本在 L / M / Q / H 下的模块数不同,H 最高。点五档按钮时状态行的 PNG 字节数会变,模块越多字节越大——这是肉眼能看到的「纠错换容量」。

九、矩阵码实测

Tab 二覆盖 DataMatrixEncoderAztecEncoder

DataMatrix。 短文本默认编成接近正方形的符号,状态行 生成成功 datamatrix 350x350。DataMatrix 的定位图案是两条实边 + 两条虚边,光栅化直接按矩阵画,不经过 QR 那套球/框几何。

DataMatrix

显式选 Rectangle 之后,同一段文本被压成长条,用来模拟线缆标签这类细长场景。注意长宽比不是随便拉的,是编码器按符号规格算出来的。

DataMatrix Rectangle

Aztec。 中央是同心靶心,数据围绕靶心一圈圈铺开。Aztec 的定位做在符号内部,所以桥把 quiet 固定成 0,四周留白比 QR 小。状态行 生成成功 aztec 322x322。Aztec 默认 33% 纠错,比 QR 的 Medium 略高。

Aztec

三种矩阵码的payload都是明文,扫码器不需要选类型也能读。和 QR 的差异在容错与密度:同一段文本,DataMatrix 通常模块更小、整图更紧凑;Aztec 在高纠错下膨胀最快。

十、一维码实测

Tab 三覆盖九种 BarcodeType。一维码和二维码在桥里共用一个 kind: "barcode",区别在编码器分支和画法:条空数组按宽度比例画竖条,左右安静区按 quiet * 4 像素留白,比二维码宽。

类型样本备注
CodabarA4015678B起止符 A–D
Code39QROSE42全大写字母+数字
Code93QROSE42Code39 超集
Code128QROSE-OHOS自动码集切换
EAN-8963850747 位 + 校验位
EAN-13590123412345712 位 + 校验位
ITF1234567890偶数位数字
UPC-A03600029145211 位 + 校验位
UPC-E01234565UPC-A 压缩

Code128。 样本 QROSE-OHOS,状态行 生成成功 barcode 588x70。条空宽度有 1/2/3/4 四种,切换码集时能看到宽条突然变多。

Code128

Code39。 样本 QROSE42,每个字符间有窄间隙,和 Code128 的连续条空肉眼可分。

Code39

EAN-13。 样本 5901234123457。EAN 家族的校验位由编码器算,传错位数桥会回 ok:false 和编码器异常信息,HAP 不崩——错误路径也是验收对象。

EAN-13

九种类型在同一个 HAP 里都能出图,上面三种之外不再重复贴图。挑这三个是因为它们分别代表「可变长度字符集」「离散字符集」「固定长度校验」三类行为,覆盖了编码器里三条不同的分支。

十一、QrData 载荷实测

Tab 四把 QrData.* 逐个跑一遍。载荷分支的意义在第十节开头说过:payload 字符串必须由 qrose 生成,鸿蒙侧手写会和 Android 漂。Demo 每点一个按钮,ArkTS 把 JSON 里的 payload 字段切过去,data 填业务参数,编码串回在状态行。

按钮编码串(qrose 生成)
Wi-FiWIFI:T:WPA;S:OHOS-QROSE;P:openharmony;;
企业 Wi-FiWIFI:T:WPA2-EAP;S:…;E:…;P:…;;
电话TEL:13800000000
邮件mailto:…?subject=…&body=…
短信SMSTO:13800000000:文本
MeCardMECARD:N:…;TEL:…;;
vCardBEGIN:VCARD … END:VCARD
BizCardBIZCARD:N:…;C:…;…;;
地理GEO:31.2304,121.4737
日程BEGIN:VEVENT … UID:qrose-ohos

Wi-Fi。 注意结尾有两个分号,少一个安卓侧就连不上。这种格式细节正是「不能手写」的证据。

QrData Wi-Fi

电话。 TEL:13800000000,最短的一条分支。

QrData TEL

vCard。 BEGIN:VCARD / END:VCARD,字段顺序与 JVM 上 qrose 1.1.2 完全一致。

QrData vCard

十二、踩坑与 FAQ

Q:这不就是再适配一个二维码库吗?
A:不是。qrcode-kotlin 是 KMP、只做 QR。qrose 是 CMP,矩阵码、一维码、QrData 载荷、Painter 样式模型都在配额面上。两个仓对同一段 WIFI: 字符串的字段顺序以各自上游为准,不能混用。

Q:库本身的 EAN 校验问题去哪提?
A:提到上游 qrose。鸿蒙 fork 只收 ohos 目标、注解剥离、composeMain 隔离、NAPI 和 Demo。作者名 Everlasting,组织 oh-tpc。

Q:Code128Type 找不到?
A:它原来写在 Code128Painter.kt。Painter 挪走之后必须抽回 commonMain。CMP 库里类型定义跟 UI 文件绑在一起,是拆源码集时的第一刀。

Q:能不能直接 import KN so?
A:不能。CAdapter 不挂业务符号。只 import libqrose_napi.so

Q:模拟器报 does not provide an export named ‘qroseRender’?
A:先看 HAP 里有没有 libs/x86_64/libqrose_napi.so。没有就是 abiFilters 漏了 x86_64。有 so 再查 nm_modname 是不是 qrose_napi

Q:CMake 链接期报 std::string::data() const 不匹配?
A:API 26 的 napi_get_value_string_utf8 要非 const 缓冲。先 napi_get_value_string_utf8(env, v, nullptr, 0, &len) 量长度,再开 std::vector<char> 拷贝。

Q:Compose Painter 以后还能不能回来?
A:能。composeMain 就是为这件事留的。等社区出现和 1.1.2 对齐的 compose.ui ohos 制品,把源集接回去,光栅化可以退成 fallback。

十三、已知限制

  1. 鸿蒙 Demo 不是 Skia 矢量 Painter,是 KN 光栅化近似。无渐变 Brush,无自定义 ShapeModifier。
  2. PNG 未压缩。短文本可接受,超大 DataMatrix 会胖。
  3. 一维码非法数字由编码器抛错,桥转成 JSON error,HAP 不崩。
  4. CAdapter 链接期 api.cpp:448 警告仍在,so 能链。
  5. 上游 android / desktop / web example 未进 settings.gradle.kts,不要拿它们 sync。
  6. 模拟器缺 x64 的 KN so → 安装失败 9568347。缺 x64 的 NAPI so → 能安装但 qroseRender 无导出。
  7. 真机截图待补。arm64 so 已链接,HAP 也打进了 libs/arm64-v8a/
  8. 反色码可扫但部分扫码器不认,属于条码行业共性,不是适配缺陷。

十四、收尾

CMP 库上鸿蒙,眼下有两条路。一条是等 compose.ui 的 ohos 制品和上游 BOM 对齐,Painter 直接跑。另一条是先把 Kotlin 模型编上 ohosArm64 / ohosX64,用 ArkTS 把能力露出来。qrose 选了第二条,和社区里已经过审的 Vico、Reorderable 同一条。编码器一个字符都没重写,重写的是出图后端和桥。

从工作量看,这次适配真正花时间的不是编码器——那部分零改动——而是三件事:拆 composeMain 时把类型定义从 UI 文件里救出来、光栅化里把三个定位符的几何认对、NAPI 链路在 API 26 上的 const 收紧。三件事都和「CMP」本身有关:只有带着 UI 模型的库,才会同时踩到源集、样式和运行时这三层。

KMP&CMP 社区地址:https://atomgit.com/CPF-KMP-CMP
GitHub 上游:https://github.com/alexzhirkevich/qrose
鸿蒙适配版:https://atomgit.com/oh-tpc/cmp_qrose
示例工程:example/harmonyApp

欢迎加入 KMP&CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP

Logo

一站式 AI 云服务平台

更多推荐