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代码。值得你花时间掌握。