1. 依赖注入基础回顾:Dagger / Hilt / Koin 核心概念、组件作用域、依赖图构建

1.1 依赖注入要解决什么问题

依赖注入(Dependency Injection, DI)是一种设计模式,将对象的创建与使用分离。在 BLE 开发中,扫描器、连接管理器、GATT 服务等组件存在复杂的依赖关系,手动构造会带来以下问题:

  • 紧耦合:组件之间直接通过 new 创建,难以替换测试桩或 mock 对象。
  • 生命周期混乱:BLE 组件需要与 Activity / Service 生命周期绑定,手动管理极易泄漏。
  • 重复样板代码:每个类都需要手动传入构造参数,代码冗余且容易遗漏。

依赖注入框架通过依赖图自动装配对象,开发者只需声明“我需要什么”和“如何提供”,框架在编译时或运行时完成注入。

1.2 三大框架核心概念对比

框架 核心机制 声明方式 生成时期 组件作用域 适用场景
Dagger 编译时注解 + APT 生成代码 @Module / @Provides / @Component 编译时 自定义 @Scope 注解 大型项目,追求极致性能
Hilt 基于 Dagger 的 Android 封装 @HiltViewModel / @InstallIn / @Module 编译时 预定义 @Singleton / @ActivityScoped / @FragmentScoped Android 最佳实践 + BLE 组件
Koin 运行时 DSL + Kotlin 协程 module { } / single / factory 运行时 作用域通过 DSL 嵌套控制 轻量项目、KMM 多平台
选择建议: 本课程后续 BLE 示例主要基于 Hilt(Android 官方推荐),同时提供 Koin 的备用实现。Dagger 作为底层原理讲解。

1.3 组件作用域与生命周期

作用域(Scope)决定了依赖实例在容器中的存活时间。错误的 scope 会导致 BLE 连接泄漏或回调失效。

  • Singleton(单例):整个应用进程一个实例,适用于蓝牙适配器 BluetoothAdapter。
  • ActivityScoped:随 Activity 创建/销毁,适用于扫描回调 LeScanCallback。
  • FragmentScoped:随 Fragment 生命周期,适用于设备列表 UI 绑定。
  • 自定义 Scoped:例如 @ConnectionScoped 用于管理每个 BLE 连接的生命周期。
// Hilt 中声明作用域示例
@Module
@InstallIn(SingletonComponent::class)
object BleModule {
    @Provides
    @Singleton
    fun provideBluetoothAdapter(context: Context): BluetoothAdapter {
        return BluetoothAdapter.getDefaultAdapter()
            ?: throw IllegalStateException("Bluetooth not supported")
    }

    @Provides
    @ActivityScoped
    fun provideLeScanner(adapter: BluetoothAdapter): BluetoothLeScanner {
        return adapter.bluetoothLeScanner
    }
}

1.4 依赖图的构建机制

依赖图是一个有向无环图(DAG)。框架通过以下步骤构造:

  1. 收集模块:扫描所有 @Module(或 Koin 的 module {})中的提供者声明。
  2. 解析依赖关系:每个提供者的参数作为图的入边,返回值作为出边。
  3. 检测循环:编译时(Dagger/Hilt)或运行时(Koin)报告循环依赖。
  4. 实例化顺序:按拓扑序依次创建对象,并注入到目标类。
  5. 作用域管理:每个 scope 对应一个子图,父图对象可注入子图,反之不行。
BLE 开发注意: 蓝牙适配器一般是全局单例,但 BluetoothGatt 对象需要按连接单独创建,建议使用 @ConnectionScoped 自定义 scope 来管理每个连接的生命周期。
依赖图构建流程 依赖图构建流程(DAG) @Module 收集 依赖解析 & 构建图 循环检测 拓扑排序 实例化 & 注入 扫描所有 @Module 参数 → 入边,返回值 → 出边 编译时 / 运行时报错 子图对应 scope 依赖图必须有向无环

1.5 三种框架的声明式注入写法

以下代码分别展示如何声明一个 BLE 扫描器实例(以 BluetoothLeScanner 为例):

// ---------- Dagger ----------
@Module
class BleModule {
    @Provides
    @Singleton
    fun provideLeScanner(adapter: BluetoothAdapter): BluetoothLeScanner {
        return adapter.bluetoothLeScanner
    }
}

@Component(modules = [BleModule::class])
@Singleton
interface AppComponent {
    fun inject(activity: MainActivity)
}

// ---------- Hilt ----------
@Module
@InstallIn(SingletonComponent::class)
object BleHiltModule {
    @Provides
    @Singleton
    fun provideLeScanner(@ApplicationContext context: Context): BluetoothLeScanner {
        val adapter = BluetoothAdapter.getDefaultAdapter()
        return adapter.bluetoothLeScanner
    }
}

// ---------- Koin ----------
val bleModule = module {
    single { BluetoothAdapter.getDefaultAdapter()?.bluetoothLeScanner }
}

// 在 Application 中启动 Koin
startKoin {
    modules(bleModule)
}
常见错误 #1:循环依赖 — 例如 BLE 扫描器依赖于连接管理器,而连接管理器又依赖于扫描器。Dagger/Hilt 会在编译时报错:Dependency cycle detected。解决方法:使用 @Lazy 或拆分为多层模块。
常见错误 #2:未绑定的提供者 — 如果某个依赖在模块中没有对应的 @Provides 或 single,框架会抛出 UnsatisfiedDependencyException。务必确保所有依赖都已声明。

1.6 小结