Kotlin Multiplatform for OpenHarmony 实战:为 kable 实现 OpenHarmony 蓝牙低功耗(BLE)引擎

大家好,我是熊猫钓鱼!欢迎大家和我一起探讨技术。希望您能点赞关注,谢谢!
摘要
本文是「Kotlin Multiplatform 三方库鸿蒙化适配」系列的第四篇,围绕 kable(Juul Labs 出品的 Kotlin Multiplatform 低功耗蓝牙库,Apache-2.0)在 OpenHarmony 上的适配展开。kable 把 BLE 塑造成了一套响应式流模型:扫描是一个流、连接状态是一个流、特征变更是一个流。作者选择路线 A——用 ArkTS 实现 BleScanner / BlePeripheral 接口,桥接系统 @kit.ConnectivityKit 的 ble 模块,而不是等待上游支持 ohosArm64 或自绘一套蓝牙协议。适配沿用系列一致的语义层 / 引擎层 / 验收页三层架构;其中最大的挑战,是鸿蒙 BLE 是回调式的(扫描结果推事件、连接状态推事件、特征变更推事件),和 kable「一切皆 Flow」的模型正好错位——作者用「语义层管理器统一派发事件」把回调翻译成了响应式。文中逐一记录 6 个基于 SDK 真实 .d.ts 签名的 ArkTS 踩坑,并最终在 HarmonyOS SDK 6.0.0(20)(API 20) 下零错误编译出包(HAP 约 621 KB)。文末把 Decompose、Ktor、Notifier、kable 四篇并置,把这套系列收口成一个判断:适配的本质,是替平台之间对不齐的模型,写一层翻译。
目录
- 一个和前三篇都不太一样的起点(路线取舍)
- 第一层:语义层,先把 kable 的契约画对
- 第二层:引擎层,真正把 BLE 接到系统
- 那道绕不过的弯:回调式 BLE 怎么变成响应式
- 第三层:验收页,能扫能连能读写
- 那些只有踩过才知道的坑(6 个 ArkTS 真签名坑)
- 怎么知道它真的活了(编译 + 验证表)
- 和四篇连起来看,是四种不同的「适配」
- 三条我现在认准的判断
- 工程信息(工具链 / 版本 / 仓库 / 社区)
写完 Decompose、Ktor、Notifier 三篇,我本来以为「适配」这件事的方法论已经收得差不多了——
状态管理复刻、真接口真实现、系统能力桥接,三板斧轮着用。直到动手做 kable,我才发现蓝牙这一篇,把前三篇里那个「平台模型错位」的命题推到了最极端。
kable(io.github.juul.kable,Apache-2.0)做的是 Kotlin Multiplatform 的 BLE。它的核心体验是响应式的:你拿到一个 Scanner,scanner.peripherals 是个 Flow,设备进来就进流;你拿到一个 Peripheral,connect()、discoverServices()、读特征、写特征、订阅特征变更,背后都是协程 Flow 在驱动。它在 Kotlin 侧把「蓝牙这种天生异步、事件驱动的东西」包装得很顺滑。
我这次的目标,就是让这套响应式模型在鸿蒙上长出真东西。我觉得还是很有意义的。
一个和前三篇都不太一样的起点
动手前我还是先想清楚路线。摆在面前的选择其实就两条:
| 路线 | 做法 | 为什么没选 / 选了 |
|---|---|---|
| 路线 B:等上游给 ohosArm64 的 expect/actual | kable 上游自己支持鸿蒙,我坐等 | 上游目前没有 ohos 目标;而且 BLE 本质就是要调系统蓝牙栈,绕不开鸿蒙这层 |
| 路线 B’:自己实现一份 BLE 协议栈 | 不碰系统 API,纯 ArkTS 写 GAP/GATT | 那是重写一个蓝牙协议栈,工作量不可控,而且系统已经做得够好 |
| 路线 A:ArkTS 实现 Ble 引擎,桥接 @kit.ConnectivityKit(选) | 真接系统 BLE 能力,回调翻译成响应式 | 语义同构,能立刻跑,验证直观 |
选 A 我心里是踏实的,理由和 Ktor、Notifier 一样:系统已经把蓝牙做得足够好,我没必要在适配阶段自己造轮子。但 kable 比前两篇更考验的一点是——它的「模型」和鸿蒙的「模型」长得最不像。
Ktor 的「请求-响应」和鸿蒙 @ohos.net.http 的「请求-响应」是同构的;Notifier 的「发一条通知」和 @kit.NotificationKit 的「发一条通知」是同构的(只是点击回灌那一下错位)。可 kable 的「一切皆 Flow」,碰到鸿蒙 BLE 的「一切皆回调」,中间隔着的不是一层薄翻译,而是一整个事件模型的重写。
kable 作用与代码描述
kable 是 Juul Labs 开源的 Kotlin Multiplatform 低功耗蓝牙(BLE)库,它以响应式的编程模型统一封装了蓝牙外设的扫描、连接、服务发现、特征读写与变更订阅等能力,让一套 KMP 代码就能在 Android、iOS、桌面与鸿蒙等平台复用同一套蓝牙交互逻辑。在 OpenHarmony 上,由于系统蓝牙能力集中在 @kit.ConnectivityKit 的 ble 模块且必须以回调方式驱动,本适配沿用「路线 A:真接口真实现」的思路,用 ArkTS 在鸿蒙侧等价实现 kable 的 BleScanner / BlePeripheral 契约,把系统的回调式 BLE 事件翻译成语义层的响应式派发,从而让上层 KMP 业务无需感知平台差异即可跑通完整蓝牙链路。
代码采用三层架构组织:ble/Ble.ets 为语义层,定义 BleScanResult、BleGattService、BleCharacteristic、BleConnectionState、BleScanner、BlePeripheral 与全局管理器 KableBle,纯逻辑、不引入任何 @kit.*;bridge/OhosBle.ets 为引擎层,通过 OhosBleScanner 与 OhosBlePeripheral 桥接 ble.startBLEScan、createGattClientDevice、connect、getServices、readCharacteristicValue、writeCharacteristicValue、setCharacteristicChangeNotification 等系统 API,完成回调到响应式模型的转换;pages/KableDemo.ets 为验收页,按「扫描列表→连接→发现服务→读/写/订阅特征」的流程展示能力并输出事件日志,另含「模拟演示(无需蓝牙)」按钮,可在无蓝牙芯片的模拟器上直接走通扫描→连接→发现→读取全流程,验证适配链路的正确性。
开发代码界面如下:

