11、Hilt依赖注入(上):Hilt的组件层次与作用域、@Module与@Provides的用法、@Binds与接口注入、Hilt在ViewModel中的集成

依赖注入,说白了就是帮你把对象“组装”好,你不用自己 new。我刚开始接触 Android 时,最烦的就是在 Activity 里写一堆初始化代码。后来用了 Dagger,又觉得配置太复杂。直到 Google 推出了 Hilt,我才觉得依赖注入终于“接地气”了。

这一章,我们来聊聊 Hilt 的核心机制。我会结合自己踩过的坑,带你搞懂组件层次、作用域、Module 写法,还有 ViewModel 的集成。

Hilt 的组件层次与作用域

Hilt 的组件层次,说白了就是一套“生命周期管理”机制。每个组件都对应 Android 的一个生命周期节点。

我画了一张图,帮你快速理解:

Hilt 组件层次结构 SingletonComponent @Singleton / Application 级别 ActivityRetainedComponent @ActivityRetainedScoped ViewModelComponent @ViewModelScoped ActivityComponent @ActivityScoped FragmentComponent @FragmentScoped ServiceComponent @ServiceScoped ViewComponent @ViewScoped ViewWithFragmentComponent @ViewScoped 箭头方向:上层组件可以访问下层组件的依赖 下层组件不能访问上层组件的依赖(作用域限制)

从上图可以看到,Hilt 的组件是分层的。最顶层是 SingletonComponent,对应 Application 的生命周期。往下依次是 Activity、Fragment、View 等。

每个组件都有自己的作用域注解。比如 @Singleton 对应 Application 级别,@ActivityScoped 对应 Activity 级别。你想想看,如果一个对象被标记为 @Singleton,那它在整个应用里只有一个实例。如果标记为 @ActivityScoped,那它在同一个 Activity 里是单例的。

核心原则:上层组件可以访问下层组件的依赖,但下层组件不能访问上层组件的依赖。说白了,就是“爸爸可以拿儿子的东西,儿子不能拿爸爸的东西”。

我在项目中遇到过一个问题:有个网络请求的 Retrofit 实例,我把它放在了 @ActivityScoped 里。结果每次旋转屏幕,Activity 重建,Retrofit 也跟着重建。后来我改成 @Singleton,问题就解决了。嗯,这里要注意,作用域选错了,性能会受影响。

@Module 与 @Provides 的用法

有些类不是我们自己写的,比如 Retrofit、OkHttp、Room 数据库。这些第三方类,我们没法给它们加 @Inject 注解。这时候就需要 @Module@Provides 来帮忙。

说白了,@Module 就是一个“生产车间”,@Provides 就是车间里的“生产线”。

来看一个例子:

@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()
    }
}

这里有几个关键点:

  • @InstallIn(SingletonComponent::class) 表示这个 Module 安装在 SingletonComponent 里,也就是 Application 级别。
  • @Provides 告诉 Hilt:“嘿,这个函数能生产某个类型的对象”。
  • 函数参数会自动注入。比如 provideRetrofit 需要 OkHttpClient,Hilt 会自动从容器里找。

我的习惯:我会把网络相关的 Module 单独放在一个 di 包下,命名为 NetworkModule。数据库、本地存储也各自独立。这样找起来方便,也符合单一职责。

你可能会问:为什么用 object 而不是 class?因为 Module 里通常没有状态,用 object 更简洁。如果 Module 需要构造函数参数,那就用 class

@Binds 与接口注入

接口注入,说白了就是“面向接口编程”。我们定义一个接口,然后告诉 Hilt:“当有人要这个接口时,给他这个实现类”。

举个例子:

// 定义接口
interface UserRepository {
    fun getUser(): User
}

// 实现类
class UserRepositoryImpl @Inject constructor(
    private val apiService: ApiService
) : UserRepository {
    override fun getUser(): User {
        return apiService.fetchUser()
    }
}

// Module 绑定
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    @Singleton
    abstract fun bindUserRepository(
        userRepositoryImpl: UserRepositoryImpl
    ): UserRepository
}

这里用 @Binds 而不是 @Provides。为什么?因为 @Binds 更高效。它告诉 Hilt:“这个接口的实现类就是它,你直接拿过去用”。而 @Provides 需要执行一个函数,多了一层调用。

我曾经踩过的坑:一开始我分不清 @Binds@Provides,全都用 @Provides。后来发现编译时间变长了,而且代码也不够清晰。记住:@Binds 用于接口绑定,@Provides 用于第三方类或复杂构造。

另外,@Binds 方法必须是抽象的,而且 Module 必须是 abstract class,不能是 object。这个细节要注意。

Hilt 在 ViewModel 中的集成

ViewModel 的注入,是 Hilt 最常用的场景之一。以前我们要用 ViewModelProvider.Factory,写一堆模板代码。有了 Hilt,一切都变得简单了。

首先,在 ViewModel 上加上 @HiltViewModel 注解:

@HiltViewModel
class MainViewModel @Inject constructor(
    private val userRepository: UserRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _user = MutableStateFlow<User?>(null)
    val user: StateFlow<User?> = _user.asStateFlow()

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

然后,在 Activity 或 Fragment 里,直接用 by viewModels()by hiltViewModel()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel.loadUser()
    }
}

就这么简单。Hilt 会自动创建 ViewModel,并注入它的依赖。你不需要写任何 Factory。

注意:使用 @HiltViewModel 时,ViewModel 的构造函数必须用 @Inject 注解。而且 SavedStateHandle 是自动注入的,不需要你手动提供。

我记得有一次,同事问我:“为什么我的 ViewModel 里拿不到 Application 上下文?” 我一看,他用了 @Inject 但没加 @HiltViewModel。嗯,这个注解不能少,它是 Hilt 识别 ViewModel 的关键。

另外,如果你需要在 ViewModel 里使用 Application 上下文,可以用 @ApplicationContext 注解:

@HiltViewModel
class MyViewModel @Inject constructor(
    @ApplicationContext private val context: Context,
    private val repository: UserRepository
) : ViewModel() {
    // ...
}

但我不建议这么做。ViewModel 应该尽量不依赖 Context,除非你真的需要。比如获取系统服务或读取资源文件。

小结

这一章我们聊了 Hilt 的组件层次、作用域、Module 和 Provides、Binds 接口注入,还有 ViewModel 的集成。说白了,Hilt 就是帮你管理对象的生命周期和依赖关系,让你少写模板代码。

我个人觉得,Hilt 最大的价值在于“约定优于配置”。你只要按照规则写注解,它就能自动帮你搞定一切。当然,刚开始接触时可能会觉得注解太多,但用习惯了就会发现,它真的能提升开发效率。

下一章,我们会继续深入 Hilt,聊聊多模块项目中的依赖注入、自定义组件,以及一些高级用法。到时候见。


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