依赖注入(Hilt/Dagger):Hilt基础、@Inject、@Module、@Provides、组件作用域

依赖注入,说白了就是「我要什么,你给我什么」。

我刚开始接触 Android 开发时,习惯直接在 Activity 里 new 对象。后来项目越来越大,改一处代码要牵连好几个文件,那叫一个痛苦。直到我遇到了 Dagger,后来又等来了官方亲儿子 Hilt——嗯,真香。

为什么需要依赖注入?

你想想看,一个类如果自己负责创建所有依赖,它就得知道太多东西了。比如一个网络请求的 ViewModel,它要自己创建 Retrofit、OkHttp、Gson……这哪是业务逻辑,这分明是搬砖。

依赖注入的核心思想就一句话:把创建对象的权力交出去。谁用谁声明,谁创建谁负责。

依赖注入的好处:

  • 解耦:类不再依赖具体实现,只依赖接口
  • 可测试:轻松替换 Mock 对象
  • 可复用:同一个依赖可以在多处共享
  • 生命周期管理:作用域控制对象的创建和销毁

Hilt 是什么?

Hilt 是 Google 基于 Dagger 封装的依赖注入框架。它简化了 Dagger 的配置,让你少写很多模板代码。

我个人习惯用 Hilt 替代 Dagger,除非项目对包体积有极致要求。Hilt 的编译期注解处理虽然会多生成一些代码,但开发效率提升是实打实的。

Hilt 基础配置

首先,在项目根 build.gradle 添加 Hilt 插件:

// 根 build.gradle
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'
}

最后,在 Application 类上添加 @HiltAndroidApp

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

小提示:别忘了在 AndroidManifest.xml 中声明你的 Application 类。

@Inject:告诉 Hilt 怎么创建对象

@Inject 有两个用途:

  • 标注构造函数:告诉 Hilt「这个类我可以自己创建」
  • 标注字段:告诉 Hilt「我需要这个依赖,你给我注入」

看个例子:

// 告诉 Hilt:UserRepository 可以通过构造函数创建
class UserRepository @Inject constructor(
    private val apiService: ApiService
) {
    fun getUser(): User {
        return apiService.fetchUser()
    }
}

// 在 Activity 中使用
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var userRepository: UserRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // userRepository 已经被注入了,直接用
        val user = userRepository.getUser()
    }
}

这里要注意:@AndroidEntryPoint 是 Hilt 为 Android 组件提供的入口注解。Activity、Fragment、Service、BroadcastReceiver 都需要加这个。

我曾经踩过的坑:在 Fragment 中使用 @Inject 注入时,一定要在 onAttach 之后才能访问注入的字段。如果在构造函数里就访问,会报空指针。因为 Hilt 的注入时机是在 onAttach 之后。

@Module 和 @Provides:当 Hilt 不会自己创建时

有些类不是你能控制的,比如 Retrofit、OkHttp、Room 数据库。这时候就需要 @Module@Provides 出场了。

@Module 标注一个类,告诉 Hilt「这里面有创建依赖的方法」。

@Provides 标注方法,告诉 Hilt「用这个方法创建依赖」。

看代码:

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

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

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

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

这里有几个关键点:

  • @InstallIn(SingletonComponent::class):指定这个 Module 安装在哪个组件中。SingletonComponent 表示全局单例。
  • @Singleton:作用域注解,表示这个依赖在整个应用中只有一个实例。
  • 方法参数会自动注入:比如 provideRetrofit 需要 OkHttpClient,Hilt 会自动从容器中找。

核心原则:能用 @Inject 的尽量用 @Inject,只有第三方库才用 @Module + @Provides。这样代码更简洁,也更容易维护。

组件作用域:控制对象的生命周期

作用域决定了依赖的存活时间。Hilt 提供了几个内置的作用域:

作用域 对应组件 生命周期
@Singleton SingletonComponent 整个应用生命周期
@ActivityScoped ActivityComponent Activity 生命周期
@FragmentScoped FragmentComponent Fragment 生命周期
@ViewModelScoped ViewModelComponent ViewModel 生命周期
@ServiceScoped ServiceComponent Service 生命周期

为什么要用作用域?