不得不说,Dev Eco 26最新版比之前功能更加丰富好用了,看我写的demo都智能化标识了代码。

第一层:语义层,先把 kable 的契约画对
和前三篇一样,我第一步没写引擎,而是先写了个不碰任何 @kit.* 的语义层 Ble.ets。它把 kable 的契约原样画一遍:
/** 一次扫描发现的设备 */
export class BleScanResult {
readonly id: string;
name: string;
rssi: number;
connectable: boolean;
rawData: ArrayBuffer;
constructor(id: string, name: string, rssi: number, connectable: boolean, rawData: ArrayBuffer) { /* ... */ }
}
/** 一个 GATT 特征(value 用 ArrayBuffer,与系统侧一致) */
export class BleCharacteristic {
serviceUuid: string;
characteristicUuid: string;
value: ArrayBuffer;
constructor(serviceUuid: string, characteristicUuid: string, value: ArrayBuffer) { /* ... */ }
}
/** 一个 GATT 服务 */
export class BleGattService {
serviceUuid: string;
isPrimary: boolean;
characteristics: BleCharacteristic[];
constructor(serviceUuid: string, isPrimary: boolean, characteristics: BleCharacteristic[]) { /* ... */ }
}
/** 连接状态(对应系统 ProfileConnectionState) */
export enum BleConnectionState {
DISCONNECTED = 0, CONNECTING = 1, CONNECTED = 2, DISCONNECTING = 3
}
/** 扫描器 / 外设:外部只面对这两个接口 */
export interface BleScanner { start(filterName?: string): void; stop(): void; }
export interface BlePeripheral {
readonly id: string;
connect(): Promise<void>;
disconnect(): void;
discoverServices(): Promise<BleGattService[]>;
read(characteristic: BleCharacteristic): Promise<BleCharacteristic>;
write(characteristic: BleCharacteristic, value: ArrayBuffer, withResponse: boolean): Promise<void>;
setNotify(characteristic: BleCharacteristic, enable: boolean): Promise<void>;
}
这一层一共 162 行,它不 import 任何平台 API。kable 最值钱的不是它的平台实现,是这套「扫描 / 连接 / 服务 / 特征 / 变更」的与平台无关的模型。只要模型画对了,底层换成 @kit.ConnectivityKit 就是顺理成章的事。
第二层:引擎层,真正把 BLE 接到系统
骨架画好,引擎层 OhosBle.ets 把语义层的接口接到系统 ble 模块上。核心逻辑长这样(已按 SDK 真实签名校正):
import { ble, constant } from '@kit.ConnectivityKit';
/** 扫描器:桥接 ble.startBLEScan / ble.on('BLEDeviceFind') */
export class OhosBleScanner implements BleScanner {
private readonly onFound: (results: Array<ble.ScanResult>) => void;
constructor() {
this.onFound = (results: Array<ble.ScanResult>): void => {
for (const r of results) {
const mapped: BleScanResult = new BleScanResult(r.deviceId, r.deviceName, r.rssi, r.connectable, r.data);
KableBle.dispatchScanResult(mapped); // 翻译:回调 → 语义层派发
}
};
}
start(filterName?: string): void {
if (this.scanning) { return; }
const filters: Array<ble.ScanFilter> = [];
if (filterName !== undefined && filterName.length > 0) { filters.push({ name: filterName }); }
ble.on('BLEDeviceFind', this.onFound);
ble.startBLEScan(filters);
this.scanning = true;
}
stop(): void { /* ble.off(...) + ble.stopBLEScan() */ }
}
/** 外设:桥接 GattClientDevice 的 connect / discoverServices / 读写 / 订阅 */
export class OhosBlePeripheral implements BlePeripheral {
readonly id: string;
private readonly device: ble.GattClientDevice;
constructor(id: string) {
this.id = id;
this.device = ble.createGattClientDevice(id);
// 特征变更是设备级事件,构造时挂一次,按 uuid 过滤后派发
this.device.on('BLECharacteristicChange', (changed: ble.BLECharacteristic): void => {
KableBle.dispatchCharacteristicChanged(new BleCharacteristic(changed.serviceUuid, changed.characteristicUuid, changed.characteristicValue));
});
}
connect(): Promise<void> {
return new Promise<void>((resolve, reject): void => {
const onState: (change: ble.BLEConnectionChangeState) => void = (change): void => {
if (change.deviceId !== this.id) { return; }
if (change.state === constant.ProfileConnectionState.STATE_CONNECTED) {
this.device.off('BLEConnectionStateChange', onState);
KableBle.dispatchConnectionState(BleConnectionState.CONNECTED, this.id);
resolve();
} else if (change.state === constant.ProfileConnectionState.STATE_DISCONNECTED) {
this.device.off('BLEConnectionStateChange', onState);
KableBle.dispatchConnectionState(BleConnectionState.DISCONNECTED, this.id);
reject(new BleException('BLE 连接失败或已断开', change.state));
}
};
this.device.on('BLEConnectionStateChange', onState);
this.device.connect();
});
}
async discoverServices(): Promise<BleGattService[]> {
const list: Array<ble.GattService> = await this.device.getServices();
const mapped: BleGattService[] = [];
for (const s of list) {
const chars: BleCharacteristic[] = [];
for (const c of s.characteristics) { chars.push(new BleCharacteristic(c.serviceUuid, c.characteristicUuid, c.characteristicValue)); }
mapped.push(new BleGattService(s.serviceUuid, s.isPrimary, chars));
}
return mapped;
}
async write(characteristic: BleCharacteristic, value: ArrayBuffer, withResponse: boolean): Promise<void> {
const target: ble.BLECharacteristic = this.findSystemChar(characteristic);
target.characteristicValue = value;
const writeType: ble.GattWriteType = withResponse ? ble.GattWriteType.WRITE : ble.GattWriteType.WRITE_NO_RESPONSE;
await this.device.writeCharacteristicValue(target, writeType);
}
// read / setNotify 同理,都是「系统调用 + 语义层派发」的薄翻译
}
你看 createGattClientDevice / connect / getServices / readCharacteristicValue / writeCharacteristicValue / setCharacteristicChangeNotification 这几个调用,名字和 Android 的 BluetoothGatt 几乎是镜像的。这就是我开头说的「最舒服的适配」——契约对得上,系统能力也对得上,中间只隔一层很薄的翻译。
那道绕不过的弯:回调式 BLE 怎么变成响应式
但「最舒服」是错觉,因为 kable 和鸿蒙之间最大的不同,正好戳在 kable 最金贵的能力上。
kable 把扫描、连接、特征变更都做成了 Flow——你订阅流,事件自己来。鸿蒙 BLE 不是。ble.startBLEScan 之后,设备是通过 ble.on('BLEDeviceFind', callback) 一个一个推给你的;连接状态是通过 GattClientDevice.on('BLEConnectionStateChange', callback) 推给你的;特征变更是通过 on('BLECharacteristicChange', callback) 推给你的。三个独立的回调入口,没有任何「流」的概念。
换句话说,kable 的「响应式体验」,在鸿蒙上得靠我这一层 KableBle 管理器来重建。这就是我说的「绕一道弯」,也是这一篇比前两篇更考验平台判断的地方——你得先想清楚:kable 的 Flow,在鸿蒙的世界里,本质上是一堆散落的回调,你得把它们收口到一个派发中心,再让页面去订阅。
我是这么接的:
- 扫描器订阅
ble.on('BLEDeviceFind'),每来一批结果,逐条KableBle.dispatchScanResult(...)。 - 外设构造时订阅
on('BLECharacteristicChange'),变更来了KableBle.dispatchCharacteristicChanged(...)。 - 连接时订阅
on('BLEConnectionStateChange'),状态变了KableBle.dispatchConnectionState(...),并在 CONNECTED 时 resolve 那个Promise<void>。 - 页面只跟
KableBle打交道:注册一个BleListener,扫描结果、连接状态、特征变更就从三个回调入口,统一回流到页面的日志卡。
写这一段的时候我有点感慨:kable 把「蓝牙这种异步事件」抽象成两条干净的流,到鸿蒙上,这两条流被拆成了一圈 on/off 事件订阅 + 一个派发中心 + 一堆 uuid 过滤。抽象没变,但「抽象落到平台」的那一步,辛苦程度完全不一样。适配的含金量,往往就藏在这种「抽象和平台对不齐」的缝隙里。
(顺带说一个已知的小遗憾:连接我用了 Promise<void> + 内部超时靠系统状态事件,没有设硬超时;如果设备不在范围内,connect() 可能迟迟不 resolve。对于 demo 主路径——设备在线、走 CONNECTED——这件事是稳的。要彻底解决,可以给 connect() 加一个 setTimeout 兜底 reject。这篇先不展开,留个口子。)
第三层:验收页,能扫能连能读写
为了让这个引擎「看得见摸得着」,我写了 KableDemo.ets 验收页,和 Ktor / Notifier 页平级(首页点按钮切换)。这一页要证明四件事:自研 BleScanner 真能调 @kit.ConnectivityKit 扫到设备;点列表项能 connect + discoverServices,把 GATT 树拉出来;read / write / setNotify 真能打到系统 BLE;连接状态变化、特征变更能回灌到页面日志。
「换引擎」同样只要一行:
aboutToAppear(): void {
KableBle.setScanner(new OhosBleScanner()); // 这一行就是「换引擎」
this.listener = new DemoListener(/* 三个回调:扫描/连接/变更 → 写进日志卡 */);
KableBle.addListener(this.listener);
}
DemoListener 是用一个具体 class 实现 BleListener 的——原因下面「ArkTS 坑」里会讲,直接把对象字面量赋给这个接口在 ArkTS 下会踩红线。
通过编译:

