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 之间,既隔离了框架又保留了测试灵活性。

BleManager 接口抽象层架构 客户端(ViewModel / UseCase) <<接口>> BleManager BleManagerImpl Android BLE API FakeBleManager(测试用) @Binds 模块(Hilt) 依赖 Hilt注入 生产绑定 调用 提供实现

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() 防止积压。