举个例子:你的网络请求库应该是全局单例,因为创建 OkHttpClient 很重。但某个 Activity 专用的数据缓存,就应该在 Activity 销毁时一起清理。

@ActivityScoped
class UserSession @Inject constructor(
    private val context: Activity
) {
    // 这个对象只在 Activity 存活期间存在
    private val cache = mutableMapOf<String, Any>()

    fun put(key: String, value: Any) {
        cache[key] = value
    }

    fun get(key: String): Any? {
        return cache[key]
    }
}

注意:作用域不能乱用。如果你给一个依赖加了 @ActivityScoped,那么所有注入这个依赖的地方都会拿到同一个实例。如果某个 Fragment 想独立持有自己的实例,就不能用作用域,或者用更小的作用域。

Hilt 的组件层级

Hilt 的组件是有层级关系的。子组件可以访问父组件的依赖,但反过来不行。

Hilt 组件层级结构 SingletonComponent @Singleton · 应用全局 ActivityComponent @ActivityScoped · Activity 生命周期 FragmentComponent @FragmentScoped · Fragment 生命周期 ViewModelComponent 作用域规则 • 父组件依赖子组件可用 • 子组件依赖父组件不可用 • @Singleton 全局唯一 • @ActivityScoped 每个 Activity 一个实例 • @FragmentScoped 每个 Fragment 一个实例 • @ViewModelScoped 每个 ViewModel 一个实例 • 不加作用域 = 每次 注入都创建新实例

这张图展示了 Hilt 的组件层级。从上到下依次是:SingletonComponent → ActivityComponent → FragmentComponent → ViewModelComponent。

每个子组件都可以访问父组件的依赖。比如 Fragment 可以拿到 Activity 的依赖,但 Activity 拿不到 Fragment 的。

实战:一个完整的 Hilt 示例

把上面学到的知识串起来,写一个完整的例子:

// 1. Application
@HiltAndroidApp
class MyApp : Application()

// 2. 网络模块
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder().build()

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

    @Provides
    @Singleton
    fun provideApi(retrofit: Retrofit): ApiService =
        retrofit.create(ApiService::class.java)
}

// 3. Repository
class UserRepository @Inject constructor(
    private val api: ApiService
) {
    suspend fun getUser(): User = api.getUser()
}

// 4. ViewModel
@HiltViewModel
class MainViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {
    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user.asStateFlow()

    fun loadUser() {
        viewModelScope.launch {
            _user.value = repo.getUser()
        }
    }
}

// 5. Activity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.loadUser()
    }
}

我的习惯:把 Module 按功能拆分,比如 NetworkModule、DatabaseModule、AnalyticsModule。每个 Module 只负责一类依赖,这样好找也好改。

避坑指南

我用 Hilt 这几年,踩过不少坑。分享几个最常见的:

  • @HiltViewModel 必须配合 by viewModels() 使用:不能自己 new ViewModel,否则注入不生效。
  • Fragment 的 @AndroidEntryPoint 别忘了:我经常写完 Activity 就忘了 Fragment 也要加。
  • @Module 里的方法不要用 private:Hilt 是通过编译期生成代码来调用的,private 方法它调不了。
  • 作用域要匹配:如果你在 SingletonComponent 里提供了一个 @ActivityScoped 的依赖,Hilt 会报错。因为父组件不能提供子组件作用域的依赖。

我曾经遇到的一个诡异问题:在 Fragment 里注入了一个 @ActivityScoped 的依赖,结果 Fragment 销毁重建后,拿到的还是旧的实例。后来才发现,@ActivityScoped 的实例在 Activity 销毁前一直存在。如果 Fragment 需要独立实例,要么不加作用域,要么用 @FragmentScoped。

总结

Hilt 的核心就三件事:

  1. @Inject:告诉 Hilt 怎么创建你的类
  2. @Module + @Provides:告诉 Hilt 怎么创建第三方库的类
  3. 作用域:控制依赖的生命周期

说白了,Hilt 就是一个帮你管理对象创建和生命周期的工具。用好了,代码清爽,测试方便。用不好,各种空指针和生命周期问题够你喝一壶的。

我个人建议:从小项目开始用 Hilt,慢慢体会它的设计思路。别一上来就搞复杂的多 Module 项目,容易把自己绕晕。


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