3. 创建 BleManager 接口与抽象层
在 BLE 开发中,直接依赖 BluetoothAdapter、BluetoothGatt 等系统 API 会导致代码与 Android 框架强耦合,难以进行单元测试和 Hilt 注入。本章将定义一套松耦合的接口抽象层,将扫描、连接、读写操作封装为稳定的契约,上层业务只依赖接口,具体实现交由 Hilt 提供的绑定模块注入。
3.1 为什么需要抽象层
- 隔离框架依赖:业务代码不直接调用 Android BLE API,测试时可用 Mock 实现替换真实蓝牙行为。
- 统一生命周期管理:所有 BLE 操作集中在一组接口方法中,便于在 ViewModel / Service 中触发和取消。
- 支持多实现切换:同一接口可对应不同底层实现(例如生产用
BleManagerImpl,测试用FakeBleManager)。 - Hilt 注入友好:接口 +
@Binds模块是 Hilt 推荐的最佳实践,构造参数由 Hilt 自动提供。
3.2 接口定义原则
| 关注点 | 原则 | 说明 |
|---|---|---|
| 扫描 | 响应式流 | 使用 Flow<ScanResult> 返回扫描结果,便于协程消费和测试。 |
| 连接 | 状态机 | 返回 StateFlow<ConnectionState>,对外暴露连接、断开、重连过程。 |
| 读写 | 挂起函数 | suspend fun readCharacteristic() / suspend fun writeCharacteristic(),简化调用链。 |
| 生命周期 | 可取消 | 所有长时间操作返回 Job 或 Closeable,便于在 onCleared() 时释放。 |
3.3 核心接口定义
以下代码展示了 BleManager 接口的核心抽象,包含扫描、连接、服务发现、特征读写等常用操作。所有方法均采用 Kotlin 协程原语,与 Hilt 的 @HiltViewModel 或 @AndroidEntryPoint 组合时无需额外线程切换。
// BleManager.kt
interface BleManager {
// ---------- 扫描 ----------
fun startScan(): Flow<ScanResult>
fun stopScan()
// ---------- 连接 ----------
fun connect(deviceAddress: String): StateFlow<ConnectionState>
fun disconnect(deviceAddress: String)
// ---------- 服务与特征 ----------
suspend fun discoverServices(deviceAddress: String): List<BluetoothGattService>
suspend fun readCharacteristic(
deviceAddress: String,
characteristicUuid: UUID
): ByteArray?
suspend fun writeCharacteristic(
deviceAddress: String,
characteristicUuid: UUID,
value: ByteArray
): Boolean
// ---------- 通知 ----------
fun observeCharacteristic(
deviceAddress: String,
characteristicUuid: UUID
): Flow<ByteArray>
// ---------- 生命周期 ----------
fun release()
}
enum class ConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
DISCONNECTING,
RECONNECTING
}
设计解释:
startScan()返回冷流Flow,每次订阅都会触发扫描,停止扫描通过cancel()或调用stopScan()完成。connect()返回StateFlow,保证连接状态变化可以持续观察,且上层可以使用stateIn()转为可观察状态。readCharacteristic()和writeCharacteristic()是挂起函数,内部自动处理 GATT 队列和超时。observeCharacteristic()返回Flow,用于接收远端设备主动推送的通知/指示。
3.4 接口层次与注入关系
下图展示了客户端(ViewModel / UseCase)仅依赖 BleManager 接口,Hilt 通过 @Binds 将 BleManagerImpl 绑定到该接口。抽象层位于业务与系统 API 之间,既隔离了框架又保留了测试灵活性。
3.5 如何用 Hilt 提供接口实现
在 Hilt 模块中使用 @Binds 将具体实现绑定到接口。以下为典型模块定义:
// BleModule.kt
@Module
@InstallIn(SingletonComponent::class)
abstract class BleModule {
@Binds
@Singleton
abstract fun bindBleManager(impl: BleManagerImpl): BleManager
}
同时 BleManagerImpl 的构造函数使用 @Inject 注解,Hilt 自动提供 Context 和 BluetoothAdapter:
// BleManagerImpl.kt
class BleManagerImpl @Inject constructor(
private val context: Context,
private val bleAdapter: BluetoothAdapter
) : BleManager {
// 实现细节...
}
测试技巧:单元测试时,使用 @Module 替换绑定模块,将 BleManager 绑定到 FakeBleManager:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [BleModule::class]
)
abstract class TestBleModule {
@Binds
abstract fun bindFakeBleManager(impl: FakeBleManager): BleManager
}
3.6 常见错误与排查
- 接口方法不匹配协程作用域:
Flow/StateFlow返回类型必须准确,否则编译时无法通过 Hilt 的限定符检查。 - 忘记标记
@Singleton:BleManager应为单例,否则每次注入都会创建新实例导致连接状态丢失。 - 在 ViewModel 中直接持有
BleManager的引用而不使用viewModelScope:会导致协程泄漏,应当始终在viewModelScope内收集Flow。 @Binds方法参数类型错误:必须接收一个接口的实现子类,返回接口类型。混淆类型会导致 Hilt 编译错误error: [Dagger/MissingBinding]。
3.7 验收清单
| 检查项 | 通过条件 |
|---|---|
| 接口定义完整 | 包含扫描、连接、读写、通知、生命周期五个维度 |
| 返回类型使用 Flow/StateFlow/suspend | 所有异步操作遵守协程原语 |
| Hilt 绑定正确 | 编译通过且无 Dagger 错误 |
| 单元测试可替换 | @TestInstallIn 能成功替换生产绑定 |
| 非空安全处理 | 所有返回 nullable 的方法有明确文档和空安全处理 |
性能提醒:接口方法中避免在协程中重复创建 UUID 或 BluetoothGattDescriptor 对象,应在构造函数中预缓存。另需注意 Flow 的背压问题——扫描回调频率很高时应对 ScanResult 使用 conflate() 或 buffer() 防止积压。