Kotlin协程封装Android蓝牙开发:从扫描连接到数据通信的完整实践 简介这是一份面向Android开发者与Kotlin初学者的蓝牙通信实战源码包聚焦短距离无线通信在移动设备中的典型应用如设备发现、配对、数据传输等核心场景。资源共326个文件压缩包大小24.36MB涵盖62个Kotlin源文件实现协程驱动的蓝牙逻辑、34个Java文件保障与旧项目兼容、133个XML布局与配置文件定义UI及权限声明、17个AAR库含多个版本ppblutoothkit蓝牙SDK、14个SO本地库支撑底层蓝牙协议栈调用以及Gradle构建脚本、Markdown说明文档和PNG资源图等。已有423人学习下载适合希望掌握KotlinAndroid蓝牙开发全流程的中初级开发者。读者可直接运行调试完整示例程序深入理解蓝牙权限适配、扫描连接状态机、跨语言调用机制并基于多版本AAR快速对比SDK演进差异为工业级蓝牙IoT应用开发打下坚实基础。1. 项目概述与核心价值最近在做一个智能硬件项目需要和Android设备进行蓝牙通信于是花了不少时间研究Kotlin下的蓝牙开发。我发现虽然官方文档和网上教程不少但要么是Java老代码要么是零散的API调用示例真正能拿来即用、结构清晰、符合现代Kotlin协程和架构思想的完整示例并不多。所以我决定自己动手整理并实现一个“基于Kotlin语言的蓝牙库示例程序Android版”。这个项目不仅仅是一个Demo它更像是一个可复用的蓝牙通信脚手架封装了从设备扫描、连接、数据读写到状态管理的完整流程并且融入了我在实际开发中踩过的坑和总结的最佳实践。这个示例程序的核心价值在于它为你提供了一个高内聚、低耦合的蓝牙通信模块。无论你是要开发一个智能手环的配套App还是一个需要通过蓝牙控制硬件的IoT应用甚至是做一个简单的蓝牙串口调试工具你都可以直接把这个模块集成进去快速搭建起蓝牙通信的基础设施而无需再从零开始研究那些繁琐且容易出错的BluetoothAdapter和BluetoothGatt。项目完全采用Kotlin编写大量使用了协程Coroutines来处理异步操作让回调地狱成为过去式代码逻辑清晰得像写同步代码一样舒服。同时我也考虑了常见的兼容性问题和异常处理确保在大多数Android设备上都能稳定运行。2. 整体架构设计与技术选型2.1 为什么选择纯Kotlin与协程在Android开发中处理蓝牙这种典型的I/O密集型、强异步操作的任务传统的回调Callback或RxJava方式会让代码变得嵌套很深难以维护。Kotlin协程的出现为异步编程带来了革命性的简化。它允许我们以近乎同步的代码风格来编写异步逻辑极大地提升了代码的可读性和可维护性。在这个蓝牙库示例中我全面采用Kotlin协程的Flow和Channel来处理蓝牙事件流。例如设备扫描结果不再通过回调接口一个个返回而是通过一个FlowListBluetoothDevice持续发射UI层如ViewModel可以很方便地收集collect这个流并更新UI。连接状态、服务发现、数据接收等所有异步过程都被封装成了挂起函数suspend function你可以在协程作用域内顺序调用它们就像调用普通函数一样异常也可以通过try-catch直接捕获。注意使用协程需要处理好生命周期。示例中会结合Android的Lifecycle使用lifecycleScope或viewModelScope来启动协程确保在界面销毁时自动取消避免内存泄漏。2.2 核心模块分层解析一个好的库不能把所有代码都堆在一起。我采用了清晰的分层架构将不同职责的代码分离使得核心逻辑独立于UI也便于单元测试。1. 数据层Data Layer实体类Entities定义纯净的数据模型如BluetoothDevice封装原生BluetoothDevice并添加自定义状态如信号强度RSSI、是否已绑定等、BluetoothService、BluetoothCharacteristic等。这些类不包含任何Android框架依赖。仓库接口Repository Interface定义蓝牙操作的核心契约例如interface BluetoothRepository { suspend fun scanDevices(): FlowScanResult; suspend fun connect(deviceAddress: String): ConnectionResult; }。依赖接口而非具体实现这是依赖注入和可测试性的关键。2. 领域层Domain Layer用例Use Cases / Interactors封装具体的业务逻辑。例如一个ConnectToDeviceUseCase会协调调用仓库的connect方法并根据连接结果可能触发重试逻辑或更新某个特定的状态。这一层使业务规则更加明确。3. 框架层Framework Layer / 实现层仓库实现Repository Implementation这里是和Android蓝牙APIBluetoothAdapter,BluetoothLeScanner,BluetoothGatt等打交道的地方。我创建了一个AndroidBluetoothRepository类来实现BluetoothRepository接口。所有对系统蓝牙服务的复杂操作、权限检查、回调转换都被封装在这里。权限与系统服务封装处理Android 6.0的动态位置权限、Android 12的蓝牙新权限BLUETOOTH_SCAN,BLUETOOTH_CONNECT以及优雅地获取和检查BluetoothAdapter是否可用。4. 表现层Presentation LayerViewModel持有仓库或用例的引用负责准备UI所需的数据。它通过调用挂起函数或收集Flow来获取设备列表、连接状态等并将其转换为StateFlow或LiveData暴露给UI。UICompose/View使用Jetpack Compose现代首选或传统XML布局来构建界面。Compose的声明式特性与Flow/StateFlow是天作之合可以非常流畅地实现状态驱动UI。这种分层确保了核心的蓝牙通信逻辑框架层与业务逻辑领域层和界面逻辑表现层解耦。未来如果你想换一个UI框架或者甚至将这个模块移植到其他平台理论上只需要替换表现层和框架层中与平台相关的部分即可。3. 核心功能实现与代码拆解3.1 蓝牙设备扫描从权限到结果流扫描是蓝牙交互的第一步也是最容易出问题的一步。下面我详细拆解实现过程。第一步权限处理从Android 6.0 (API 23)开始蓝牙扫描需要ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限因为蓝牙信标可以用于位置追踪。从Android 12 (API 31)开始引入了更细粒度的蓝牙权限BLUETOOTH_SCAN、BLUETOOTH_CONNECT和BLUETOOTH_ADVERTISE。我们的代码必须兼容这些版本。// 在ViewModel或Activity/Fragment中请求权限 private val requiredPermissions buildList { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { add(Manifest.permission.BLUETOOTH_SCAN) add(Manifest.permission.BLUETOOTH_CONNECT) // Android 12上如果不需要获取物理位置可以声明usesPermissionFlagsneverForLocation // 但通常我们仍需要旧版位置权限以兼容旧设备 } add(Manifest.permission.ACCESS_FINE_LOCATION) }.toTypedArray() // 使用 Accompanist Permissions 库Compose或 ActivityResult API 请求权限第二步封装扫描逻辑在AndroidBluetoothRepository中我们实现扫描功能。关键点是将基于回调的BluetoothLeScanner.startScan转换为KotlinFlow。override fun scanDevices(): FlowScanResult callbackFlow { val bluetoothAdapter BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter null || !bluetoothAdapter.isEnabled) { close(IllegalStateException(Bluetooth is not available or disabled)) returncallbackFlow } val scanner bluetoothAdapter.bluetoothLeScanner val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟模式发现设备快 .build() // 过滤器可以根据服务UUID过滤这里不过滤任何设备 val filters listOfScanFilter() // 空列表表示扫描所有BLE设备 val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { // 将每个扫描结果发送到Flow中 trySend(result).isSuccess // 忽略发送失败的情况 } override fun onScanFailed(errorCode: Int) { close(ScanFailedException(Scan failed with error code: $errorCode)) } } scanner.startScan(filters, settings, scanCallback) // 当Flow收集者停止收集时或协程取消执行此块停止扫描 awaitClose { scanner.stopScan(scanCallback) Log.d(TAG, Bluetooth scan stopped.) } }这段代码的精髓在于callbackFlow构建器。它创建了一个Flow并在内部设置了一个传统的回调scanCallback。每当onScanResult被调用我们就用trySend将结果发射到流中。awaitClose块定义了当流被取消例如用户停止扫描、界面退出时需要执行的清理操作——即停止扫描。这样UI层只需要调用repository.scanDevices().collect { ... }就能持续收到设备更新并在离开界面时自动停止扫描资源管理非常优雅。实操心得扫描非常耗电。在UI设计中最好提供一个手动“开始扫描/停止扫描”的按钮而不是一直扫描。也可以在awaitClose里加入延迟实现一个“扫描10秒后自动停止”的逻辑。3.2 设备连接与GATT通信状态管理与数据读写连接和通信是蓝牙开发的核心也是坑最多的地方。BluetoothGatt的回调机制非常繁琐我们需要用协程将其“驯服”。第一步连接与发现服务连接操作本身是异步的我们需要将其封装为挂起函数。override suspend fun connect(deviceAddress: String): ConnectionResult { return suspendCancellableCoroutine { continuation - val bluetoothAdapter BluetoothAdapter.getDefaultAdapter() val device bluetoothAdapter.getRemoteDevice(deviceAddress) // 注意Android API建议在UI线程外进行GATT连接但Context是必须的。 // 我们传入ApplicationContext并设置自动连接为false以获得更可控的行为。 val gatt device.connectGatt(context, false, gattCallback) // 将gatt对象与回调关联起来以便在回调中处理 connectingGattMap[deviceAddress] gatt // 设置一个超时防止连接过程挂起 continuation.invokeOnCancellation { gatt.disconnect() gatt.close() connectingGattMap.remove(deviceAddress) } // 连接状态会在gattCallback中通知我们需要在那里resume这个continuation // 这里我们用一个临时的“连接状态通道”来传递结果 val connectionChannel ChannelConnectionResult(capacity 1) pendingConnectionChannels[deviceAddress] connectionChannel // 启动一个协程来等待连接结果并带有超时 CoroutineScope(continuation.context).launch { val result withTimeoutOrNull(10000) { // 10秒连接超时 connectionChannel.receive() } pendingConnectionChannels.remove(deviceAddress) if (result ! null) { continuation.resume(result) } else { continuation.resume(ConnectionResult.Failure(TimeoutException(Connection timeout))) gatt.disconnect() gatt.close() } } } } // GATT回调处理连接状态变化和服务发现 private val gattCallback object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { val address gatt.device.address if (newState BluetoothProfile.STATE_CONNECTED) { Log.i(TAG, Connected to GATT server for $address.) // 连接成功开始发现服务这是另一个异步操作 gatt.discoverServices() } else if (newState BluetoothProfile.STATE_DISCONNECTED) { Log.i(TAG, Disconnected from GATT server for $address.) // 通知上层连接断开 _connectionStateFlow.emit(ConnectionState.Disconnected) gatt.close() // 重要断开后必须close释放资源 connectedGattMap.remove(address) // 如果是正在进行的连接过程失败了通知等待的channel pendingConnectionChannels[address]?.trySend(ConnectionResult.Failure(Exception(Disconnected during connection))) } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { if (status BluetoothGatt.GATT_SUCCESS) { Log.i(TAG, Services discovered for ${gatt.device.address}) // 服务发现成功可以认为连接真正就绪 connectedGattMap[gatt.device.address] gatt _connectionStateFlow.emit(ConnectionState.Connected(gatt.device)) // 通知等待连接的channel成功 pendingConnectionChannels[gatt.device.address]?.trySend(ConnectionResult.Success(gatt.device)) } else { Log.w(TAG, onServicesDiscovered received: $status) // 服务发现失败连接不完整 pendingConnectionChannels[gatt.device.address]?.trySend(ConnectionResult.Failure(Exception(Service discovery failed: $status))) gatt.disconnect() } } }这段代码看起来复杂但逻辑很清晰connect函数启动连接并等待结果通过Channel。GattCallback在连接状态变化和服务发现完成后通过Channel通知等待的协程。我们引入了超时机制和资源清理invokeOnCancellation确保任何情况下都不会泄露BluetoothGatt对象。第二步读写特征值Characteristic连接并发现服务后我们就可以通过BluetoothGatt读写特征值了。同样我们将异步回调封装为挂起函数。suspend fun readCharacteristic(deviceAddress: String, serviceUuid: UUID, characteristicUuid: UUID): ByteArray { val gatt connectedGattMap[deviceAddress] ?: throw IllegalStateException(Not connected to $deviceAddress) val service gatt.getService(serviceUuid) ?: throw IllegalArgumentException(Service not found) val characteristic service.getCharacteristic(characteristicUuid) ?: throw IllegalArgumentException(Characteristic not found) return suspendCancellableCoroutine { continuation - val callback object : BluetoothGattCallback() { override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { if (status BluetoothGatt.GATT_SUCCESS) { val value characteristic.value continuation.resume(value ?: byteArrayOf()) } else { continuation.resumeWithException(IOException(Failed to read characteristic, status: $status)) } // 移除这个临时回调避免干扰其他操作 // 实际项目中可能需要一个更全局的回调分发机制 } } // 临时替换Gatt回调简化示例生产环境需更健壮的管理 // 更好的做法是使用一个中央回调根据transaction ID分发结果。 gatt.callback callback if (!gatt.readCharacteristic(characteristic)) { continuation.resumeWithException(IOException(GATT read request failed immediately)) } continuation.invokeOnCancellation { // 清理 } } } // 写操作类似但需要区分WRITE_TYPE_DEFAULT和WRITE_TYPE_NO_RESPONSE suspend fun writeCharacteristic(deviceAddress: String, serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray, writeType: Int BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) { // ... 实现逻辑与read类似设置characteristic的value和writeType然后调用gatt.writeCharacteristic }这里的关键挑战是BluetoothGatt的所有操作读、写、通知都共享同一个回调对象BluetoothGattCallback。如何将一次特定的操作比如读某个特征值与其结果正确匹配上面的简化示例临时替换了回调这在简单场景下可行但不适合并发操作。更健壮的做法是引入一个“操作队列”或“Transaction ID”机制。我们可以维护一个待处理操作的映射表Map在发起操作时生成一个唯一ID并将其与一个CompletableDeferred关联。在全局的BluetoothGattCallback中根据回调参数如特征值的UUID或我们自行添加的Transaction ID找到对应的CompletableDeferred并完成它resume或resumeWithException。这是将基于回调的API完美适配到协程世界的经典模式。3.3 状态管理使用StateFlow与Sealed Class蓝牙连接有多种状态未连接、连接中、已连接、正在发现服务、连接失败、断开连接等。使用Kotlin的密封类Sealed Class配合StateFlow来管理这些状态可以让UI层非常清晰地进行响应。// 定义连接状态 sealed class ConnectionState { object Disconnected : ConnectionState() object Connecting : ConnectionState() data class Connected(val device: BluetoothDevice) : ConnectionState() data class DiscoveringServices(val device: BluetoothDevice) : ConnectionState() data class Error(val exception: Throwable) : ConnectionState() } // 在Repository或一个专门的StateHolder中管理状态 class BluetoothConnectionStateHolder { private val _connectionState MutableStateFlowConnectionState(ConnectionState.Disconnected) val connectionState: StateFlowConnectionState _connectionState.asStateFlow() // 在GattCallback的onConnectionStateChange、onServicesDiscovered等方法中更新状态 fun updateState(newState: ConnectionState) { _connectionState.value newState } }在UICompose中你可以这样收集状态val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() when (connectionState) { is ConnectionState.Disconnected - { /* 显示连接按钮 */ } is ConnectionState.Connecting - { /* 显示进度条 */ } is ConnectionState.Connected - { /* 显示已连接设备信息和控制界面 */ } is ConnectionState.Error - { /* 显示错误信息 */ } }这种模式使得状态驱动UI变得非常直观和类型安全。4. 常见问题、避坑指南与实战技巧蓝牙开发中遇到的很多问题不是逻辑错误而是对系统行为、API限制理解不足导致的。下面是我总结的“血泪经验”。4.1 权限与兼容性陷阱问题1在Android 12设备上扫描不到BLE设备。原因从Android 12开始如果应用需要获取蓝牙设备的物理位置信息大多数扫描场景都需要除了BLUETOOTH_SCAN权限还必须申请ACCESS_FINE_LOCATION权限。或者你可以在AndroidManifest.xml中声明android:usesPermissionFlagsneverForLocation但这意味着你无法获取设备的信号强度RSSI对于需要根据信号强度过滤或定位的场景不适用。解决方案始终同时请求BLUETOOTH_SCAN和ACCESS_FINE_LOCATION权限。在清单文件中为BLUETOOTH_SCAN权限添加android:usesPermissionFlagsneverForLocation仅在你确定不需要位置信息时使用。问题2在部分国产定制系统如MIUI, EMUI上权限弹窗不正常或功能被限制。原因这些系统有更激进的省电策略和权限管理可能会在后台禁止蓝牙扫描。解决方案引导用户去系统设置中为你的应用开启“自启动”、“允许后台活动”等选项体验很差但有时不得不做。在扫描时尝试将ScanSettings的setScanMode设置为SCAN_MODE_LOW_POWER或SCAN_MODE_BALANCED高功耗模式可能被系统拦截。考虑使用前台服务Foreground Service来进行长时间的蓝牙操作并在通知栏显示一个持续的通知这能显著降低被系统杀死的概率。4.2 连接与GATT操作中的“玄学”问题问题3BluetoothGatt.connect()或gatt.discoverServices()后没有回调。原因这是最常见的问题之一。BluetoothGatt对象是单次使用的且与特定的BluetoothDevice和Context绑定。常见错误包括在同一个BluetoothDevice上多次调用connectGatt产生了多个BluetoothGatt实例回调可能发到旧的实例上。持有BluetoothGatt的Context如Activity被销毁导致内部资源失效。没有在UI线程外进行连接操作虽然这不是强制的但官方建议如此。解决方案单例管理为每个设备地址维护一个全局的BluetoothGatt实例。在连接前检查是否已存在连接如果存在先disconnect()和close()旧的。使用Application Context调用connectGatt时传入ApplicationContext而非Activity的Context避免因Activity生命周期导致问题。超时与重试如示例代码所示为连接和发现服务操作添加超时机制。超时后主动调用gatt.disconnect()和gatt.close()然后进行有限次数的重试。问题4写入数据成功但设备没反应或者onCharacteristicWrite回调的status不是GATT_SUCCESS。原因特征值Characteristic的写入类型Write Type设置错误。有两种主要类型WRITE_TYPE_DEFAULT(0x02)需要设备的确认响应ACK可靠性高但速度慢。WRITE_TYPE_NO_RESPONSE(0x01)不需要确认速度快但可能丢包。 有些设备的特征值只支持其中一种。如果你用WRITE_TYPE_DEFAULT去写一个只支持WRITE_TYPE_NO_RESPONSE的特征写入会失败。解决方案在写入前检查特征值支持的属性characteristic.properties。如果包含PROPERTY_WRITE_NO_RESPONSE则使用WRITE_TYPE_NO_RESPONSE如果包含PROPERTY_WRITE则使用WRITE_TYPE_DEFAULT。可以通过characteristic.writeType ...来设置。问题5接收不到设备发来的通知Notification数据。原因要使能通知需要三步缺一不可找到具有PROPERTY_NOTIFY或PROPERTY_INDICATE属性的特征值。调用gatt.setCharacteristicNotification(characteristic, true)启用通知。向该特征值的客户端特征配置描述符Client Characteristic Configuration Descriptor, CCCDUUID通常是0x2902写入BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE或ENABLE_INDICATION_VALUE。解决方案确保三步都正确执行。示例代码如下suspend fun enableNotification(characteristic: BluetoothGattCharacteristic) { val gatt characteristic.service.device // 假设能拿到gatt // 1. 启用通知 gatt.setCharacteristicNotification(characteristic, true) // 2. 获取CCCD描述符并写入启用值 val descriptor characteristic.getDescriptor(UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)) descriptor?.value BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor) // 这也是一个异步操作需要等待回调 }启用后设备发送的数据就会在BluetoothGattCallback.onCharacteristicChanged()回调中收到。4.3 性能、资源与最佳实践1. 及时关闭GATT连接BluetoothGatt对象持有系统底层资源。在断开连接后必须调用gatt.close()来释放资源。否则多次连接后可能导致系统蓝牙资源耗尽表现为新的连接无法建立。一个好的模式是在onConnectionStateChange收到STATE_DISCONNECTED后立即调用close()。2. 避免在主线程进行蓝牙操作虽然一些基础的BluetoothAdapter操作可以在主线程进行但扫描、连接、数据读写等耗时操作应该放在后台线程。使用协程可以很自然地做到这一点只需确保在Dispatchers.IO或自定义的调度器上启动协程。3. 合理管理扫描持续扫描是耗电大户。在App进入后台时应停止扫描。可以使用Lifecycle库来监听应用生命周期或者在Service中管理扫描。对于需要后台持续扫描的场景如Beacon应用必须使用前台服务并告知用户。4. 处理设备绑定Bonding有些蓝牙设备如某些耳机、键盘需要进行配对/绑定Bonding才能进行更高安全级别的通信。这个过程会由系统弹出对话框你的应用可以通过广播接收器BroadcastReceiver监听BluetoothDevice.ACTION_BOND_STATE_CHANGED来获知绑定状态变化。在BluetoothGattCallback中也可能收到onServicesDiscovered之前的状态码GATT_INSUFFICIENT_AUTHENTICATION提示你需要先绑定。5. 示例程序UI层构建与集成有了强大的后台蓝牙库前端UI的职责就变得清晰而简单响应用户操作、展示状态、发送指令。这里我使用Jetpack Compose来构建UI因为它与StateFlow的状态驱动模式配合得天衣无缝。5.1 设备扫描列表在ViewModel中我们暴露一个设备列表的StateFlow和一个开始/停止扫描的函数。class BluetoothViewModel(private val repository: BluetoothRepository) : ViewModel() { private val _scanning MutableStateFlow(false) val scanning: StateFlowBoolean _scanning.asStateFlow() private val _devices MutableStateFlowListScannedDevice(emptyList()) val devices: StateFlowListScannedDevice _devices.asStateFlow() private var scanJob: Job? null fun startScan() { if (_scanning.value) return _scanning.value true _devices.value emptyList() // 清空旧列表 scanJob viewModelScope.launch { repository.scanDevices() .catch { e - Log.e(ViewModel, Scan error, e) } .collect { scanResult - // 更新设备列表可以按信号强度排序、去重 val newDevice ScannedDevice( device scanResult.device, rssi scanResult.rssi, scanRecord scanResult.scanRecord?.bytes ) _devices.update { currentList - // 简单的去重逻辑如果地址已存在更新RSSI否则添加 val existing currentList.find { it.device.address newDevice.device.address } if (existing ! null) { currentList.map { if (it.device.address newDevice.device.address) newDevice else it } } else { currentList newDevice }.sortedByDescending { it.rssi } // 按信号强度降序排列 } } } } fun stopScan() { scanJob?.cancel() _scanning.value false } }在Compose UI中Composable fun DeviceScanScreen(viewModel: BluetoothViewModel) { val devices by viewModel.devices.collectAsStateWithLifecycle() val scanning by viewModel.scanning.collectAsStateWithLifecycle() Column { Button(onClick { if (scanning) viewModel.stopScan() else viewModel.startScan() }) { Text(if (scanning) 停止扫描 else 开始扫描) } LazyColumn { items(devices) { device - DeviceItem(device device, onClick { /* 触发连接 */ }) } } } }5.2 连接状态管理与数据交互连接状态的展示同样直接绑定到StateFlow。当用户点击一个设备进行连接时ViewModel调用仓库的connect函数并更新连接状态。// 在ViewModel中处理连接 fun connectToDevice(deviceAddress: String) { viewModelScope.launch { _connectionState.value ConnectionState.Connecting val result repository.connect(deviceAddress) when (result) { is ConnectionResult.Success - { _connectionState.value ConnectionState.Connected(result.device) // 连接成功后可以自动订阅某个特征值的通知 startListeningToNotifications(deviceAddress) } is ConnectionResult.Failure - { _connectionState.value ConnectionState.Error(result.exception) } } } } private fun startListeningToNotifications(deviceAddress: String) { viewModelScope.launch { // 假设我们知道要监听的服务UUID和特征值UUID val serviceUuid UUID.fromString(0000ffe0-0000-1000-8000-00805f9b34fb) val charUuid UUID.fromString(0000ffe1-0000-1000-8000-00805f9b34fb) try { repository.enableNotification(deviceAddress, serviceUuid, charUuid) // 开始收集通知数据流 repository.notificationFlow(deviceAddress, serviceUuid, charUuid) .collect { data - // 处理接收到的数据例如更新UI或解析协议 _receivedData.emit(String(data, Charsets.UTF_8)) } } catch (e: Exception) { Log.e(ViewModel, Failed to enable notification, e) } } }UI层根据connectionState显示不同的界面并提供一个发送数据的按钮。Composable fun DeviceControlScreen(viewModel: BluetoothViewModel) { val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val receivedData by viewModel.receivedData.collectAsStateWithLifecycle() when (val state connectionState) { is ConnectionState.Connecting - { CircularProgressIndicator() Text(正在连接 ${state.device.name}...) } is ConnectionState.Connected - { Column { Text(已连接到 ${state.device.name}) TextField( value inputText, onValueChange { inputText it }, label { Text(发送数据) } ) Button(onClick { viewModel.sendData(inputText.toByteArray()) }) { Text(发送) } Text(接收到的数据) Text(receivedData) } } is ConnectionState.Error - { Text(连接出错${state.exception.message}) Button(onClick { /* 重连 */ }) { Text(重试) } } else - { /* 未连接状态 */ } } }5.3 项目结构总结与源码获取最终一个完整的、基于Kotlin的Android蓝牙示例程序其项目结构可能如下所示app/ ├── src/main/ │ ├── java/com/example/bluetoothdemo/ │ │ └── (可能有一些Java兼容类如果有) │ ├── kotlin/com/example/bluetoothdemo/ │ │ ├── data/ │ │ │ ├── model/ # 数据模型 (ScannedDevice, ConnectionState) │ │ │ ├── repository/ # 仓库接口 (BluetoothRepository) │ │ │ └── impl/ # 仓库实现 (AndroidBluetoothRepository) │ │ ├── domain/ # 用例 (ConnectUseCase, SendDataUseCase) │ │ ├── di/ # 依赖注入模块 (Koin/Hilt) │ │ ├── presentation/ │ │ │ ├── viewmodel/ # ViewModels │ │ │ └── ui/ # Compose UI Screens Components │ │ └── utils/ # 扩展函数、常量等 │ └── res/ # 资源文件 ├── build.gradle.kts # 模块级构建配置 └── proguard-rules.pro # 混淆规则这个结构清晰地将蓝牙通信的核心逻辑data层和domain层与Android框架和UIpresentation层分离。你可以轻松地将data层和domain层打包成一个独立的Android Library模块供其他项目复用。关于源码由于篇幅限制这里无法贴出全部上万行代码。但上述关键代码段已经勾勒出了整个库的骨架。在实际开发中你还需要处理更多细节例如更完善的错误处理和重试机制。蓝牙适配器不可用、被用户关闭等情况下的友好提示。对不同Android版本的API差异做更精细的兼容处理。对接收到的二进制数据按照你与硬件设备约定的协议进行解析和打包。我建议你以本文的思路为蓝图动手搭建自己的项目。从最简单的扫描和连接开始逐步添加数据读写、通知、状态管理等功能。每实现一个功能就充分测试并记录下遇到的问题和解决方案。这个过程本身就是对Android蓝牙开发最深刻的学习。本文还有配套的精品资源点击获取