
简介这是一份面向Android开发者与Kotlin初学者的蓝牙通信实战源码包聚焦短距离无线通信在移动设备中的典型应用如设备发现、配对、数据传输等核心场景。资源共326个文件压缩包大小24.36MB涵盖62个Kotlin源文件实现协程驱动的蓝牙逻辑、34个Java文件保障与旧项目兼容、133个XML布局与配置文件定义UI与权限、17个AAR库含多个版本的ppblutoothkit蓝牙SDK、14个SO本地库支撑底层蓝牙协议栈调用以及Gradle构建脚本、Markdown说明文档和PNG资源图等。已有423人学习下载适合希望掌握KotlinAndroid蓝牙开发全流程的中初级开发者。读者可直接复用模块化代码结构深入理解AAR多版本依赖管理、Kotlin扩展函数封装蓝牙API、Java/Kotlin混合调用机制并基于真实SDK如ppblutoothkit-3.5.x系列开展调试与二次开发。1. 项目概述与核心价值最近在整理过往项目时翻到了一个基于Kotlin语言实现的Android蓝牙库示例程序。这个项目虽然不大但麻雀虽小五脏俱全它完整地封装了Android原生蓝牙API特别是BLE低功耗蓝牙的核心操作并提供了一个清晰、可直接运行的Demo。对于正在学习Android蓝牙开发尤其是想从Java转向Kotlin或者苦于官方文档过于庞杂、找不到一个完整落地案例的朋友来说这个项目源码或许能提供一个不错的参考起点。简单来说这个项目解决了一个很实际的问题如何将Android系统那套略显繁琐的蓝牙API封装成一个易于理解、便于集成、且符合现代Kotlin协程编程习惯的工具库。它不是一个追求大而全的通用框架而是一个聚焦于BLE通信的“样板间”展示了从设备扫描、连接、服务发现、数据读写到连接状态管理的完整链路。无论你是想快速在自己的App里集成蓝牙功能还是想深入学习Kotlin在Android硬件交互中的应用这个设计源码都能让你避开不少初期摸索的坑。2. 整体架构与设计思路拆解2.1 为什么选择Kotlin与BLE在动手设计之前首先要明确技术选型。项目标题直接点明了“Kotlin语言”和“Android版”这背后有充分的考量。首先Kotlin如今已是Android开发的官方首选语言。相比Java它的空安全特性、扩展函数、更简洁的Lambda表达式以及协程能极大地提升开发效率和代码健壮性。在蓝牙这种涉及大量异步回调、状态管理的场景下Kotlin协程可以将传统的“回调地狱”转换为线性的、更易于理解的挂起函数调用这是用Kotlin重写此类库的核心优势之一。其次聚焦于BLEBluetooth Low Energy低功耗蓝牙而非经典蓝牙是出于对主流物联网和智能设备趋势的响应。BLE功耗极低非常适合手环、传感器、智能家居设备等需要长时间待机并间歇性传输小批量数据的场景。Android对BLE的支持从API 18Android 4.3开始现已非常成熟。我们的库主要面向的就是这类BLE设备通信。2.2 核心设计目标与模块划分这个蓝牙库的设计目标很明确高内聚、低耦合、易使用。整个库被划分为几个清晰的层次而不是把所有代码堆在一个类里。1. 设备管理层 (DeviceManager)这是最外层的入口负责蓝牙适配器的检查、开关、以及最关键的——设备扫描。它不关心具体连接只负责发现周围的BLE设备并以列表的形式返回给调用者。这里的设计要点是扫描过程应该可以被随时启动和停止扫描结果应该通过一个Flow或LiveData暴露出来方便UI层进行观察和刷新。2. 连接与通信层 (BleClient)这是库的核心。每个BleClient实例代表与一个远程BLE设备的连接会话。它内部封装了连接管理建立连接、监控连接状态连接、断开、连接失败、自动重连策略。服务与特征发现连接成功后自动发现设备支持的GATT服务Service和特征值Characteristic。这是后续读写操作的基础。数据读写提供对特定特征值的读、写、通知Notify/Indicate开启/关闭等方法。所有操作都应该是异步的并返回明确的结果成功/失败及数据。3. 数据解析层 (Parser/Converter)蓝牙设备传输的数据通常是原始的字节数组ByteArray。这一层提供一些工具函数帮助开发者将这些字节数组转换为需要的类型比如Int、String、Float等反之亦然。这虽然不是必须的但能极大提升开发体验。4. 示例应用层 (Demo App)一个独立的Android应用模块直观地演示了如何使用上述库。它应该包含扫描设备列表界面、设备详情/控制界面。通过这个Demo开发者可以最快速度跑通整个流程理解API的调用顺序。这样的分层设计使得库本身功能聚焦Demo清晰易懂使用者可以根据需要只关注自己感兴趣的层次。3. 核心实现细节与关键技术点3.1 权限与适配器初始化任何蓝牙操作的前提是获取权限和蓝牙适配器。这看似简单但坑不少。权限声明在AndroidManifest.xml中根据目标API级别需要声明BLUETOOTH用于经典蓝牙和BLE的一般通信、BLUETOOTH_ADMIN用于启动设备发现或进行蓝牙设置、以及Android 6.0以上需要的ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限因为蓝牙扫描可以用于位置推断。对于Android 12API 31及以上还需要BLUETOOTH_SCAN、BLUETOOTH_CONNECT和BLUETOOTH_ADVERTISE这些运行时权限。// 在代码中动态请求权限简化示例 private fun checkAndRequestPermissions() { val requiredPermissions mutableListOfString() if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { requiredPermissions.addAll(listOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT )) } else { requiredPermissions.addAll(listOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN )) } // ... 使用 ActivityResultLauncher 请求权限 }适配器获取通过BluetoothManager获取BluetoothAdapter。这里一定要处理适配器为空设备不支持蓝牙或未开启的情况。我们的库应该在初始化时就检查这些状态并给出明确的错误提示而不是在后续操作中崩溃。val bluetoothManager getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothAdapter bluetoothManager.adapter if (bluetoothAdapter null) { // 设备不支持蓝牙 return } if (!bluetoothAdapter.isEnabled) { // 引导用户开启蓝牙可以发送一个Intent val enableBtIntent Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }注意在Android模拟器上蓝牙适配器可能为null或无法正常工作。真机测试是必须的。另外权限请求的逻辑需要妥善处理用户“拒绝且不再询问”的情况提供友好的引导。3.2 设备扫描的现代实现扫描是发现设备的第一步。传统方法使用BluetoothAdapter.startLeScan(LeScanCallback)但它在API 21后已被标记为过时。推荐使用BluetoothLeScanner。关键步骤获取BluetoothLeScanner实例。创建ScanSettings配置扫描参数如扫描模式SCAN_MODE_LOW_LATENCY追求速度SCAN_MODE_LOW_POWER追求省电、回调类型等。创建ListScanFilter可以过滤目标设备例如按设备名称、MAC地址或服务UUID过滤。对于通用扫描可以传空列表。实现ScanCallback接收结果。用Flow包装扫描结果这是体现Kotlin优势的地方。我们可以创建一个callbackFlow将扫描到的设备发射到流中。fun scanDevices(): FlowScanResult callbackFlow { val scanner bluetoothAdapter.bluetoothLeScanner val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() val filters mutableListOfScanFilter() // 可以添加过滤条件 val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { trySend(result) // 将结果发送到Flow } override fun onScanFailed(errorCode: Int) { close(Throwable(Scan failed with code: $errorCode)) } } scanner.startScan(filters, settings, scanCallback) // 当Flow收集被取消时例如界面销毁停止扫描 awaitClose { scanner.stopScan(scanCallback) } }这样在UI层如ViewModel或Compose中就可以非常优雅地收集设备列表viewModelScope.launch { deviceManager.scanDevices() .map { it.device } // 提取BluetoothDevice对象 .distinctBy { it.address } // 按地址去重 .collect { device - // 更新UI设备列表 _deviceList.update { list - list device } } }实操心得扫描是非常耗电的操作务必在界面不可见如onPause或找到目标设备后及时调用stopScan。callbackFlow配合awaitClose能很好地管理生命周期。另外扫描到的ScanResult包含RSSI信号强度和ScanRecord广播数据这些信息对于设备筛选和UI展示如信号格非常有用。3.3 BLE连接、服务发现与状态管理这是整个库最复杂也最核心的部分。Android原生的BluetoothGattAPI是典型的状态机加回调模式直接使用很容易写出难以维护的代码。连接与GattCallback封装我们创建一个BleClient类在其内部持有BluetoothDevice和BluetoothGatt实例。核心是实现一个BluetoothGattCallback并将其所有回调方法如onConnectionStateChange,onServicesDiscovered,onCharacteristicRead/Write/Changed的事件转换为更易于处理的状态和Channel/Flow。class BleClient(private val device: BluetoothDevice) { private var bluetoothGatt: BluetoothGatt? null private val gattCallback MyGattCallback() // 内部类实现回调 suspend fun connect(): ConnectionResult { return suspendCancellableCoroutine { continuation - // 注意在Android API 23以上建议使用 device.connectGatt(context, false, callback, TRANSPORT_LE) bluetoothGatt device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) // 连接结果将在 onConnectionStateChange 回调中通过 continuation 返回 gattCallback.connectionContinuation continuation } } private inner class MyGattCallback : BluetoothGattCallback() { var connectionContinuation: CancellableContinuationConnectionResult? null override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { super.onConnectionStateChange(gatt, status, newState) val result when (newState) { BluetoothProfile.STATE_CONNECTED - { // 连接成功立即发现服务 gatt.discoverServices() ConnectionResult.Success } BluetoothProfile.STATE_DISCONNECTED - { ConnectionResult.Disconnected } else - ConnectionResult.Failure(Exception(Unknown state: $newState)) } connectionContinuation?.resume(result) connectionContinuation null } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { // 服务发现完成可以在这里缓存服务UUID和特征值 if (status BluetoothGatt.GATT_SUCCESS) { val services gatt.services // 处理服务列表... } } // ... 其他回调如 onCharacteristicRead, onCharacteristicWrite } }状态管理我们需要维护一个清晰的连接状态例如Disconnected、Connecting、Connected、Disconnecting。这个状态应该通过一个StateFlow暴露出去方便UI层观察并更新界面比如连接按钮的文本和状态。服务与特征缓存在onServicesDiscovered回调成功后遍历gatt.services可以按UUID将服务和其特征值缓存到一个Map中。这样后续的读写操作就不需要每次都去gatt里查找直接使用缓存的BluetoothGattCharacteristic对象即可效率更高也更安全。注意事项所有BluetoothGatt的操作都必须是串行的。你不能在前一个readCharacteristic操作的回调还没触发时就发起下一个writeCharacteristic操作否则会导致不可预知的行为。一种常见的做法是使用一个队列Channel来管理待执行的操作命令或者使用Mutex锁来保证同一时间只有一个操作在进行。3.4 数据读写与通知的协程化封装读写和开启通知是交互的关键。我们需要将基于回调的API封装成挂起函数。读操作封装suspend fun readCharacteristic(characteristic: BluetoothGattCharacteristic): ResultByteArray { return suspendCancellableCoroutine { continuation - val callback gattCallback callback.pendingReadContinuation continuation // 将continuation暂存到callback中 if (!bluetoothGatt?.readCharacteristic(characteristic) true) { // 如果立即返回false表示请求失败 continuation.resume(Result.failure(Exception(Read request failed immediately))) callback.pendingReadContinuation null } // 成功发起请求后结果将在 onCharacteristicRead 回调中处理 } } // 在 MyGattCallback 中 override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { val cont pendingReadContinuation pendingReadContinuation null if (status BluetoothGatt.GATT_SUCCESS) { cont?.resume(Result.success(characteristic.value)) } else { cont?.resume(Result.failure(Exception(GATT read error: $status))) } }写操作与通知/指示的封装类似。对于写操作需要区分WRITE_TYPE_DEFAULT需要响应和WRITE_TYPE_NO_RESPONSE无响应更快但不保证送达。对于通知需要先执行setCharacteristicNotification(characteristic, true)然后找到对应的CCCDClient Characteristic Configuration Descriptor并写入BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE来开启。数据流Flow化通知这是非常实用的模式。一旦开启了某个特征值的通知设备就会主动推送数据。我们可以将其转换为一个FlowByteArray让数据像水流一样被收集。fun getNotificationFlow(characteristic: BluetoothGattCharacteristic): FlowByteArray callbackFlow { // 1. 开启通知 val enableSuccess enableNotificationForCharacteristic(characteristic) // 这是一个封装好的挂起函数 if (!enableSuccess) { close(Throwable(Failed to enable notification)) returncallbackFlow } // 2. 在GattCallback中监听 onCharacteristicChanged并将数据发送到channel val listener { data: ByteArray - trySend(data) } notificationListeners[characteristic.uuid] listener // 3. 当Flow收集停止时关闭通知并移除监听器 awaitClose { disableNotificationForCharacteristic(characteristic) notificationListeners.remove(characteristic.uuid) } }这样在业务层就可以这样使用viewModelScope.launch { client.getNotificationFlow(heartRateChar) .collect { bytes - val heartRate parseHeartRate(bytes) // 解析数据 _heartRateLiveData.value heartRate } }4. 示例程序Demo App的构建与界面设计一个优秀的库需要一个同样优秀的Demo来展示其用法。这个示例程序采用最新的Jetpack Compose来构建UI清晰展示从扫描到交互的全过程。4.1 使用MVVM架构组织代码Demo采用ViewModelCompose的架构。MainViewModel持有DeviceManager和当前连接的BleClient。UI状态如设备列表、连接状态、接收到的数据都封装在ViewModel内部的StateFlow或MutableState中。class MainViewModel : ViewModel() { private val deviceManager DeviceManager(context) // UI状态 private val _deviceList MutableStateFlowListBluetoothDevice(emptyList()) val deviceList: StateFlowListBluetoothDevice _deviceList.asStateFlow() private val _connectionState MutableStateFlow(ConnectionState.Disconnected) val connectionState: StateFlowConnectionState _connectionState private var currentClient: BleClient? null init { viewModelScope.launch { // 开始扫描并收集结果 deviceManager.scanDevices().collect { scanResult - // 更新设备列表 } } } fun connectToDevice(device: BluetoothDevice) { viewModelScope.launch { _connectionState.value ConnectionState.Connecting val client BleClient(device) val result client.connect() if (result is ConnectionResult.Success) { currentClient client _connectionState.value ConnectionState.Connected // 开始监听连接状态变化 client.connectionStateFlow.collect { state - _connectionState.value state } } else { _connectionState.value ConnectionState.Disconnected // 处理错误 } } } }4.2 Compose UI界面实现UI分为两个主要屏幕设备扫描列表页和设备控制详情页。扫描列表页(DeviceListScreen)顶部一个按钮控制扫描的开始与停止。中间是一个LazyColumn列表展示扫描到的每个设备的名称、MAC地址和信号强度RSSI。RSSI可以用WiFi信号类似的图标直观表示。点击列表项导航到详情页并尝试连接。Composable fun DeviceListScreen(viewModel: MainViewModel, onDeviceClick: (BluetoothDevice) - Unit) { val devices by viewModel.deviceList.collectAsStateWithLifecycle() val isScanning by viewModel.isScanning.collectAsStateWithLifecycle() Column(modifier Modifier.fillMaxSize()) { Button( onClick { if (isScanning) viewModel.stopScan() else viewModel.startScan() }, modifier Modifier.padding(16.dp) ) { Text(if (isScanning) 停止扫描 else 开始扫描) } LazyColumn { items(devices) { device - DeviceItem( device device, onClick { onDeviceClick(device) } ) } } } }设备控制详情页(DeviceDetailScreen)显示设备基本信息名称、状态。展示发现的服务和特征值列表。可以点击特征值进行读、写、开启通知等操作。一个区域用于显示从设备接收到的数据特别是来自通知的数据。一个输入框和按钮用于向可写的特征值发送数据。这个页面的状态管理更复杂因为它需要根据连接状态动态显示内容并处理用户的交互操作。所有对BleClient的调用都应在ViewModel的协程作用域内进行确保生命周期安全。4.3 数据解析与展示在详情页收到原始字节数组(ByteArray)后需要解析成可读的格式。Demo中应包含几个简单的解析器示例十六进制字符串显示最通用的方式将ByteArray转为“0A 1B 2C”这样的字符串。UTF-8字符串解析如果设备发送的是文本信息。数值解析例如从特定字节位置解析出Int或Float。这里要注意字节序大端/小端。// 简单的工具对象 object DataParser { fun toHexString(bytes: ByteArray): String bytes.joinToString( ) { %02X.format(it) } fun toUtf8String(bytes: ByteArray): String String(bytes, Charsets.UTF_8) fun bytesToIntLittleEndian(bytes: ByteArray, offset: Int 0): Int { // 小端序解析例如 bytes[0]是低位 return (bytes[offset].toInt() and 0xFF) or ((bytes[offset 1].toInt() and 0xFF) shl 8) // 可根据长度扩展 } }在UI上可以提供一个下拉菜单让用户选择不同的解析方式来查看同一份数据。5. 开发中的常见问题、调试技巧与优化建议在实际开发和使用这个库的过程中我遇到了不少典型问题这里总结一下希望能帮你省点时间。5.1 连接失败与状态码解读连接失败是最常见的问题。onConnectionStateChange回调中的status参数是GATT状态码不是连接状态。newState才是连接状态STATE_CONNECTED或STATE_DISCONNECTED。status 133 (0x85)这是一个非常常见的错误通常意味着连接建立失败。原因可能包括设备不在范围内、设备拒绝了连接、系统资源不足、或者在Android 6.0上缺少定位权限这个原因极其隐蔽。务必确保动态权限已授予。status 257 (0x101)GATT内部错误通常发生在多次快速连接/断开操作后。给蓝牙栈一点“冷静期”或者尝试重启手机蓝牙。status 8 (0x08)超时。排查步骤确认蓝牙已开启设备已开机且在范围内。确认所有必要的运行时权限尤其是定位权限都已授予。检查是否在onDestroy或恰当的生命周期回调中正确调用了gatt.close()。资源泄露会导致后续连接失败。尝试将设备从系统蓝牙设置中取消配对然后重新连接。旧的错误配对信息可能导致问题。5.2 读写操作无回调或失败操作无回调最常见的原因是操作队列被阻塞或BluetoothGatt对象被意外释放。确保你的操作是串行的并且持有有效的gatt引用。写操作失败检查特征值的属性。尝试写入前务必确认characteristic.properties包含PROPERTY_WRITE或PROPERTY_WRITE_NO_RESPONSE。写之前可能需要先成功读取一次特征值某些设备有此要求。读操作返回空数据有些设备的特征值需要先订阅通知Notify才能读取到有效数据或者读取操作本身需要触发设备的一次测量。查阅设备的数据手册至关重要。5.3 通知Notify不工作这是BLE开发中最令人头疼的问题之一。流程必须完全正确gatt.setCharacteristicNotification(characteristic, true)必须成功返回true。找到该特征值对应的CCCD描述符UUID为0x2902。向该描述符写入BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE或ENABLE_INDICATION_VALUE。等待onDescriptorWrite回调确认写入成功。之后设备发送的数据才会触发onCharacteristicChanged回调。实操心得步骤2和3很容易出错。不是所有特征值都有CCCD。一定要在onServicesDiscovered成功后通过characteristic.getDescriptor(UUID.fromString(00002902-0000-1000-8000-00805f9b34fb))来获取并检查其是否为null。5.4 后台连接与保活Android系统为了省电会在应用进入后台一段时间后限制其网络和蓝牙活动。这可能导致连接断开或数据无法接收。应对策略前台服务Foreground Service如果需要长时间在后台保持连接必须启动一个前台服务并显示一个持续的通知。这是Android 8.0API 26后的强制要求。WorkManager或AlarmManager用于在特定时间或间隔执行任务但可能不适合需要实时通信的场景。使用BluetoothGatt的autoConnect参数在connectGatt时将其设为true系统会尝试在后台维持连接或自动重连但这并不完全可靠且行为因厂商定制系统而异。白名单策略引导用户将你的应用加入电池优化白名单忽略电池优化但这需要用户手动操作。最佳实践对于大多数消费级App建议在应用进入后台时优雅地断开BLE连接并在回到前台时重新连接。如果必须后台连接务必使用前台服务并清晰告知用户。5.5 性能与资源优化扫描优化使用ScanFilter减少不必要的扫描结果处理降低CPU和电量消耗。在找到目标设备后立即停止扫描。连接管理实现连接池或单例管理避免对同一设备创建多个BleClient实例。确保在不需要时如界面销毁、应用退出调用gatt.close()和gatt.disconnect()释放资源。数据缓存如之前所述缓存服务和特征值对象避免重复查找。协程作用域管理在ViewModel或LifecycleScope中启动协程确保界面销毁时自动取消防止内存泄漏。对于可能长时间运行的数据流收集如通知流使用repeatOnLifecycle或flowWithLifecycle来确保只在界面处于活动状态时收集。6. 从示例到生产库的扩展与封装建议这个示例程序提供了一个坚实的基础但要用于真实的生产环境还需要考虑更多。1. 增加重连机制网络不稳定或设备移动都可能导致连接意外断开。可以在BleClient内部实现一个带指数退避策略的自动重连逻辑。监听连接状态一旦非主动断开则延迟一段时间后尝试重连。2. 操作超时处理所有挂起函数如connect,readCharacteristic,writeCharacteristic都应该有超时机制。可以使用withTimeoutOrNull来包装操作避免因为设备无响应而永远挂起。suspend fun readCharacteristicWithTimeout(char: BluetoothGattCharacteristic, timeoutMs: Long 5000): ResultByteArray? { return withTimeoutOrNull(timeoutMs) { readCharacteristic(char) } ?: Result.failure(TimeoutCancellationException(Read operation timed out)) }3. 日志与调试工具在库内部集成一个可开关的日志系统记录关键步骤连接、发现服务、读写操作及其结果。这对于线上问题排查至关重要。可以提供一个setLogger方法让使用者注入自己的日志实现。4. 更完善的错误类型不要只用Exception定义一套丰富的错误密封类Sealed Class如BluetoothNotAvailableError、PermissionDeniedError、GattOperationError(status: Int)、TimeoutError等。这样调用者能更精确地处理错误。5. 考虑发布为AAR库将核心模块打包成Android Archive (AAR)发布到公司的Maven仓库或公开的Maven Central/JCenter。提供清晰的README文档、API文档使用Dokka或KDoc和版本更新日志。6. 单元测试与集成测试为关键类编写单元测试使用Mockito等框架模拟Android依赖。虽然测试硬件交互很难但可以测试业务逻辑如数据解析、状态机转换等。集成测试则需要真机和真实蓝牙设备配合。最后这个基于Kotlin的蓝牙库示例其价值不仅在于提供可运行的代码更在于展示了一种用现代Kotlin协程和响应式思维来简化传统复杂API的设计模式。当你理解了设备扫描如何用Flow表达连接回调如何转化为挂起函数数据通知如何变成数据流你就能将这种模式应用到Android开发的其他领域比如传感器、NFC、甚至网络请求的封装从而写出更清晰、更健壮、更易于维护的代码。本文还有配套的精品资源点击获取