简介:本资源是一套面向Android开发者与移动通信学习者的Kotlin蓝牙开发实战示例,聚焦短距离无线通信场景,解决蓝牙设备发现、配对、数据传输等核心功能的工程化实现问题,适用于具备基础Android开发能力的学习者进阶实践。压缩包共326个文件,总大小24.36MB,涵盖62个Kotlin源文件(含协程与扩展函数等现代语法实践)、34个Java文件(保障Java/Kotlin混合项目兼容性)、133个XML布局与配置文件(支撑UI与Manifest定义)、17个AAR库(含多个版本ppblutoothkit蓝牙SDK,体现迭代适配过程)以及14个SO本地库(支撑底层蓝牙协议栈调用)。已有423人学习下载,资源结构清晰、模块完整,提供从权限申请、扫描连接到数据收发的全链路代码参考,并附带Gradle构建配置、Markdown说明文档及多分辨率PNG资源,便于快速理解架构设计与移植集成。
1. 项目缘起:为什么我们需要一个Kotlin蓝牙库示例?
如果你是一名Android开发者,最近在项目中需要集成蓝牙功能,尤其是低功耗蓝牙(BLE),你大概率会和我有同样的感受:官方文档的示例代码要么是Java的,要么是过时的,要么就是过于零散,难以直接上手。更别提那些隐藏在BluetoothLeGatt示例项目中,混杂着AsyncTask和Handler的老旧代码了。当你想用现代、简洁的Kotlin来重构时,会发现网上能找到的要么是零碎的代码片段,要么是封装得过于复杂、难以理解的第三方库。
这就是我决定动手整理并开源一个“基于Kotlin语言的蓝牙库示例程序Android版设计源码”的直接原因。这个项目不是一个功能大而全的通用蓝牙框架,它的核心定位非常明确:一个清晰、现代、可直接复用的Kotlin BLE操作模板。它剥离了业务逻辑,专注于展示在Android平台上,如何使用Kotlin协程、Flow等现代语言特性,优雅、安全地处理蓝牙扫描、连接、数据读写、通知监听等核心流程。
在开始之前,我们先明确一下这个示例程序的价值。它不仅仅是几行代码,而是解决了一系列实际开发中的痛点:
- 架构清晰:采用MVVM模式(或更准确的,一个简化的MVI思想)分离了UI、业务逻辑和蓝牙底层操作,便于理解和扩展。
- 现代Kotlin实践:全程使用Kotlin编写,大量运用协程处理异步回调,用
StateFlow管理UI状态,避免了回调地狱和内存泄漏。 - 生命周期安全:与Android的
Lifecycle深度集成,确保蓝牙操作在页面销毁时自动清理,杜绝资源泄露。 - 错误处理完备:对蓝牙权限、位置服务、蓝牙开关状态、连接超时、服务发现失败等常见异常场景进行了封装和处理。
- 可拔插设计:核心的蓝牙管理器(
BluetoothManager)接口化,方便你替换为其他蓝牙库(如RxAndroidBle)或进行单元测试。
这个项目适合所有正在或即将进行Android蓝牙开发的同行,无论你是想快速搭建一个BLE功能原型,还是想学习如何用Kotlin现代化地处理硬件交互,它都能提供一个扎实的起点。接下来,我将从环境搭建开始,带你一步步拆解这个示例程序的设计与实现。
2. 环境准备与项目结构概览
在深入代码之前,我们需要把环境搭建好。这个示例基于Android Studio进行开发,对SDK版本和依赖库有明确要求。
2.1 开发环境与依赖配置
首先,确保你的build.gradle (Module: app)文件中的配置如下。这里的关键是Kotlin协程和Lifecycle相关库,它们是实现异步操作和生命周期感知的基石。
android { compileSdk 34 defaultConfig { minSdk 21 // BLE需要API 18,但为了更好的权限模型和现代API,建议21+ targetSdk 34 ... } buildFeatures { viewBinding true // 或使用Compose,这里以ViewBinding为例 } kotlinOptions { jvmTarget = '1.8' } } dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' // 测试依赖 testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' }注意:这里没有引入任何第三方蓝牙库。我们直接使用Android官方的
android.bluetooth包,目的是为了让你透彻理解原生API的工作机制。在实际大型项目中,你可能会选择RxAndroidBle等库来简化操作,但掌握底层原理是有效使用和调试高级库的前提。
2.2 项目模块与包结构设计
一个清晰的项目结构是代码可维护性的第一道保障。本示例采用了按功能分层的包结构,而非按类型(如把所有Activity放一起)。以下是核心的包目录:
com.example.blekotlindemo/ ├── ui/ │ ├── MainActivity.kt // 主界面,负责UI展示和用户交互 │ └── DeviceListFragment.kt // 设备列表Fragment ├── viewmodel/ │ └── BleViewModel.kt // 持有和处理蓝牙相关状态与逻辑 ├── bluetooth/ │ ├── manager/ │ │ ├── IBluetoothManager.kt // 蓝牙管理器接口 │ │ └── BluetoothManagerImpl.kt // 蓝牙管理器具体实现(核心) │ ├── model/ │ │ ├── BleDevice.kt // 蓝牙设备数据类 │ │ ├── ConnectionState.kt // 连接状态枚举类 │ │ └── GattAction.kt // GATT操作类型(读、写、通知等) │ └── callback/ │ └── SimplifiedBluetoothGattCallback.kt // 简化的GATT回调封装 ├── utils/ │ ├── PermissionsHelper.kt // 权限请求工具 │ └── Extensions.kt // Kotlin扩展函数 └── di/ (可选) └── 依赖注入相关设置,如使用Koin或Hilt这种结构的好处一目了然:ui层只关心界面和用户输入;viewmodel作为中间层,将bluetooth层的复杂操作转换为UI可观察的简单状态;bluetooth层是真正的引擎,负责所有与系统蓝牙API的交互。model包定义了数据传输对象,callback包处理系统回调的转换。当你需要替换蓝牙实现或修改UI时,影响范围被严格控制在了单个模块内。
3. 核心实现:从权限到连接的完整链路
一切就绪,我们进入最核心的部分。蓝牙开发的第一步,永远不是打开蓝牙,而是处理权限。Android的权限模型在不断演进,处理BLE需要格外小心。
3.1 运行时权限与蓝牙开关检测
从Android 12 (API 31) 开始,蓝牙扫描需要BLUETOOTH_SCAN权限,并且该权限可以是neverForLocation的,这解决了长期以来BLE扫描必须请求精确定位权限的尴尬。我们的PermissionsHelper需要智能地处理不同API版本。
object PermissionsHelper { // 定义所需的权限数组 @RequiresApi(Build.VERSION_CODES.S) fun getBlePermissions(): Array<String> = arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) @SuppressLint("InlinedApi") fun getBlePermissionsLegacy(): Array<String> = arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, // API < 31 需要位置权限 Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN ) fun checkAndRequestBlePermissions(activity: FragmentActivity): Boolean { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { getBlePermissions() } else { getBlePermissionsLegacy() } val deniedPermissions = permissions.filter { ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED }.toTypedArray() return if (deniedPermissions.isNotEmpty()) { activity.requestPermissions(deniedPermissions, REQUEST_CODE_BLE_PERMISSIONS) false } else { true } } }在MainActivity中,我们这样使用它:
class MainActivity : AppCompatActivity() { private lateinit var viewModel: BleViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... 初始化UI和ViewModel lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { // 监听权限检查结果 viewModel.permissionGranted.collect { granted -> if (granted) { checkBluetoothAndStartScan() } else { showPermissionRationale() } } } } } private fun checkBluetoothAndStartScan() { val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter() when { bluetoothAdapter == null -> { // 设备不支持蓝牙 showError("设备不支持蓝牙") } !bluetoothAdapter.isEnabled -> { // 请求用户打开蓝牙 val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) } else -> { // 一切就绪,通知ViewModel开始扫描 viewModel.startScan() } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE_BLE_PERMISSIONS) { val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } viewModel.onPermissionResult(allGranted) } } }这里的关键点在于,我们将权限状态和蓝牙开关状态都通过ViewModel中的StateFlow来管理。UI(Activity/Fragment)只负责发起请求和展示结果,状态变化的逻辑集中在ViewModel中,这使得代码更易于测试,也避免了在生命周期复杂的Activity中埋下状态管理的隐患。
3.2 蓝牙扫描的现代化封装
传统的蓝牙扫描需要注册一个BroadcastReceiver来接收BluetoothDevice.ACTION_FOUND广播,对于BLE,则使用BluetoothLeScanner.startScan(scanCallback)。这些API都是基于回调的,在Kotlin协程时代,我们可以将其封装成更易用的Flow。
在BluetoothManagerImpl中,我们实现扫描功能:
class BluetoothManagerImpl @Inject constructor( private val context: Context, private val scope: CoroutineScope ) : IBluetoothManager { private val _scanResults = MutableStateFlow<List<BleDevice>>(emptyList()) override val scanResults: StateFlow<List<BleDevice>> = _scanResults.asStateFlow() private var bluetoothLeScanner: BluetoothLeScanner? = null private var scanCallback: ScanCallback? = null override fun startScan() { val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter() bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner if (bluetoothLeScanner == null) { _scanError.tryEmit("蓝牙适配器不可用") return } // 停止之前的扫描(如果存在) stopScan() // 清空旧结果 _scanResults.value = emptyList() // 配置扫描过滤器(这里不过滤,扫描所有设备) val filters = listOf<ScanFilter>() // 空列表表示不过滤 val settings = ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟模式,发现设备快但耗电 .build() scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult?) { result?.device?.let { device -> val bleDevice = BleDevice( name = device.name ?: "Unknown", address = device.address, rssi = result.rssi ) // 更新扫描结果,这里简单去重(按地址) _scanResults.update { list -> if (list.any { it.address == bleDevice.address }) { list.map { if (it.address == bleDevice.address) bleDevice else it } } else { list + bleDevice } } } } override fun onScanFailed(errorCode: Int) { _scanError.tryEmit("扫描失败,错误码: $errorCode") } } try { bluetoothLeScanner?.startScan(filters, settings, scanCallback) _isScanning.value = true } catch (e: SecurityException) { _scanError.tryEmit("无蓝牙扫描权限: ${e.message}") } catch (e: IllegalStateException) { _scanError.tryEmit("蓝牙适配器状态异常: ${e.message}") } } override fun stopScan() { scanCallback?.let { callback -> try { bluetoothLeScanner?.stopScan(callback) } catch (e: Exception) { Log.e("BluetoothManager", "停止扫描时出错", e) } } scanCallback = null _isScanning.value = false } }在ViewModel中,我们暴露一个简单的状态给UI:
class BleViewModel @Inject constructor( private val bluetoothManager: IBluetoothManager ) : ViewModel() { // UI可以直接观察这些StateFlow val scanResults: StateFlow<List<BleDevice>> = bluetoothManager.scanResults val isScanning: StateFlow<Boolean> = bluetoothManager.isScanning val connectionState: StateFlow<ConnectionState> = bluetoothManager.connectionState fun startScan() { viewModelScope.launch { bluetoothManager.startScan() } } fun stopScan() { viewModelScope.launch { bluetoothManager.stopScan() } } }这样,在Fragment中,我们只需要监听scanResults这个StateFlow,列表就会自动更新。这种响应式编程模式极大地简化了UI逻辑。
3.3 设备连接、服务发现与数据通信
扫描到设备后,下一步就是连接。这是BLE开发中最复杂的一环,涉及BluetoothGatt的一系列异步回调。我们的目标是将其封装成顺序执行的协程挂起函数。
首先,在IBluetoothManager接口中定义连接函数:
interface IBluetoothManager { suspend fun connect(deviceAddress: String): Result<Unit> fun disconnect() suspend fun writeCharacteristic(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray): Result<Unit> suspend fun readCharacteristic(serviceUuid: UUID, characteristicUuid: UUID): Result<ByteArray> fun enableNotification(serviceUuid: UUID, characteristicUuid: UUID, enable: Boolean) // ... 其他状态Flow }在BluetoothManagerImpl中实现connect函数。这里的关键是使用suspendCancellableCoroutine将回调转换为协程:
override suspend fun connect(deviceAddress: String): Result<Unit> = suspendCancellableCoroutine { continuation -> val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() val device = bluetoothAdapter?.getRemoteDevice(deviceAddress) if (device == null) { continuation.resume(Result.failure(IllegalArgumentException("设备地址无效或未找到设备"))) return@suspendCancellableCoroutine } // 先断开之前的连接(如果有) disconnectGatt() _connectionState.value = ConnectionState.CONNECTING currentDeviceAddress = deviceAddress // 注意:这里使用 autoConnect = false 以快速连接,实际可根据场景调整 val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) } else { device.connectGatt(context, false, gattCallback) } this.bluetoothGatt = gatt // 设置一个连接超时 scope.launch { delay(CONNECTION_TIMEOUT_MS) if (_connectionState.value == ConnectionState.CONNECTING) { disconnectGatt() continuation.resume(Result.failure(TimeoutException("连接超时"))) } } // 在GattCallback中处理连接结果 gattCallback.onConnected = { gatt -> _connectionState.value = ConnectionState.CONNECTED continuation.resume(Result.success(Unit)) } gattCallback.onConnectionFailed = { exception -> _connectionState.value = ConnectionState.DISCONNECTED continuation.resume(Result.failure(exception ?: Exception("连接失败"))) } }这里的gattCallback是我们封装的SimplifiedBluetoothGattCallback,它内部处理了onConnectionStateChange、onServicesDiscovered、onCharacteristicRead/Write、onCharacteristicChanged等所有回调,并将它们转换为更易处理的事件或挂起函数的续体(continuation)。
服务发现通常在连接成功后自动或手动触发。在我们的设计中,连接成功后会自动开始发现服务:
// 在 SimplifiedBluetoothGattCallback 的 onConnectionStateChange 中 override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTED -> { // 连接成功,开始发现服务 gatt.discoverServices() onConnected?.invoke(gatt) } BluetoothProfile.STATE_DISCONNECTED -> { onDisconnected?.invoke() gatt.close() } } }发现服务成功后,我们就可以进行读写操作了。以写特征值为例:
override suspend fun writeCharacteristic(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray): Result<Unit> = suspendCancellableCoroutine { continuation -> val gatt = bluetoothGatt if (gatt == null || _connectionState.value != ConnectionState.CONNECTED) { continuation.resume(Result.failure(IllegalStateException("未连接或Gatt对象为空"))) return@suspendCancellableCoroutine } val service = gatt.getService(serviceUuid) val characteristic = service?.getCharacteristic(characteristicUuid) if (characteristic == null) { continuation.resume(Result.failure(IllegalArgumentException("未找到指定的服务或特征"))) return@suspendCancellableCoroutine } // 设置特征值并指定写类型 characteristic.value = data // 根据特征属性决定写入类型 val writeType = when { characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0 -> { BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE } characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0 -> { BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT } else -> { continuation.resume(Result.failure(UnsupportedOperationException("该特征不支持写入"))) return@suspendCancellableCoroutine } } characteristic.writeType = writeType // 将回调与本次挂起关联起来 gattCallback.pendingWriteContinuation = continuation if (!gatt.writeCharacteristic(characteristic)) { gattCallback.pendingWriteContinuation = null continuation.resume(Result.failure(IOException("写入请求发送失败"))) } }在SimplifiedBluetoothGattCallback的onCharacteristicWrite回调中,我们需要取出对应的continuation并恢复它:
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { val cont = pendingWriteContinuation pendingWriteContinuation = null if (status == BluetoothGatt.GATT_SUCCESS) { cont?.resume(Result.success(Unit)) } else { cont?.resume(Result.failure(IOException("写入失败,状态码: $status"))) } }通过这种方式,我们将所有异步、基于回调的蓝牙API,封装成了线性的、可读性极强的协程挂起函数。在ViewModel或业务层,你可以像调用普通函数一样使用它们:
viewModelScope.launch { when (val result = bluetoothManager.connect(deviceAddress)) { is Result.Success -> { // 连接成功,可以开始读写操作 val writeResult = bluetoothManager.writeCharacteristic( serviceUuid = SERVICE_UUID_HEART_RATE, characteristicUuid = CHAR_UUID_HEART_RATE_MEASUREMENT, data = byteArrayOf(0x01) // 例如,使能通知 ) if (writeResult.isSuccess) { // 写入成功 } } is Result.Failure -> { // 处理连接失败 showError(result.exception.message) } } }4. 状态管理与UI联动的实战技巧
将底层蓝牙操作封装好后,如何优雅地在UI上反映状态变化,是提升用户体验的关键。我们使用StateFlow和ViewModel来构建响应式UI。
4.1 使用Sealed Class定义清晰的UI状态
对于连接状态,一个简单的枚举可能不够。我们使用密封类(Sealed Class)来定义所有可能的UI状态,这比使用多个独立的LiveData或Flow更清晰,也便于Compose或DataBinding使用。
// 在 BleViewModel 或一个独立的状态类中 sealed class BleUiState { object Idle : BleUiState() // 初始空闲状态 object Scanning : BleUiState() // 扫描中 data class ScanResults(val devices: List<BleDevice>) : BleUiState() // 扫描结果 object Connecting : BleUiState() // 连接中 data class Connected(val deviceName: String) : BleUiState() // 已连接 data class DataReceived(val data: ByteArray) : BleUiState() // 收到数据 data class Error(val message: String) : BleUiState() // 错误状态 object Disconnected : BleUiState() // 已断开 } // 在ViewModel中合并多个状态流 class BleViewModel @Inject constructor( private val bluetoothManager: IBluetoothManager ) : ViewModel() { private val _uiState = MutableStateFlow<BleUiState>(BleUiState.Idle) val uiState: StateFlow<BleUiState> = _uiState.asStateFlow() init { viewModelScope.launch { // 合并扫描状态和连接状态,驱动UI combine( bluetoothManager.isScanning, bluetoothManager.connectionState, bluetoothManager.scanResults, bluetoothManager.receivedData ) { isScanning, connState, devices, data -> when { isScanning -> BleUiState.Scanning connState == ConnectionState.CONNECTED -> BleUiState.Connected(bluetoothManager.connectedDeviceName ?: "Unknown") connState == ConnectionState.CONNECTING -> BleUiState.Connecting connState == ConnectionState.DISCONNECTED && devices.isEmpty() -> BleUiState.Idle connState == ConnectionState.DISCONNECTED -> BleUiState.ScanResults(devices) data != null -> BleUiState.DataReceived(data) else -> BleUiState.Idle } }.collect { newState -> _uiState.value = newState } } // 单独收集错误流 viewModelScope.launch { bluetoothManager.errorMessages.collect { errorMsg -> if (errorMsg.isNotBlank()) { _uiState.value = BleUiState.Error(errorMsg) } } } } }在UI层(Activity/Fragment),观察这个统一的uiState即可:
// 在Fragment中 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> when (state) { is BleUiState.Scanning -> { binding.progressBar.visibility = View.VISIBLE binding.scanButton.text = "停止扫描" } is BleUiState.ScanResults -> { binding.progressBar.visibility = View.GONE adapter.submitList(state.devices) } is BleUiState.Connecting -> { showToast("正在连接...") } is BleUiState.Connected -> { showToast("已连接到 ${state.deviceName}") // 更新UI,显示数据交互界面 } is BleUiState.Error -> { showErrorDialog(state.message) } // ... 处理其他状态 } } } }4.2 处理屏幕旋转与进程死亡
蓝牙连接是长时操作,且持有系统资源(BluetoothGatt)。必须妥善处理配置变更(如屏幕旋转)和进程死亡。
1. 使用ViewModel保存关键状态:ViewModel在配置变更时不会销毁,因此我们将设备地址、连接状态等保存在ViewModel中。旋转屏幕后,ViewModel可以尝试重新连接。
2. 在onCleared中释放资源:当ViewModel不再需要时(如Activity被finish),必须断开蓝牙连接并释放资源。
override fun onCleared() { super.onCleared() viewModelScope.launch { bluetoothManager.disconnect() bluetoothManager.stopScan() } }3. 处理进程死亡(可选,高级场景):如果你的应用需要后台保持连接,可以考虑使用Foreground Service,并将关键状态(如设备地址)保存到SharedPreferences或DataStore中。当应用从进程死亡中恢复时,ViewModel会重新创建,此时可以从持久化存储中读取设备地址并尝试重新连接。本示例程序聚焦于前台交互,暂不涉及此复杂场景。
4.3 通知(Notification)的启用与数据监听
对于需要设备主动上报数据的特征(如心率测量),需要启用通知(Notification)或指示(Indication)。
fun enableNotification(serviceUuid: UUID, characteristicUuid: UUID, enable: Boolean) { val gatt = bluetoothGatt ?: return val service = gatt.getService(serviceUuid) ?: return val characteristic = service.getCharacteristic(characteristicUuid) ?: return // 1. 先设置客户端特征配置描述符(CCCD) val descriptor = characteristic.getDescriptor(CCC_DESCRIPTOR_UUID) // UUID: 0x2902 descriptor?.value = if (enable) { BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE } else { BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE } // 2. 写入描述符 gatt.writeDescriptor(descriptor) // 3. 如果写入成功,在onDescriptorWrite回调中,再设置特征值的通知 gattCallback.onDescriptorWriteSucceeded = { desc -> if (desc.uuid == CCC_DESCRIPTOR_UUID) { gatt.setCharacteristicNotification(characteristic, enable) } } }启用后,设备发送的数据会触发SimplifiedBluetoothGattCallback的onCharacteristicChanged回调,我们在这里将数据通过Flow发送出去:
// 在 SimplifiedBluetoothGattCallback 中 override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val data = characteristic.value _receivedData.tryEmit(data) }在ViewModel中收集这个receivedDataFlow,并合并到uiState中,UI就能实时更新了。
5. 避坑指南与性能优化
纸上得来终觉浅,绝知此事要躬行。下面分享几个我在实际开发中踩过的坑和总结的优化点,这些在官方文档里往往不会细说。
5.1 连接失败与“133”错误码
这是BLE开发中最常见的错误之一。当你调用connectGatt后,onConnectionStateChange回调中的status参数可能会返回133(或其他非0值),紧接着状态变为STATE_DISCONNECTED。
可能的原因和解决方案:
- 系统层面限制:部分手机厂商(特别是国内定制ROM)对后台扫描和连接有严格限制。确保你的应用在前台运行,并且用户给予了所有必要权限(包括后台位置权限,如果需要)。
- 设备端拒绝:有些BLE设备有连接间隔、安全要求等限制。检查设备文档,确认你的手机兼容性。
- Gatt对象未及时关闭:同一个设备,在断开连接后,必须调用
gatt.close()释放资源,否则再次连接可能会失败。确保你的disconnect逻辑里包含了close()。 - 连接超时:像我们上面实现的,添加一个连接超时机制(如30秒)是非常必要的。超时后主动断开并清理,给用户明确的反馈。
- 重试策略:对于偶发的连接失败,可以实现一个简单的指数退避重试机制,但不要无限重试,通常2-3次后就应该提示用户检查设备和环境。
5.2 扫描耗电与后台限制
持续扫描是耗电大户。我们的示例中使用了SCAN_MODE_LOW_LATENCY,这在前台快速发现设备时是合适的。但在实际应用中,需要考虑更多场景:
- 前台扫描:使用
SCAN_MODE_LOW_LATENCY或SCAN_MODE_BALANCED。 - 后台扫描:如果应用需要在后台持续扫描(如Beacon应用),必须使用
SCAN_MODE_LOW_POWER,并且从Android 8.0开始,后台扫描有严格的限制(时间窗口、发现次数限制)。通常需要结合AlarmManager或WorkManager进行周期扫描。 - 扫描过滤器:使用
ScanFilter可以大幅减少不必要的回调,节省电量。例如,只扫描特定服务UUID或设备名称的设备。val filter = ScanFilter.Builder() .setServiceUuid(ParcelUuid(SERVICE_UUID_HEART_RATE)) .build() val filters = listOf(filter)
5.3 读写操作超时与队列管理
Android的BLE栈内部有一个操作队列。如果你在前一个写操作的回调onCharacteristicWrite收到之前,又发起了下一个写操作,可能会导致第二个操作失败或行为异常。
解决方案:实现一个简单的操作队列。
class BluetoothManagerImpl { private val operationQueue = Channel<GattOperation>(capacity = Channel.UNLIMITED) private val operationScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { operationScope.launch { for (op in operationQueue) { try { when (op) { is GattOperation.Write -> { performWrite(op.serviceUuid, op.charUuid, op.data) } is GattOperation.Read -> { performRead(op.serviceUuid, op.charUuid) } } } catch (e: Exception) { // 处理单个操作失败,不影响队列继续执行 _errorMessages.tryEmit("操作失败: ${e.message}") } } } } suspend fun writeCharacteristicQueued(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray) { operationQueue.send(GattOperation.Write(serviceUuid, characteristicUuid, data)) } private suspend fun performWrite(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray) { // 这里使用我们之前实现的挂起函数 writeCharacteristic // 但确保它是顺序执行的 writeCharacteristic(serviceUuid, characteristicUuid, data).fold( onSuccess = { /* 成功处理 */ }, onFailure = { throw it } ) } } sealed class GattOperation { data class Write(val serviceUuid: UUID, val charUuid: UUID, val data: ByteArray) : GattOperation() data class Read(val serviceUuid: UUID, val charUuid: UUID) : GattOperation() }这样,所有读写操作都会按顺序执行,避免了并发问题。对于需要高吞吐量的场景,你可能需要更复杂的队列优先级管理,但对于大多数应用,一个FIFO队列已经足够。
5.4 内存泄漏预防
蓝牙相关的回调持有Context或Activity引用是内存泄漏的常见根源。
- 在
BluetoothManager中持有Application Context:在初始化BluetoothManagerImpl时,传入Application Context(通过依赖注入或context.applicationContext),而不是Activity Context。 - 及时取消协程:所有在
viewModelScope或自定义scope中启动的协程,都会在ViewModel的onCleared或scope取消时自动取消。确保你的蓝牙操作(如连接超时)是可取消的(使用suspendCancellableCoroutine)。 - 解除回调引用:在
BluetoothManager的disconnect和cleanup方法中,不仅要将bluetoothGatt置为null,还要将gattCallback内部对continuation等临时引用也置为null。
这个基于Kotlin的蓝牙库示例程序,从权限处理、扫描、连接到数据读写,完整地展示了一套现代化、健壮且易于理解的Android BLE开发实践。它没有追求大而全的功能,而是力求在每一个环节都做到清晰和可靠。你可以直接将它作为新项目的基础模块,也可以从中抽取思想来改造现有的蓝牙代码。最重要的是,希望它能帮助你避开那些我曾经踩过的坑,更顺畅地开发出稳定可靠的蓝牙应用。
本文还有配套的精品资源,点击获取