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在底层帮你管理了startLeScanstopLeScan的配对,并且把回调包装成了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)

看到没?readCharacteristicwriteCharacteristic都是挂起函数,直接返回结果。原生写法里,读取数据需要等onCharacteristicRead回调,现在变成了同步调用。

我个人觉得,这种设计特别适合「先读取配置,再开始通信」的场景。比如连接蓝牙打印机,先读设备状态,再发打印指令,逻辑清晰多了。

23.5 整体架构图

下面这张图展示了蓝牙KTX的核心流程:

蓝牙KTX核心流程 蓝牙扫描 bluetoothDevices() → Flow 建立连接 establishConnection() → Flow 服务发现 自动完成 数据读写 readCharacteristic() / writeCharacteristic() 接收通知 observe() → Flow KTX带来的变化 • 回调 → Flow/挂起函数 • 手动管理 → 自动生命周期 • 状态机 → 声明式编程 • 线程切换 → 协程上下文 • 代码量减少50%以上

23.6 实际项目中的最佳实践

说了这么多,来点实战经验。我在做智能家居App时,总结了几条蓝牙KTX的使用原则:

场景 推荐做法 原因
扫描附近设备 bluetoothDevices() + filter 避免手动管理扫描回调
连接已知设备 establishConnection().first() 只关心首次连接成功
持续监控连接 establishConnection().collect 实时感知断连重连
批量数据读写 async并发执行 提高吞吐量
接收设备通知 observe() + Flow 天然支持背压和取消

小技巧:如果你需要同时连接多个设备,可以用coroutineScope启动多个协程,每个协程管理一个设备的连接。这样断开一个不会影响其他设备。

23.7 注意事项与避坑

嗯,这里要注意几个点:

  • 权限问题:蓝牙KTX不帮你处理权限申请。记得在扫描前检查BLUETOOTH_SCANBLUETOOTH_CONNECT权限(Android 12+)。
  • 主线程限制:虽然KTX用协程简化了异步,但蓝牙操作本身需要在主线程调用。KTX内部会帮你切线程,但你的协程上下文最好用Dispatchers.Main
  • 设备兼容性:有些老设备对BLE支持不完整。我遇到过某款国产手机,establishConnection()返回的Flow永远不会发射false,后来加了超时兜底才解决。

曾经踩过的坑:我在一个项目里同时用了bluetoothDevices()establishConnection(),结果发现扫描到的设备对象不能直接用于连接——因为扫描返回的是BluetoothDevice的临时实例。正确的做法是通过设备地址重新获取BluetoothDevice对象,或者缓存扫描结果中的设备引用。

蓝牙KTX给我的感觉是,它把硬件交互的「仪式感」去掉了。你不再需要关心底层怎么注册回调、怎么管理状态,只需要关注业务逻辑。说白了,就是让蓝牙开发变得像网络请求一样自然。

如果你之前被原生蓝牙API折磨过,不妨试试KTX。我个人觉得,它至少能帮你省下30%的代码量,而且bug更少。嗯,这就是蓝牙KTX的核心价值。


公众号:蓝海资料掘金营,微信deep3321