1. 依赖注入初探:什么是依赖注入?为什么Android项目需要它?手动依赖注入的痛点
大家好,我是你们的老朋友。今天咱们来聊聊依赖注入——这个在Android圈子里被说烂了,但很多人其实没真正搞懂的概念。
说实话,我刚开始接触Android开发那会儿,也觉得依赖注入就是个花架子。不就是new个对象嘛,搞那么复杂干嘛?直到我接手了一个有几十个Activity的老项目,每次改个网络库版本都要改几十处代码……嗯,从那以后,我再也不敢小看DI了。
1.1 什么是依赖注入?
先别急着看定义。咱们从一个最简单的例子说起。
假设你写了一个用户登录的ViewModel:
class LoginViewModel {
private val api = UserApi()
private val repo = UserRepository(api)
fun login(username: String, password: String) {
repo.login(username, password)
}
}
这段代码有什么问题?LoginViewModel自己创建了它需要的依赖。说白了,它既当裁判又当运动员。
依赖注入的核心思想就一句话:别自己new,让别人给你送过来。
改成依赖注入的写法:
class LoginViewModel(
private val api: UserApi,
private val repo: UserRepository
) {
fun login(username: String, password: String) {
repo.login(username, password)
}
}
看到了吗?LoginViewModel不再关心UserApi和UserRepository怎么创建。它只管用。谁负责创建?交给外部的注入器(比如Dagger、Hilt、Koin)。
依赖注入的定义:一个对象接收它所依赖的其他对象的过程。这些依赖由外部传入,而非对象内部自行创建。
1.2 为什么Android项目需要它?
你可能会问:我手动new对象也写了好几年,不也活得好好的?
嗯,这话没毛病。但Android项目有几个特点,让依赖注入变得特别有价值。
1.2.1 生命周期管理
Android组件有复杂的生命周期。Activity销毁重建时,你手动new的对象也跟着丢了。但如果你用依赖注入框架管理,它可以帮你保持单例、控制作用域。
我在项目中遇到过最头疼的事:一个网络请求在Activity旋转屏幕后回调,结果Activity已经重建了,旧的对象还在引用旧的实例……用DI框架后,这种问题基本绝迹。
1.2.2 测试友好
这是依赖注入最大的价值之一。你想想看,如果LoginViewModel内部直接new了一个RealUserApi,你想测试登录逻辑怎么办?只能连真实的服务器。
但用了依赖注入:
class LoginViewModelTest {
@Test
fun testLogin() {
val mockApi = mock<UserApi>()
val mockRepo = UserRepository(mockApi)
val viewModel = LoginViewModel(mockApi, mockRepo)
// 现在你可以控制mockApi的行为
// 测试各种边界情况
}
}
说白了,依赖注入让单元测试变得可行。没有DI,你写测试时得用反射或者各种黑科技去替换依赖,那叫一个痛苦。
1.2.3 解耦与可维护性
我见过一个项目,网络库从OkHttp换成Retrofit,改了两百多处代码。为什么?因为每个类都自己new了HttpClient。
如果用了依赖注入,你只需要改一处——提供依赖的地方。其他所有使用者,代码一行都不用动。
我的经验:依赖注入带来的解耦效果,在项目规模超过10个模块时会特别明显。小项目可能感觉不到,但一旦项目膨胀,DI就是你的救命稻草。
1.3 手动依赖注入的痛点
好,现在你理解了依赖注入的好处。那咱们自己手动实现行不行?
当然可以。很多小项目一开始就是手动DI。但规模大了以后,手动DI的痛点会越来越明显。
1.3.1 模板代码爆炸
假设你有这样一个依赖链:
class AppContainer {
// 网络层
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com")
.client(okHttpClient)
.build()
val userApi = retrofit.create(UserApi::class.java)
// 数据层
val userRepo = UserRepository(userApi)
val orderRepo = OrderRepository(userApi)
// ViewModel层
fun createLoginViewModel() = LoginViewModel(userApi, userRepo)
fun createOrderViewModel() = OrderViewModel(orderRepo)
}
这还只是三个模块。真实项目里,依赖可能有几十上百个。每次新增一个依赖,你都得手动去AppContainer里注册。改一个依赖的构造参数,所有相关的地方都得跟着改。
我曾经维护过一个手动DI的项目,AppContainer类有800多行。每次改代码都提心吊胆,生怕漏了某个依赖没更新。
1.3.2 作用域管理困难
有些对象应该是全局单例(比如OkHttpClient),有些应该跟Activity生命周期绑定(比如某个Presenter),有些应该跟用户会话绑定(比如登录状态)。
手动管理这些作用域,你得自己写Map、自己控制生命周期。稍不留神就会内存泄漏。
我曾经踩过的坑:在Application里保存了一个Activity级别的对象,结果Activity销毁后这个对象还在被引用,导致整个Activity无法被GC回收。查了两天才找到原因。
1.3.3 循环依赖难以发现
A依赖B,B依赖C,C依赖A。这种循环依赖在手动DI中很难被发现。你只有在运行时才会看到StackOverflowError。
而好的DI框架(比如Dagger)在编译期就能检测出循环依赖,直接报错给你看。
1.3.4 测试成本高
手动DI的项目,你想替换某个依赖做测试?你得手动创建整个依赖树,或者用反射去替换。这工作量,说实话,很多人直接放弃了写测试。
| 对比项 | 手动DI | DI框架 |
|---|---|---|
| 模板代码量 | 多,随项目规模线性增长 | 少,框架自动生成 |
| 作用域管理 | 手动,容易出错 | 框架内置,声明式 |
| 循环依赖检测 | 运行时才能发现 | 编译期即可检测 |
| 测试友好度 | 低,替换依赖成本高 | 高,天然支持Mock |
| 学习成本 | 低,不需要学框架 | 中等,需要理解概念 |
1.4 小结
依赖注入不是什么高深莫测的技术。说白了,就是把对象的创建和使用分开。Android项目因为生命周期复杂、测试要求高、模块耦合多,特别需要依赖注入。
手动DI在小项目里够用,但项目一膨胀,痛点就全出来了。这也是为什么业界最终选择了Dagger、Hilt、Koin这些框架。
下一章,咱们会深入聊聊Android平台上主流的DI框架,看看它们各自的特点和适用场景。到时候我会结合我实际项目中的选型经验,给你一些参考建议。
一句话总结:依赖注入不是银弹,但它能帮你写出更干净、更可测试、更易维护的Android代码。值得你花时间掌握。