部署:

执行效果如下:

搜索匹配运行测试:

那些只有踩过才知道的坑
这一节留给想照着做的朋友。下面每一个,都是我在 hvigor 的红字里一个个认出来的,没有一个是我提前知道的。
BLECharacteristic 的对象字面量缺 descriptors 字段会编译失败。 我在 findSystemChar 的兜底分支写了 { serviceUuid, characteristicUuid, characteristicValue },编译器当场不认:Property 'descriptors' is missing。去翻 SDK 的 ble.d.ts 才发现,BLECharacteristic 里 descriptors: Array<BLEDescriptor> 是必填字段(不像 properties / permissions 那样带 ?)。兜底对象补上 descriptors: [] 就过了。一句话:系统类型里看似可选的东西,未必真的可选,以 .d.ts 为准。
模块级函数不能用 this. 调。 我把 bufToHex(ArrayBuffer 转十六进制,纯工具函数)写在文件顶部模块作用域,页面里却写成 this.bufToHex(c.value),编译器报 Property 'bufToHex' does not exist on type 'KableDemo'。ArkTS 里模块函数不走 this,直接 bufToHex(...) 调即可。这个坑小,但第一次见挺懵。
List 组件没有 maxHeight。 我想给扫描列表限个高,写了 .layoutWeight(1).maxHeight(180),编译器说 Property 'maxHeight' does not exist on type 'ListAttribute'。(低版本 ArkUI 的 List 确实没暴露 maxHeight,用 height(180) 固定高度即可;List 放在 Scroll 里,固定高度比 maxHeight 更稳妥。)
Row 组件没有 wrapContent。 我给按钮行写了 .width('100%').wrapContent(),编译器照样不认。ArkUI 的 Row 没有 wrapContent 这个属性——要么用 Flex 容器开 wrap,要么直接 .width('100%') 让按钮按比例排。我选了后者。
对象字面量不能直接赋给含方法签名的接口。 这是最让我意外的一个,和 Notifier 那篇一模一样。我本来写 const listener: BleListener = { onScanResult: (r) => {...}, ... },编译器报 Object literal must correspond to some explicitly declared class or interface。原因大概是 BleListener 的方法参数用到了具体类型,ArkTS 的 arkts-no-untyped-obj-literals 规则对这个组合特别敏感。解决办法是用一个具体 class 实现接口(像上面的 DemoListener),通过构造函数把闭包注进去——class 实现接口不受这条字面量规则约束。
BLE 扫描取设备名,运行时往往还要位置权限。 编译期 ble.startBLEScan 只声明 ohos.permission.ACCESS_BLUETOOTH,我在 module.json5 里也只加了这一个。但真机上,Android/iOS 那套「扫 BLE 拿设备名需要位置权限」的规则在鸿蒙上同样存在——不授予位置权限,deviceName 常常是空的。这件事我如实记在这里,没藏着:demo 已声明 ACCESS_BLUETOOTH,真机验证时若设备名为空,先去查位置权限。
怎么知道它真的活了
验证分两层,和前三篇一致。
第一层是编译。我在 HarmonyOS SDK 6.0.0(20)(API 20)下对工程做了编译验证,BUILD SUCCESSFUL,零 ArkTS error(仅余若干 router.back 废弃告警与「函数可能抛异常」提示,属已知噪音)。产出的 entry-default-unsigned.hap 约 621 KB,未签名,模拟器直接能装。这一版 HAP 比 Notifier 那篇的 529 KB 又大了一些,因为多了 BLE 这层适配代码,符合预期。
第二层才是真刀真枪:DevEco 里起真机或开发板(模拟器没有蓝牙芯片,跑不了 BLE),装好 HAP,首页点「KMP kable 蓝牙适配 Demo(BLE 扫描/连接/GATT)→」,进去点按钮。各按钮的预期我列一下,方便你对着查:
| 按钮 | 你该看到什么 |
|---|---|
| 开始扫描 | 日志卡出现 scan 开始扫描;附近 BLE 设备出现在列表(需真机+开蓝牙+位置权限) |
| 点列表里的设备 | 状态变「连接中」,连上后日志 conn 已连接;下方出现该设备的服务树 |
| 点一个特征 | 该特征标「✔已选」 |
| 读特征 | 日志 read 带 uuid 和十六进制特征值 |
| 写特征(KMP) | 日志 write 带 uuid(往特征写 ASCII “KMP”) |
| 订阅通知 | 日志 notify 已订阅;外设主动发数时,日志 changed 带新值 |
| 断开 | 日志 conn 已断开 |
说句心里话,当我在真机上点开扫描、看着列表一条条冒出来、点连接再看到 GATT 树展开的时候,那种「它真的和系统蓝牙栈通了」的踏实感,和当初 Ktor 点出第一个 200、Notifier 点开通知栏是同一种。只不过这次,我还得多确认一件事:扫描的回调、连接的回调、特征的回调,能原路回到我的派发中心、再回到页面监听者——那才是 kable 的魂。
和四篇连起来看,是四种不同的「适配」
写到这里,我想把 Decompose、Ktor、Notifier、kable 四篇连起来,因为我觉得这才是整个系列最值得讲清楚的一点。
| 维度 | Decompose | Ktor | KMPNotifier | kable |
|---|---|---|---|---|
| 本质 | 纯状态管理 | 真实网络能力 | 真实系统能力(通知) | 真实系统能力(BLE) |
| 鸿蒙上怎么做的 | ArkTS 等价复刻 | ArkTS 真实现 HttpClientEngine | ArkTS 真实现 LocalNotifier | ArkTS 真实现 Ble 引擎 |
| 最难的弯 | 组件树根必须在宿主创建一次 | DNS/TLS 交给系统(别硬刚) | 点击/动作不能直接回调,WantAgent 回灌 | 回调式 BLE 没有 Flow,自己翻译成派发 |
.so 就绪后 | 替换桥接层 | 替换引擎层 | 替换引擎层 | 替换引擎层(换 Kotlin 侧实现) |
| 验证难度 | 相对容易(无外部依赖) | 难(真联网、权限、DNS/TLS 要通) | 中(不用联网,权限+回灌要通) | 中(需真机/开发板,无芯片模拟器跑不了) |
Decompose 考验耐心(把状态机重写一遍),Ktor 考验你对网络边界的判断(别去造 TLS),Notifier 考验你对「事件模型」落地的判断(Android/iOS 的回调在鸿蒙上要翻译成 Ability 启动),kable 考验你对「响应式模型」落地的判断(kable 的 Flow,在鸿蒙上得翻译成一个派发中心)。四篇下来,我越来越确信一句话:适配的难,从来不在「翻译 API」,而在「翻译那些平台之间对不齐的模型」。
三条我现在认准的判断
折腾完这一圈,有四件事我想得很清楚:
先看抽象层,再决定写什么。 kable 把「BLE」抽成了 Scanner / Peripheral 这两个接口加一套事件模型,所以我的适配,说到底是「实现接口 + 重建事件派发」。抽象画在哪儿,工作量就在哪儿。
能复用平台能力,就别硬刚平台短板。 蓝牙直接交给 @kit.ConnectivityKit,TLS 那种「Ktor 式」的坑这里根本没有;唯一要自己补的,是把回调翻译成响应式——而这恰恰是无法交给系统的,必须自己接。
事件模型对不齐的时候,别试图强行 1:1 映射。 kable 的「Flow」在鸿蒙上硬要找一个等价物(比如某个内置流)是找不到的。与其拧巴,不如承认「BLE 就是回调式」这个事实,把派发做成一个清晰的 KableBle 单例。承认平台差异,反而写得最顺。
四篇收口:适配 = 替对不齐的模型写翻译。 Decompose 翻译「组件树」,Ktor 翻译「网络边界」,Notifier 翻译「点击回灌」,kable 翻译「回调→响应式」。它们表面是四个库,底层是同一件事——在 Kotlin 的抽象和鸿蒙的系统能力之间,补一层你自己的翻译层。
工程信息
- Demo 工程:
E:\huawei\hongmengdev\demo(在 Decompose / Ktor / Notifier demo 基础上新增 kable 三文件 + 首页入口,复用同一工程,不破坏原有功能) - 新增代码:约 634 行 ArkTS(
Ble.ets162 +OhosBle.ets157 +KableDemo.ets315),另在module.json5增加ohos.permission.ACCESS_BLUETOOTH权限声明与字符串资源 - 工具链:DevEco Studio 26.0.0 Release · HarmonyOS SDK
6.0.0(20)(API 20)· KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2) - 目标设备:HarmonyOS 手机 ROM 6.1+(BLE 需真机/开发板,模拟器无蓝牙芯片;DevEco 模拟器可装包但跑不了扫描)
- 适配路线:路线 A(自研
Ble引擎桥接@kit.ConnectivityKit的ble模块,回调式翻译成语义层派发) - 关键权限:
ohos.permission.ACCESS_BLUETOOTH(扫描取设备名真机常还需位置权限) - 适配后仓库:https://atomgit.com/wdracky/kmp-ohos-adapters/tree/main/kable
- KMP/CMP 鸿蒙化社区:https://atomgit.com/CPF-KMP-CMP
欢迎加入KMP&CMP 鸿蒙社区: https://atomgit.com/CPF-KMP-CMP
推荐 AtomCode: https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths
顺手说一句:到这篇,KMP 三方库鸿蒙化适配系列的第一阶段(Decompose / Ktor / Notifier / kable 四篇)就算收口了。每一篇走的都是「语义层不碰平台、引擎层桥接系统、验收页真机验证」的三层架构,区别只在平台模型错位的那道弯长什么样。
后续若要往下走,最值得补的反而是前面几篇都提到的「冷启/超时兜底」——把没接住的事件先缓存,等页面就绪再补派。那是把 demo 级适配推向生产级适配的最后一公里。
更多推荐


所有评论(0)