2. Hilt依赖注入基础:@Inject、@Module、@Provides
好,咱们正式开始动手。这章是Hilt的基石,说白了就是搞懂三样东西:@Inject、@Module 和 @Provides。
你想想看,Hilt本质上是个编译期注解处理器。它帮我们生成代码,在运行时把依赖“喂”给需要的对象。那么问题来了,它怎么知道该喂什么、怎么喂?
嗯,答案就在这三个注解里。我刚开始用Dagger的时候,也被这三兄弟搞晕过。今天咱们就捋清楚它们各自管什么、怎么搭配用。
核心概念一句话总结:
@Inject告诉Hilt:“嘿,这个东西需要你帮忙创建,或者它能创建别人需要的东西。”@Module是“工具箱”或“说明书”,用来告诉Hilt那些它自己搞不定的依赖该怎么造。@Provides是“说明书”里的具体条目,就是“造这个东西的步骤”。
2.1 @Inject:告诉Hilt“我要什么,我能造什么”
@Inject 主要有两个用法,你别把它想复杂了。
用法一:构造方法注入
把你的类的构造方法加上 @Inject。Hilt看到后就会知道:“哦,这个类我能帮你new出来。”我自己特别喜欢这种方式,因为它最直观。
class AnalyticsService @Inject constructor(
private val remoteDataSource: RemoteDataSource,
private val localCache: LocalCache
) {
fun trackEvent(eventName: String) {
// ...
}
}
我的习惯:能用构造注入的,我绝不用字段注入。原因很简单——构造注入让依赖关系一目了然,单元测试也好mock。
用法二:字段注入
用在Activity、Fragment这类由系统创建实例的类上。你不能改它们的构造方法,所以只能注入到字段。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var analyticsService: AnalyticsService
@Inject lateinit var userManager: UserManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 此时 analyticsService 和 userManager 已经可用
}
}
注意!字段注入的变量必须是 lateinit var,而且注解写在字段上。我曾经见过有人把 @Inject 写在 getter 上……嗯,编译倒是能过,但运行时会崩得很惨。
2.2 @Module:给Hilt配说明书
那问题来了——如果依赖是接口、抽象类、或者第三方的类(比如 OkHttp),我总不能去改它们的构造方法吧?
这时候就需要 @Module 出场了。它是一个容器,专门放那些“如何创建复杂依赖”的方法。
@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()
}
}
看到没?@Module 负责打包,@InstallIn 告诉Hilt“这个模块在哪个组件生效”。
关键点:模块里的方法用 @Provides 标注,返回什么类型,Hilt就知道它能提供什么依赖。
2.3 @Provides:手把手教Hilt造对象
@Provides 是 @Module 内部的“生产方法”。说白了,就是“要造这个对象,请按这个方法走”。
我简单列一下常用的几种场景,你可直接套用:
| 场景 | 示例 | 说明 |
|---|---|---|
| 创建接口实例 | @Provides fun provideRepo(impl: RepoImpl): Repo |
返回接口类型,Hilt自动注入实现类 |
| 创建第三方库对象 | @Provides fun provideOkHttp(): OkHttpClient |
框架之外的类必须由Module提供 |
| 创建带配置的对象 | @Provides fun provideGson(): Gson |
可以配置日期格式等选项 |
2.4 限定符:解决“同一个类型多个实例”
我在项目中遇到过最头疼的问题——两个 String 类型的依赖。一个代表API baseUrl,一个代表数据库名称。Hilt一看,都是 String,它就懵了。
解决方案?用 @Qualifier(限定符)。咱自己造一个注解来区分。
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BaseUrl
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DatabaseName
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@BaseUrl
@Provides
@Singleton
fun provideBaseUrl(): String = "https://api.example.com"
@DatabaseName
@Provides
@Singleton
fun provideDatabaseName(): String = "user_database"
}
// 使用
class UserRepository @Inject constructor(
@BaseUrl private val baseUrl: String,
@DatabaseName private val dbName: String
) { ... }
避坑指南:我曾经在团队里看到有人把 @Qualifier 和 @Named 混用。我个人建议统一用自定义 @Qualifier,因为 @Named 是字符串比较,重构时容易改漏。自定义注解安全得多。
2.5 Provider:懒加载与工厂模式
有时候你不想立刻创建对象,或者想每次都拿个新的。可以用 Provider。把依赖声明成 Provider<T>,每次调 .get() 就生产一个实例。
class LoginViewModel @Inject constructor(
private val userManagerProvider: Provider<UserManager>
) {
fun login() {
// 每次点击登录,都获得一个新的 UserManager 实例
val userManager = userManagerProvider.get()
userManager.performLogin()
}
}
注意,如果 UserManager 本身标记了 @Singleton,那 Provider.get() 返回的始终是同一个单例。这个细节很关键,我之前排查过一个bug,就是没搞清单例和Provider的协作规则。
2.6 本章小结
到这,Hilt依赖注入的三个核心注解你应该心里有数了:
- @Inject:我自己的类,用它标记构造;系统创建的类,用它标记字段。
- @Module + @Provides:第三方的、接口的、需要配置的依赖,放这里集中管理。
- @Qualifier:同类型不同实例时,用自定义注解区分。
- Provider:需要懒加载或每次获取新实例时使用。
一句话记住:Hilt依赖注入的底层逻辑就是——@Inject 提出需求,@Module + @Provides 给出生产方案,@Qualifier 解决歧义。