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,结果内存泄漏查了半天。选择作用域的时候,可以问自己三个问题:

  1. 这个对象需要跨多个Activity共享吗?→ 用 @Singleton
  2. 这个对象只在当前Activity内有效吗?→ 用 @ActivityScoped
  3. 这个对象需要跟随ViewModel的存活周期吗?→ 用 @ViewModelScoped

如果都不满足,那就别加作用域。Hilt默认每次注入都会创建新实例,这在很多场景下反而是最安全的。

6.6 作用域层级关系

Hilt的作用域是有层级关系的。简单来说:

  • SingletonComponent 是最顶层的
  • ActivityComponent 在它下面
  • FragmentComponent 又在 ActivityComponent 下面
  • ViewModelComponent 是独立的一条线

这意味着,你可以在 @Singleton 的模块里注入 @ActivityScoped 的对象吗?不行。反过来,在 @ActivityScoped 的模块里注入 @Singleton 的对象是可以的。说白了,作用域只能从大到小引用,不能反过来。

Hilt 作用域层级关系 @Singleton (SingletonComponent) @ActivityScoped (ActivityComponent) @FragmentScoped (FragmentComponent) @ViewModelScoped (ViewModelComponent) 箭头方向表示“可以注入”的方向 上层作用域可以注入下层作用域的对象,反之不行

这张图很直观地展示了作用域的层级。我建议你在写代码之前,先在脑子里过一遍这个层级关系,避免出现编译错误。

💡 个人经验
我一般会这样分配作用域:网络层和数据库用 @Singleton,页面状态管理用 @ActivityScoped,列表项的数据加载用 @FragmentScoped,而需要保留在ViewModel里的数据用 @ViewModelScoped。这样分层清晰,出了问题也好排查。

好了,作用域这块就讲到这里。记住一句话:作用域越小,越安全;作用域越大,越方便。找到平衡点,才是高手。


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