news 2026/9/2 5:21:37

Android BLE开发实战:基于Kotlin协程与MVVM的现代蓝牙库设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android BLE开发实战:基于Kotlin协程与MVVM的现代蓝牙库设计

简介:本资源是一套面向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示例项目中,混杂着AsyncTaskHandler的老旧代码了。当你想用现代、简洁的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,它内部处理了onConnectionStateChangeonServicesDiscoveredonCharacteristicRead/WriteonCharacteristicChanged等所有回调,并将它们转换为更易处理的事件或挂起函数的续体(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("写入请求发送失败"))) } }

SimplifiedBluetoothGattCallbackonCharacteristicWrite回调中,我们需要取出对应的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上反映状态变化,是提升用户体验的关键。我们使用StateFlowViewModel来构建响应式UI。

4.1 使用Sealed Class定义清晰的UI状态

对于连接状态,一个简单的枚举可能不够。我们使用密封类(Sealed Class)来定义所有可能的UI状态,这比使用多个独立的LiveDataFlow更清晰,也便于ComposeDataBinding使用。

// 在 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,并将关键状态(如设备地址)保存到SharedPreferencesDataStore中。当应用从进程死亡中恢复时,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) } } }

启用后,设备发送的数据会触发SimplifiedBluetoothGattCallbackonCharacteristicChanged回调,我们在这里将数据通过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

可能的原因和解决方案:

  1. 系统层面限制:部分手机厂商(特别是国内定制ROM)对后台扫描和连接有严格限制。确保你的应用在前台运行,并且用户给予了所有必要权限(包括后台位置权限,如果需要)。
  2. 设备端拒绝:有些BLE设备有连接间隔、安全要求等限制。检查设备文档,确认你的手机兼容性。
  3. Gatt对象未及时关闭:同一个设备,在断开连接后,必须调用gatt.close()释放资源,否则再次连接可能会失败。确保你的disconnect逻辑里包含了close()
  4. 连接超时:像我们上面实现的,添加一个连接超时机制(如30秒)是非常必要的。超时后主动断开并清理,给用户明确的反馈。
  5. 重试策略:对于偶发的连接失败,可以实现一个简单的指数退避重试机制,但不要无限重试,通常2-3次后就应该提示用户检查设备和环境。

5.2 扫描耗电与后台限制

持续扫描是耗电大户。我们的示例中使用了SCAN_MODE_LOW_LATENCY,这在前台快速发现设备时是合适的。但在实际应用中,需要考虑更多场景:

  • 前台扫描:使用SCAN_MODE_LOW_LATENCYSCAN_MODE_BALANCED
  • 后台扫描:如果应用需要在后台持续扫描(如Beacon应用),必须使用SCAN_MODE_LOW_POWER,并且从Android 8.0开始,后台扫描有严格的限制(时间窗口、发现次数限制)。通常需要结合AlarmManagerWorkManager进行周期扫描。
  • 扫描过滤器:使用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 内存泄漏预防

蓝牙相关的回调持有ContextActivity引用是内存泄漏的常见根源。

  • BluetoothManager中持有Application Context:在初始化BluetoothManagerImpl时,传入Application Context(通过依赖注入或context.applicationContext),而不是Activity Context
  • 及时取消协程:所有在viewModelScope或自定义scope中启动的协程,都会在ViewModelonClearedscope取消时自动取消。确保你的蓝牙操作(如连接超时)是可取消的(使用suspendCancellableCoroutine)。
  • 解除回调引用:在BluetoothManagerdisconnectcleanup方法中,不仅要将bluetoothGatt置为null,还要将gattCallback内部对continuation等临时引用也置为null

这个基于Kotlin的蓝牙库示例程序,从权限处理、扫描、连接到数据读写,完整地展示了一套现代化、健壮且易于理解的Android BLE开发实践。它没有追求大而全的功能,而是力求在每一个环节都做到清晰和可靠。你可以直接将它作为新项目的基础模块,也可以从中抽取思想来改造现有的蓝牙代码。最重要的是,希望它能帮助你避开那些我曾经踩过的坑,更顺畅地开发出稳定可靠的蓝牙应用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 5:18:56

前端路由历史管理:从History API原理到SPA状态恢复实战

最近在开发一个历史记录管理功能时&#xff0c;我遇到了一个典型的“历史包袱”问题&#xff1a;用户操作路径复杂&#xff0c;前进后退逻辑混乱&#xff0c;状态恢复总是不准确。团队里一位经验丰富的同事看了一眼代码&#xff0c;半开玩笑地说&#xff1a;“你这‘history’&…

作者头像 李华
网站建设 2026/9/2 5:18:24

毕业设计实战:构建可解释、可迭代的电影推荐系统核心指南

最近在帮几个计算机专业的学生看毕业设计选题&#xff0c;发现一个很有意思的现象&#xff1a;几乎每届都有不少同学把“电影推荐系统”作为毕业设计的首选。这本身是个好选题&#xff0c;它综合了数据处理、算法应用和前后端开发&#xff0c;能很好地展示技术栈。但问题在于&a…

作者头像 李华
网站建设 2026/9/2 5:17:34

XMC电机控制工程:硬件规范、工具链与FOC协同设计

简介&#xff1a;本资源是基于英飞凌XMC1301单片机开发的电动车驱动器完整嵌入式工程&#xff0c;面向电机控制工程师、嵌入式开发者及高校电力电子方向学习者&#xff0c;聚焦无刷直流电机&#xff08;BLDC&#xff09;的FOC矢量控制、电池管理与实时故障保护等核心问题。压缩…

作者头像 李华
网站建设 2026/9/2 5:17:29

电力线通信多径信道建模:Zimmermann模型原理与MATLAB实现

简介&#xff1a;本资源是面向通信工程、电力电子及信号处理方向本科生与研究生的MATLAB仿真工具包&#xff0c;聚焦电力线通信&#xff08;PLC&#xff09;中关键的多径信道建模问题&#xff0c;实现Manfred Zimmermann经典理论模型的完整数值仿真。资源共3个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/2 5:16:25

四足机器人源码解读:从分层结构到实机部署的实战指南

简介&#xff1a;基于Arduino的四足机器人完整控制源码包&#xff0c;面向机器人爱好者、高校创客及电子设计初学者&#xff0c;解决从步态算法到舵机控制的项目落地难题。压缩包共16个文件&#xff0c;总大小22.45MB&#xff0c;含3个ino主程序、1个h头文件以及cpp/pde扩展代码…

作者头像 李华
网站建设 2026/9/2 5:15:00

STM32驱动7寸RGB电容屏全攻略:LTDC配置与GT911触摸开发实战

简介&#xff1a;本资源面向嵌入式开发工程师、STM32初学者及工业人机界面&#xff08;HMI&#xff09;项目开发者&#xff0c;提供一套完整的7英寸RGB接口电容触摸屏&#xff08;GT911驱动&#xff09;软硬件集成解决方案&#xff0c;解决屏幕适配难、触控调试复杂、原理图与封…

作者头像 李华