6. Hilt作用域:@Singleton、@ActivityScoped、@FragmentScoped、@ViewModelScoped
依赖注入里有个很核心的问题——对象的生命周期。说白了,就是对象该什么时候创建,什么时候销毁。我刚开始用Dagger的时候,就踩过这个坑:一个本该跟着Activity走的网络请求对象,因为没加作用域,结果在内存里活了好几个页面,数据全乱套了。
Hilt提供了几个现成的作用域注解,帮你精确控制对象的存活范围。咱们一个一个来看。
6.1 @Singleton:全局单例
@Singleton 是最简单粗暴的作用域。被它标记的对象,在整个Application的生命周期内只有一个实例。适合放那些全局共享的东西,比如 Retrofit 实例、OkHttpClient、数据库对象。
@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()
}
}
这里有个细节要注意:@Singleton 必须配合 @InstallIn(SingletonComponent::class) 使用。你想想看,如果模块装在别的Component里,那单例的范围就不对了。
我曾经在一个项目里,把某个网络请求的缓存对象标记为 @Singleton,结果用户切换账号后,缓存里还是旧数据。全局单例虽然方便,但一定要想清楚:这个对象真的需要在整个App生命周期内都存在吗?
6.2 @ActivityScoped:跟着Activity走
有些对象只在一个Activity的范围内有效。比如某个页面的状态管理器、页面级别的数据仓库。这时候用 @ActivityScoped 就对了。
@Module
@InstallIn(ActivityComponent::class)
object MainActivityModule {
@Provides
@ActivityScoped
fun providePageStateManager(): PageStateManager {
return PageStateManager()
}
}
被 @ActivityScoped 标记的对象,会在Activity创建时生成,Activity销毁时自动释放。我个人的习惯是,把那些需要持有Activity上下文的对象,都加上这个作用域。比如图片加载的Glide实例,或者需要监听Activity生命周期的工具类。
如果你在 Activity 里注入了一个 @ActivityScoped 的对象,记得在 onDestroy 里做清理工作。虽然Hilt会帮你释放引用,但有些资源(比如网络请求的订阅)还是得手动取消。
6.3 @FragmentScoped:Fragment级别的隔离
Fragment的生命周期比Activity更复杂。同一个Activity里可能有好几个Fragment,它们之间不应该共享某些状态。这时候 @FragmentScoped 就派上用场了。
@Module
@InstallIn(FragmentComponent::class)
object DetailFragmentModule {
@Provides
@FragmentScoped
fun provideDetailViewModelFactory(
repository: DetailRepository
): DetailViewModelFactory {
return DetailViewModelFactory(repository)
}
}
嗯,这里要注意一点:@FragmentScoped 的对象只在该Fragment的实例内有效。如果用户旋转屏幕导致Fragment重建,那这个对象也会重新创建。我在做电商项目时,商品详情页的加载状态就是用 @FragmentScoped 管理的,这样每个商品详情Fragment都有自己的状态,互不干扰。
6.4 @ViewModelScoped:ViewModel的专属伴侣
这个作用域是Hilt专门为ViewModel设计的。被 @ViewModelScoped 标记的对象,会跟ViewModel同生共死。它比 @FragmentScoped 更精细——即使Fragment重建了,只要ViewModel还在,这个对象就还在。
@Module
@InstallIn(ViewModelComponent::class)
object UserModule {
@Provides
@ViewModelScoped
fun provideUserStateHolder(): UserStateHolder {
return UserStateHolder()
}
}
@HiltViewModel
class UserViewModel @Inject constructor(
private val userStateHolder: UserStateHolder,
private val userRepository: UserRepository
) : ViewModel() {
// ViewModel 的逻辑
}
为什么会需要 @ViewModelScoped?你想想看,如果某个数据需要在屏幕旋转后仍然保留,但又不适合放在全局单例里,那ViewModelScoped就是最佳选择。我经常用它来管理分页加载的状态——页面旋转后,加载进度和当前页码都能保留。
- @Singleton:整个App只有一个实例,适合全局工具类
- @ActivityScoped:跟着Activity走,Activity销毁就释放
- @FragmentScoped:跟着Fragment走,Fragment重建就重新创建
- @ViewModelScoped:跟着ViewModel走,屏幕旋转也不丢失
6.5 作用域的选择策略
说实话,作用域不是越大越好。我见过有人把所有东西都标成 @Singleton,结果内存泄漏查了半天。选择作用域的时候,可以问自己三个问题:
- 这个对象需要跨多个Activity共享吗?→ 用 @Singleton
- 这个对象只在当前Activity内有效吗?→ 用 @ActivityScoped
- 这个对象需要跟随ViewModel的存活周期吗?→ 用 @ViewModelScoped
如果都不满足,那就别加作用域。Hilt默认每次注入都会创建新实例,这在很多场景下反而是最安全的。
6.6 作用域层级关系
Hilt的作用域是有层级关系的。简单来说:
- SingletonComponent 是最顶层的
- ActivityComponent 在它下面
- FragmentComponent 又在 ActivityComponent 下面
- ViewModelComponent 是独立的一条线
这意味着,你可以在 @Singleton 的模块里注入 @ActivityScoped 的对象吗?不行。反过来,在 @ActivityScoped 的模块里注入 @Singleton 的对象是可以的。说白了,作用域只能从大到小引用,不能反过来。
这张图很直观地展示了作用域的层级。我建议你在写代码之前,先在脑子里过一遍这个层级关系,避免出现编译错误。
我一般会这样分配作用域:网络层和数据库用 @Singleton,页面状态管理用 @ActivityScoped,列表项的数据加载用 @FragmentScoped,而需要保留在ViewModel里的数据用 @ViewModelScoped。这样分层清晰,出了问题也好排查。
好了,作用域这块就讲到这里。记住一句话:作用域越小,越安全;作用域越大,越方便。找到平衡点,才是高手。