Compose Multiplatform 三方库 qrose 的 OpenHarmony 鸿蒙化适配实战全流程
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 |
| Kotlin | HarmonyOS Kotlin 2.2.21-1.0.0 |
| 插件仓库 | https://maven.eazytec-cloud.com/nexus/repository/maven-public/ |
| JDK | Temurin 21(写进 gradle.properties 的 org.gradle.java.home) |
| Gradle Wrapper | 8.14.1 |
| DevEco Studio | 26.0.0 |
| HarmonyOS SDK | 7.0.0(API 26) |
| 真机 ABI | ohosArm64 → arm64-v8a |
| 模拟器 ABI | ohosX64 → x86_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,就是这次适配的全部架构决策。


三、这个库有哪些功能
| 能力 | 上游入口 | 鸿蒙怎么验 |
|---|---|---|
| QR | rememberQrCodePainter / QRCode.encode | Tab「QR 样式」 |
| 纠错 Auto/L/M/Q/H | QrErrorCorrectionLevel | 五个 ECL 按钮 |
| 码点方/圆/圆角 | QrPixelShape | 三个码点按钮 |
| 定位球 / 定位框 | QrBallShape / QrFrameShape | 球、框各两态 |
| 中心 Logo | QrLogo | 开关,强制 High 纠错 |
| 纯色前景/背景 | QrBrush 纯色 | 五套色板 |
| DataMatrix | rememberDataMatrixPainter | Auto / Square / Rectangle |
| Aztec | rememberAztecPainter | 默认 33% 纠错 |
| 一维码 ×9 | BarcodeType | Codabar 到 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 出」。入参字段:
| 字段 | 类型 | 说明 |
|---|---|---|
kind | string | qr / datamatrix / aztec / barcode |
data | string | 要编码的原文;payload 非 text 时由 Demo 拼好再传 |
payload | string | text / wifi / tel / vcard … 决定 QrData 分支 |
ecl | string | Auto / Low / Medium / Quartile / High |
pixelShape / ballShape / frameShape | string | square / circle / round |
logo | bool | 中心开孔,桥强制纠错 High |
cell | int | 单模块像素 |
fg / bg | string | #RRGGBB |
回包字段:ok、png(Base64)、kind、width / height、payload(编码后的字符串,如 WIFI:T:WPA;…;;)、失败时 error。业务侧只需要关心 png 和 payload 两个。
五、六阶段路线图
- 查重。 CPF-KMP-CMP 已有 Vico、Reorderable、haze、qrcode-kotlin。qrose 不在其中。
- 钉工具链。 丢掉上游 Android Plugin 和 Compose 1.12,根工程只留 HarmonyOS Kotlin 2.2.21-1.0.0。
- 拆源码集。 所有
*Painter.kt和样式 DSL 挪到src/composeMain。Code128Type原本住在 Painter 文件里,编码器引用它——拆完第一下 JVM 编译就爆,只能把枚举抽回commonMain。 - 加目标。 三个模块
jvm + ohosArm64 + ohosX64,sharedLib 只放example/nativeApp。 - 光栅化 + C ABI + NAPI。 认 7×7 定位符几何,分球/框/数据区三种形状,PNG 走 zlib stored,CMake 再包一层。
- 全接口 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 的验收标准。
6.3 为什么必须再包 CMake NAPI
@CName("QroseRender") 是 ELF 导出,不是 ArkTS 导出。CAdapter 生成的 napi 表面只有 stdlib 压缩类型。Demo 按官方 native 模块来:napi_init.cpp 编 libqrose_napi.so,内部 dlopen 链 libohosqrose.so,调完 DisposeString。libc++_shared.so 必须按 triple 一起拷,否则运行时 dlopen 失败。模拟器是 x86_64,缺 x64 so 的安装错误码是 9568347,和上一篇完全一样。
七、分层验收
| 层 | 命令 / 动作 | 结果 |
|---|---|---|
| JVM 编码器 | :qrose:compileKotlinJvm 等三个模块 | BUILD SUCCESSFUL |
| ohos 链接 | linkDebugSharedOhosArm64 / X64 | libohosqrose.so 4.7MB / 4.3MB |
| so 布局 | entry/libs/<abi>/ | 每边 libohosqrose.so + libc++_shared.so |
| NAPI | hvigorw assembleHap,abiFilters 含 x86_64 | HAP 约 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.json5 的 abiFilters 必须同时写 arm64-v8a 和 x86_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 默认值的近似。挖孔之后低纠错档位扫不出来是正常现象,不是光栅化的锅。
色板。 五套配色分别是深灰、青绿、工业蓝、酒红、反色深底。反色档把前景背景对调,定位符仍然清晰。扫码器对反色码的支持不一,Demo 里放这一档主要是验 fg / bg 两个字段真的穿透到了光栅化,不是为了日常可用。
ECL 五档。 同一段短文本在 L / M / Q / H 下的模块数不同,H 最高。点五档按钮时状态行的 PNG 字节数会变,模块越多字节越大——这是肉眼能看到的「纠错换容量」。
九、矩阵码实测
Tab 二覆盖 DataMatrixEncoder 和 AztecEncoder。
DataMatrix。 短文本默认编成接近正方形的符号,状态行 生成成功 datamatrix 350x350。DataMatrix 的定位图案是两条实边 + 两条虚边,光栅化直接按矩阵画,不经过 QR 那套球/框几何。
显式选 Rectangle 之后,同一段文本被压成长条,用来模拟线缆标签这类细长场景。注意长宽比不是随便拉的,是编码器按符号规格算出来的。
Aztec。 中央是同心靶心,数据围绕靶心一圈圈铺开。Aztec 的定位做在符号内部,所以桥把 quiet 固定成 0,四周留白比 QR 小。状态行 生成成功 aztec 322x322。Aztec 默认 33% 纠错,比 QR 的 Medium 略高。
三种矩阵码的payload都是明文,扫码器不需要选类型也能读。和 QR 的差异在容错与密度:同一段文本,DataMatrix 通常模块更小、整图更紧凑;Aztec 在高纠错下膨胀最快。
十、一维码实测
Tab 三覆盖九种 BarcodeType。一维码和二维码在桥里共用一个 kind: "barcode",区别在编码器分支和画法:条空数组按宽度比例画竖条,左右安静区按 quiet * 4 像素留白,比二维码宽。
| 类型 | 样本 | 备注 |
|---|---|---|
| Codabar | A4015678B | 起止符 A–D |
| Code39 | QROSE42 | 全大写字母+数字 |
| Code93 | QROSE42 | Code39 超集 |
| Code128 | QROSE-OHOS | 自动码集切换 |
| EAN-8 | 96385074 | 7 位 + 校验位 |
| EAN-13 | 5901234123457 | 12 位 + 校验位 |
| ITF | 1234567890 | 偶数位数字 |
| UPC-A | 036000291452 | 11 位 + 校验位 |
| UPC-E | 01234565 | UPC-A 压缩 |
Code128。 样本 QROSE-OHOS,状态行 生成成功 barcode 588x70。条空宽度有 1/2/3/4 四种,切换码集时能看到宽条突然变多。
Code39。 样本 QROSE42,每个字符间有窄间隙,和 Code128 的连续条空肉眼可分。
EAN-13。 样本 5901234123457。EAN 家族的校验位由编码器算,传错位数桥会回 ok:false 和编码器异常信息,HAP 不崩——错误路径也是验收对象。
九种类型在同一个 HAP 里都能出图,上面三种之外不再重复贴图。挑这三个是因为它们分别代表「可变长度字符集」「离散字符集」「固定长度校验」三类行为,覆盖了编码器里三条不同的分支。
十一、QrData 载荷实测
Tab 四把 QrData.* 逐个跑一遍。载荷分支的意义在第十节开头说过:payload 字符串必须由 qrose 生成,鸿蒙侧手写会和 Android 漂。Demo 每点一个按钮,ArkTS 把 JSON 里的 payload 字段切过去,data 填业务参数,编码串回在状态行。
| 按钮 | 编码串(qrose 生成) |
|---|---|
| Wi-Fi | WIFI:T:WPA;S:OHOS-QROSE;P:openharmony;; |
| 企业 Wi-Fi | WIFI:T:WPA2-EAP;S:…;E:…;P:…;; |
| 电话 | TEL:13800000000 |
| 邮件 | mailto:…?subject=…&body=… |
| 短信 | SMSTO:13800000000:文本 |
| MeCard | MECARD:N:…;TEL:…;; |
| vCard | BEGIN:VCARD … END:VCARD |
| BizCard | BIZCARD:N:…;C:…;…;; |
| 地理 | GEO:31.2304,121.4737 |
| 日程 | BEGIN:VEVENT … UID:qrose-ohos |
Wi-Fi。 注意结尾有两个分号,少一个安卓侧就连不上。这种格式细节正是「不能手写」的证据。
电话。 TEL:13800000000,最短的一条分支。
vCard。 BEGIN:VCARD / END:VCARD,字段顺序与 JVM 上 qrose 1.1.2 完全一致。
十二、踩坑与 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。
十三、已知限制
- 鸿蒙 Demo 不是 Skia 矢量 Painter,是 KN 光栅化近似。无渐变 Brush,无自定义 ShapeModifier。
- PNG 未压缩。短文本可接受,超大 DataMatrix 会胖。
- 一维码非法数字由编码器抛错,桥转成 JSON
error,HAP 不崩。 - CAdapter 链接期
api.cpp:448警告仍在,so 能链。 - 上游 android / desktop / web example 未进
settings.gradle.kts,不要拿它们 sync。 - 模拟器缺 x64 的 KN so → 安装失败
9568347。缺 x64 的 NAPI so → 能安装但qroseRender无导出。 - 真机截图待补。arm64 so 已链接,HAP 也打进了
libs/arm64-v8a/。 - 反色码可扫但部分扫码器不认,属于条码行业共性,不是适配缺陷。
十四、收尾
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
更多推荐





所有评论(0)