第1章 依赖注入基础:理解控制反转与依赖注入
大家好,我是老张。干Android开发这些年,我见过不少因为耦合太紧导致的“改一行崩一片”的惨案。今天咱们就从最基础的依赖注入(DI)开始聊,你想想看,这玩意儿到底解决了什么问题。
1.1 什么是控制反转(IoC)
控制反转,英文Inversion of Control,简称 IoC。说白了,就是把“控制权”从你手里交出去。你写代码时,常常需要某个对象去干活,比如一个网络请求库、一个数据库操作类。传统方式是谁需要谁就自己new一个,控制权在自己手里。
我在早期做项目时,每写一个Activity就new一堆工具类,后来发现:想换一个底层实现?得把每个new的地方改一遍。那时我才意识到,把创建权交出去有多重要。
1.2 依赖注入(DI)是什么
依赖注入是IoC的一种具体实现。简单说就是:我不自己new,而是让别人把依赖“注射”进来。常见的注入方式有三种:构造器注入、字段注入(@Inject注解)、方法注入。
| 注入方式 | 描述 | 我推荐的场景 |
|---|---|---|
| 构造器注入 | 通过构造函数传参 | 核心依赖,不可变,首选 |
| 字段注入 | 用@Inject等注解直接赋值 | Android框架类(如Activity) |
| 方法注入 | 通过setter或自定义方法传参 | 可选依赖或初始化后配置 |
1.3 为什么需要依赖注入
你可能会问:“我直接new不好吗?干嘛非要搞个注入?”嗯,看看这几个痛点你就明白了。
- 解耦: 调用方不依赖具体实现,只依赖接口。换实现时只改一处。
- 可测试性: 单元测试时可以轻松传入Mock对象,不用启动整个App。
- 生命周期管理: 容器能帮你管理单例、作用域(比如ActivityScope),防止内存泄漏。
- 配置集中: 所有依赖的创建逻辑统一在一个地方,维护起来清爽。
我一同事之前接手一个老项目,里面到处都是静态工厂和单例。想加个单元测试?得先启动模拟器跑一遍完整流程……后来改成DI,测试日子好过多了。
1.4 手动依赖注入的局限
在没有Hilt这类框架之前,我们只能手动实现DI——自己写一个“容器”类,负责创建和提供依赖。比如这样:
class AppContainer {
private val database = Room.databaseBuilder(...)
val userRepository = UserRepository(database.userDao())
val orderRepository = OrderRepository(database.orderDao())
}
// 在Activity里手动接收
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val container = (application as MyApp).appContainer
val repo = container.userRepository
// ...
}
}
这种手动DI有几个明显问题:
- 模板代码爆炸: 每个依赖都要手动注册、手动传参,项目一大多达几百个类,光写容器就累死人。
- 作用域管理困难: 想实现Activity级别的单例?你得自己维护Map,还要注意在Activity销毁时清除——稍不留神就内存泄漏。
- 耦合依旧存在: Activity仍然知道容器的存在(它强依赖AppContainer),并没有完全解耦。
- 重构风险高: 修改一个依赖的构造参数,所有手动创建的地方都得改一遍。
1.5 从手动到自动:Hilt登场的前夜
手动DI的局限催生了自动DI框架。Dagger 2 先在Java/Kotlin领域兴起,但它配置繁琐、编译慢。Google后来基于Dagger推出了Hilt,专门为Android优化——自动生成组件、简化注解、集成ViewModel等。你看这张图就明白了DI的核心演进:
嗯,看完这张图你应该明白了:手动DI像你亲自买菜、切菜、炒菜;Hilt这样的自动DI则像找了一个专业厨师团队——你只管点菜(声明依赖),厨房帮你搞定一切,出错了还帮你排查。