5、依赖注入入门:为什么需要DI,Hilt框架的配置与基本使用

依赖注入,简称DI。这个词你肯定听过无数遍了。

但说实话,我刚开始接触Android时,觉得这东西就是「为了用而用」。不就是new个对象吗?搞那么复杂干嘛?直到我接手了一个老项目——一个Activity里塞了十几个Presenter,每个Presenter又依赖好几个Repository,Repository又依赖Dao和ApiService……

嗯,那感觉就像拆炸弹。改一个构造参数,牵连七八个文件。从那天起,我彻底服了DI。

5.1 为什么需要依赖注入?

我们先想一个问题:一个类要干活,往往需要别的类帮忙。比如ViewModel需要Repository,Repository需要ApiService。这种「需要别人帮忙」的关系,就叫依赖。

传统写法是这样的:

class UserRepository {
    private val api = RetrofitApiService() // 自己new
    private val dao = UserDao()            // 自己new

    fun getUser(): User {
        return api.fetchUser()
    }
}

有什么问题?

  • 耦合太紧:UserRepository 直接绑死了 RetrofitApiService。哪天想换成Mock数据做测试?得改代码。
  • 难以测试:你没法单独测UserRepository的逻辑,因为它的依赖是写死的。
  • 重复创建:每个地方new一次,内存里一堆重复对象。

依赖注入的核心思想就一句话:别自己new,让别人给你送过来

依赖注入的本质:把对象的创建和绑定,从类内部移到外部。类只负责声明「我需要什么」,不负责「怎么造出来」。

用DI改写后:

class UserRepository @Inject constructor(
    private val api: ApiService,
    private val dao: UserDao
) {
    fun getUser(): User {
        return api.fetchUser()
    }
}

看到区别了吗?UserRepository不再自己new东西了。它只是说:「给我一个ApiService和一个UserDao,我就能干活。」至于这两个东西怎么创建、是不是单例、是不是Mock——它不管。

这就是依赖倒置原则的体现。高层模块不依赖低层模块的具体实现,而是依赖抽象接口。

5.2 手动DI vs 框架DI

你可能会说:「那我手动写个工厂类,把所有依赖集中管理,不也行吗?」

当然行。我早期项目就是这么干的。写一个AppContainer类,里面new好所有对象,然后通过Application传递。

但项目一大了,问题就来了:

  • 容器类越来越臃肿,几百行new语句
  • 对象生命周期难管理——Activity销毁了,对象还在吗?
  • 作用域混乱——单例、Activity作用域、Fragment作用域,手动维护很痛苦

这时候就需要一个框架来帮我们做这些脏活累活。在Android生态里,Google官方推荐的是Hilt

我的建议:小项目可以手动DI,但一旦超过5个模块,直接上Hilt。省下的时间远大于学习成本。

5.3 Hilt是什么?

Hilt是Google基于Dagger封装的Android专用DI框架。它解决了Dagger配置繁琐的问题,同时保留了Dagger的编译期检查和性能优势。

说白了,Hilt就是Dagger的「傻瓜版」。你不需要写@Component、@Module、@Binds这些复杂注解,Hilt帮你自动生成。

Hilt的核心概念就这几个:

概念 作用 类比
@HiltAndroidApp 标记Application类,Hilt的入口 总开关
@AndroidEntryPoint 标记Activity/Fragment/View等,启用注入 插座
@Inject 告诉Hilt「这个对象我来提供」或「我需要这个对象」 插头
@Module 当第三方类无法加@Inject时,用Module提供 适配器
@Provides 在Module中声明如何创建对象 工厂方法
@Singleton 标记为全局单例 全局唯一

下面这张图展示了Hilt在MVVM架构中的位置和作用:

Hilt在MVVM架构中的依赖注入流程 UI层 Activity / Fragment @AndroidEntryPoint ViewModel层 MainViewModel @HiltViewModel 数据层 UserRepository @Inject constructor 注入 注入 Hilt 依赖容器(编译期生成) @Module @Provides @Singleton 自动管理对象创建、生命周期、作用域

5.4 Hilt的配置步骤

配置Hilt其实就三步。我带你走一遍。

5.4.1 添加依赖

在项目根目录的 build.gradle 中:

// build.gradle (Project级别)
plugins {
    id 'com.google.dagger.hilt.android' version '2.48' apply false
}

在 app/build.gradle 中:

// app/build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'com.google.dagger.hilt.android'
    id 'kotlin-kapt'
}

android {
    // ... 你的配置
}

dependencies {
    implementation 'com.google.dagger:hilt-android:2.48'
    kapt 'com.google.dagger:hilt-compiler:2.48'
    
    // 如果要用Hilt ViewModel
    implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
    // 或者
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
}

注意:Hilt使用kapt进行编译期代码生成。如果你迁移到了KSP,需要额外配置。我个人建议在稳定版本中继续用kapt,等Hilt官方全面支持KSP再迁移。

5.4.2 创建Application类并添加注解

@HiltAndroidApp
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 初始化工作
    }
}

别忘了在AndroidManifest.xml中声明:

<application
    android:name=".MyApplication"
    ...>
    ...
</application>

5.4.3 在Activity中启用注入

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var repository: UserRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // repository 已经自动注入好了,直接用
        repository.getUser()
    }
}

就这么简单。你不需要写任何工厂类,不需要手动传参。Hilt在编译期自动生成了所有依赖图。

5.5 基本使用:@Inject 和 @Module

刚才的例子中,UserRepository 是通过 @Inject constructor 提供的。但有些类我们没法加 @Inject,比如 Retrofit、OkHttpClient、Room 数据库等第三方库。

这时候就需要 @Module 了。

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

这里有几个关键点:

  • @InstallIn(SingletonComponent::class):表示这个Module里的对象是全局单例,整个App生命周期内只创建一次。
  • @Provides:告诉Hilt「这个对象怎么创建」。
  • @Singleton:标记为单例。如果不加,每次注入都会创建新对象。

避坑指南:我曾经犯过一个错误——在Module里用@Singleton标记了Retrofit,但忘了标记OkHttpClient。结果每次注入Retrofit时,OkHttpClient都是新的,导致连接池失效。记住:依赖链上的所有对象都要统一作用域。

5.6 Hilt的作用域

Hilt提供了几种作用域,控制对象的生命周期:

作用域 生命周期 使用场景
@Singleton 整个App进程 Retrofit、Room、SharedPreferences
@ActivityScoped Activity创建到销毁 Activity相关的导航、资源
@FragmentScoped Fragment创建到销毁 Fragment特有的逻辑
@ViewModelScoped ViewModel创建到销毁 ViewModel持有的数据

选择作用域的原则很简单:对象需要活多久,就用对应的作用域。别一股脑全用@Singleton,那会导致内存泄漏。

5.7 小结

依赖注入不是银弹,但它解决了Android开发中一个很实际的问题——对象管理混乱。

Hilt让DI变得简单。你只需要记住三个核心注解:@HiltAndroidApp、@AndroidEntryPoint、@Inject。剩下的,Hilt在编译期帮你搞定。

嗯,这一章的内容就到这。配置好Hilt后,下一章我们会用它来串联整个MVVM架构。

核心要点回顾

  • DI的核心是「别自己new,让别人送」
  • Hilt是Dagger的Android封装,配置简单
  • @Inject用于自己写的类,@Module用于第三方库
  • 作用域控制对象生命周期,别滥用@Singleton

公众号:蓝海资料掘金营,微信deep3321