15、Hilt进阶:@Binds、@Qualifier、@Singleton、ViewModel注入、Fragment注入、实战:模块化注入
好,咱们继续聊Hilt。上一章我们把Hilt的入门姿势捋了一遍,你大概已经能跑通一个基本的依赖注入了。但说实话,那只是冰山一角。真正到了项目里,你会发现事情远没那么简单——接口要绑定实现、同一个类型要区分不同实例、ViewModel和Fragment怎么注入、多模块项目怎么解耦……这些才是日常开发里天天要面对的问题。
我个人习惯把Hilt的进阶用法分成三个层次:绑定技巧、作用域管理、模块化架构。咱们一个一个来拆。
15.1 @Binds:接口与实现解耦
先问个问题:如果你的代码里依赖的是一个接口,Hilt怎么知道该注入哪个实现?
嗯,这就是@Binds的用武之地。它专门用来告诉Hilt:「当有人要这个接口时,你给我这个实现。」
看个例子。假设我们有一个日志接口:
interface Logger {
fun log(message: String)
}
class FileLogger @Inject constructor() : Logger {
override fun log(message: String) {
// 写入文件
}
}
现在我们要在Module里绑定它。注意,@Binds必须用在抽象方法上,而且方法参数就是实现类,返回值是接口:
@Module
@InstallIn(SingletonComponent::class)
abstract class LoggerModule {
@Binds
abstract fun bindLogger(fileLogger: FileLogger): Logger
}
这里有个坑,我刚开始用的时候踩过——@Binds方法不能和@Provides混在同一个抽象类里。如果你既有抽象绑定又有具体提供,得拆成两个Module,或者把@Provides放到companion object里。
15.2 @Qualifier:区分同类型的不同实例
有时候你会遇到这种情况:同一个接口,需要注入两个不同的实现。比如一个本地缓存、一个远程缓存,都是Cache接口。这时候Hilt就懵了——你到底要哪个?
解决方案就是自定义限定符。用@Qualifier注解给每个实现打上标签。
先定义两个注解:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LocalCache
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RemoteCache
然后在Module里分别提供:
@Module
@InstallIn(SingletonComponent::class)
object CacheModule {
@LocalCache
@Provides
fun provideLocalCache(): Cache = LocalCacheImpl()
@RemoteCache
@Provides
fun provideRemoteCache(): Cache = RemoteCacheImpl()
}
注入的时候带上限定符:
class Repository @Inject constructor(
@LocalCache private val localCache: Cache,
@RemoteCache private val remoteCache: Cache
) { ... }
15.3 @Singleton:控制对象生命周期
默认情况下,Hilt每次注入都会创建一个新实例。但有些对象我们希望全局唯一,比如数据库实例、网络客户端。这时候就用@Singleton。
用法很简单,在提供方法上加注解:
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
或者直接在实现类上加:
@Singleton
class UserRepository @Inject constructor(
private val api: ApiService
) { ... }
这里要理解一个关键点:@Singleton的作用域是Component级别的。如果你用的是SingletonComponent,那这个对象在整个应用生命周期内只有一个实例。如果你用的是ActivityComponent,那@Singleton就不适用了,得用@ActivityScoped。
| 作用域注解 | 绑定的Component | 生命周期 |
|---|---|---|
| @Singleton | SingletonComponent | Application |
| @ActivityScoped | ActivityComponent | Activity |
| @FragmentScoped | FragmentComponent | Fragment |
| @ViewModelScoped | ViewModelComponent | ViewModel |
15.4 ViewModel注入
ViewModel的注入是Hilt的一大亮点。以前我们要用ViewModelProvider.Factory,写一堆模板代码。现在只需要一个注解。
第一步,让ViewModel用@HiltViewModel注解:
@HiltViewModel
class MainViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
...
}
第二步,在Activity或Fragment里直接用by viewModels():
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 直接用,不用管Factory
}
}
你可能会问:如果ViewModel需要SavedStateHandle怎么办?Hilt也考虑到了,直接注入就行:
@HiltViewModel
class DetailViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: UserRepository
) : ViewModel() { ... }
15.5 Fragment注入
Fragment的注入和Activity类似,但有个细节要注意——Fragment必须配合宿主Activity一起使用@AndroidEntryPoint。如果Activity没加注解,Fragment的注入会直接报错。
看个完整例子:
@AndroidEntryPoint
class HomeFragment : Fragment() {
@Inject
lateinit var analytics: AnalyticsService
private val viewModel: HomeViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// analytics和viewModel都已经注入好了
}
}
这里有个常见的坑:@Inject lateinit var不能在onAttach之前使用。因为Hilt的注入发生在onAttach之后。如果你在构造函数里访问注入的字段,会拿到null。
15.6 实战:模块化注入
终于到了重头戏。真实项目往往是多模块的,比如app模块、core模块、feature模块。Hilt怎么跨模块注入?
核心思路是:每个模块提供自己的Module,然后通过@InstallIn指定到合适的Component。
假设我们有这样的模块结构:
- core:network — 提供ApiService、OkHttpClient
- core:database — 提供Room数据库、Dao
- feature:home — 依赖core模块,提供HomeViewModel
- app — 主模块,组装所有功能
在core:network模块里:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient { ... }
@Provides
@Singleton
fun provideApiService(client: OkHttpClient): ApiService { ... }
}
在feature:home模块里:
@Module
@InstallIn(ViewModelComponent::class)
object HomeModule {
@Provides
@ViewModelScoped
fun provideHomeUiState(): HomeUiState { ... }
}
然后在app模块的Application类上加上@HiltAndroidApp,所有模块的Module就会被自动发现和组装。
这里有个关键点:Hilt的模块发现是编译期的。只要你的模块依赖关系正确,Hilt就能跨模块找到对应的Provider。不需要像Dagger那样手动写Component。
最后,咱们用一张图来总结Hilt进阶的核心脉络:
说实话,Hilt的进阶用法并不复杂,但需要你在实际项目中反复练习。我刚开始做模块化注入时,也遇到过依赖找不到、作用域不匹配的问题。但只要你理解了绑定、作用域、组件注入这三个核心概念,再复杂的项目也能理清楚。
嗯,这一章的内容就到这。记住,Hilt不是银弹,但它能帮你把依赖管理的复杂度降到最低。用好它,你的代码会变得更干净、更可测试。
公众号:蓝海资料掘金营,微信deep3321