23、Bluetooth KTX:蓝牙扫描与连接的KTX扩展,简化硬件交互
蓝牙开发,说实话,在Android原生API里一直是个「体力活」。你得写一堆回调、处理各种状态机、还要小心线程切换。我早期做IoT项目时,光蓝牙连接那部分代码就写了三百多行,后来用KTX重构,直接砍掉了一半。今天我们就聊聊怎么用KTX让蓝牙开发变得清爽。
23.1 蓝牙KTX到底解决了什么问题?
原生蓝牙API有几个痛点:
- 回调地狱:扫描结果、连接状态、数据收发,全是回调嵌套
- 生命周期管理:忘记关闭扫描或断开连接,电量哗哗掉
- 线程混乱:蓝牙操作需要在特定线程执行,新手很容易踩坑
KTX的做法很聪明——用Kotlin协程和Flow把这些异步操作包装成声明式调用。你想想看,原本要写几十行的扫描逻辑,现在几行就能搞定。
核心思想:把蓝牙设备当作数据流来处理,而不是事件源。
23.2 蓝牙扫描:从回调到Flow
先看扫描。原生写法需要注册BluetoothAdapter.LeScanCallback,然后手动管理开始和停止。我项目中曾经有个bug,就是因为扫描回调没在页面销毁时取消,导致内存泄漏。
用KTX后,扫描变成了一个Flow:
// 原生写法(约30行)
val callback = object : BluetoothAdapter.LeScanCallback {
override fun onLeScan(device: BluetoothDevice?, rssi: Int, scanRecord: ByteArray?) {
// 处理设备
}
}
bluetoothAdapter.startLeScan(callback)
// 别忘了在某个时机 stopLeScan()
// KTX写法(约5行)
bluetoothAdapter.bluetoothDevices()
.filter { it.name?.contains("MyDevice") == true }
.collect { device ->
// 处理设备
}
为什么会这样?因为KTX在底层帮你管理了startLeScan和stopLeScan的配对,并且把回调包装成了Flow。你只需要关心「拿到设备后做什么」。
我的习惯:扫描时加上超时控制。用withTimeout(10_000)包裹collect,避免无限扫描。
23.3 蓝牙连接:状态机变协程
蓝牙连接的状态变化很复杂:断开、连接中、已连接、断开中……原生API用BluetoothGattCallback来通知这些状态。我曾经维护过一个项目,里面有个全局状态变量记录连接状态,结果多线程并发读写,修了整整两天。
KTX提供了connectGatt()的协程版本:
// 原生写法
val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// 连接成功
}
}
}
device.connectGatt(context, false, gattCallback)
// KTX写法
val gatt = device.connectGatt(context, false)
gatt.establishConnection().collect { isConnected ->
if (isConnected) {
// 连接成功
}
}
注意establishConnection()返回的是一个Flow<Boolean>,它会持续发射连接状态。你可以在协程作用域内用first { it }等待首次连接成功,也可以用dropWhile { !it }跳过断开状态。
避坑指南:我曾经在establishConnection()里直接用了collect,结果协程一直挂起不退出。后来发现需要配合take(1)或first()来终止收集。记住,Flow是冷流,不取消就会一直跑。
23.4 数据读写:Service Discovery也自动化了
蓝牙通信前必须先做Service Discovery,找到设备支持的Service和Characteristic。原生写法里,你需要在onServicesDiscovered回调里继续操作。KTX把这个过程也简化了:
// KTX写法:一步到位
val gatt = device.connectGatt(context, false)
gatt.establishConnection().first { it } // 等待连接
val service = gatt.getService(UUID.fromString("your-service-uuid"))
val characteristic = service.getCharacteristic(UUID.fromString("your-char-uuid"))
// 读取
val data = gatt.readCharacteristic(characteristic)
// 写入
gatt.writeCharacteristic(characteristic, bytes)
看到没?readCharacteristic和writeCharacteristic都是挂起函数,直接返回结果。原生写法里,读取数据需要等onCharacteristicRead回调,现在变成了同步调用。
我个人觉得,这种设计特别适合「先读取配置,再开始通信」的场景。比如连接蓝牙打印机,先读设备状态,再发打印指令,逻辑清晰多了。
23.5 整体架构图
下面这张图展示了蓝牙KTX的核心流程:
23.6 实际项目中的最佳实践
说了这么多,来点实战经验。我在做智能家居App时,总结了几条蓝牙KTX的使用原则:
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 扫描附近设备 | 用bluetoothDevices() + filter |
避免手动管理扫描回调 |
| 连接已知设备 | 用establishConnection().first() |
只关心首次连接成功 |
| 持续监控连接 | 用establishConnection().collect |
实时感知断连重连 |
| 批量数据读写 | 用async并发执行 |
提高吞吐量 |
| 接收设备通知 | 用observe() + Flow |
天然支持背压和取消 |
小技巧:如果你需要同时连接多个设备,可以用coroutineScope启动多个协程,每个协程管理一个设备的连接。这样断开一个不会影响其他设备。
23.7 注意事项与避坑
嗯,这里要注意几个点:
- 权限问题:蓝牙KTX不帮你处理权限申请。记得在扫描前检查
BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限(Android 12+)。 - 主线程限制:虽然KTX用协程简化了异步,但蓝牙操作本身需要在主线程调用。KTX内部会帮你切线程,但你的协程上下文最好用
Dispatchers.Main。 - 设备兼容性:有些老设备对BLE支持不完整。我遇到过某款国产手机,
establishConnection()返回的Flow永远不会发射false,后来加了超时兜底才解决。
曾经踩过的坑:我在一个项目里同时用了bluetoothDevices()和establishConnection(),结果发现扫描到的设备对象不能直接用于连接——因为扫描返回的是BluetoothDevice的临时实例。正确的做法是通过设备地址重新获取BluetoothDevice对象,或者缓存扫描结果中的设备引用。
蓝牙KTX给我的感觉是,它把硬件交互的「仪式感」去掉了。你不再需要关心底层怎么注册回调、怎么管理状态,只需要关注业务逻辑。说白了,就是让蓝牙开发变得像网络请求一样自然。
如果你之前被原生蓝牙API折磨过,不妨试试KTX。我个人觉得,它至少能帮你省下30%的代码量,而且bug更少。嗯,这就是蓝牙KTX的核心价值。
公众号:蓝海资料掘金营,微信deep3321