第3章:Hilt组件与作用域 —— @Singleton、@ActivityScoped等归纳总结
说实话,刚接触Hilt那会儿,我最头疼的就是这一坨注解。@Singleton、@ActivityScoped、@FragmentScoped……看得人眼花缭乱。到底什么时候该用哪个?用错了会怎样?
嗯,要搞懂这个,先得明白一个道理:作用域控制的是对象的生命周期。
说白了,你告诉Hilt:“这个对象我要用多久?是和App共存亡,还是Activity关闭时它就该消失?”
3.1 为什么需要作用域?
想象一下,你在Activity里注入了一个网络服务对象。如果不做任何限制,每次进入该Activity,Hilt都会创建新的实例。来回进出几次,内存里就躺着好几个相同的网络服务了。
我在项目中遇到过这种情况。一个新闻列表页,用户频繁滑动进入详情再返回,结果内存里攒了十几个ViewModel实例。界面倒是没崩,但滑动明显卡顿了。一查,就是作用域没设对。
作用域的作用就是:绑定额外的生命周期,让Hilt知道什么时候该重用对象,什么时候该清理。
3.2 Hilt的标准作用域注解
Hilt内置了几种标准作用域,每个都对应一个Android组件的生命周期。我们来看一下:
| 注解 | 绑定的生命周期 | 单例范围 | 典型使用场景 |
|---|---|---|---|
| @Singleton | Application | 整个App全局 | 网络客户端、数据库、SharedPreferences |
| @ActivityRetainedScoped | Activity(含配置变更) | Activity存活期间 | ViewModel、配置不变时需保留的对象 |
| @ActivityScoped | Activity | Activity创建到销毁 | 与Activity绑定的业务逻辑、UI控制器 |
| @FragmentScoped | Fragment | Fragment创建到销毁 | Fragment内使用的特殊依赖 |
| @ViewScoped | View | View存活期间 | 自定义View内的临时依赖 |
| @ServiceScoped | Service | Service创建到销毁 | 后台服务内使用的依赖 |
3.3 具体注解详解
3.3.1 @Singleton —— 全局单例
这是作用域里“老大”,绑定在Application的生命周期上。一个App里,用@Singleton标记的依赖只会生成一个实例,直到进程结束。
@Module
@InstallIn(SingletonComponent::class)
object NetModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}
换个角度理解:你写的每个网络请求都经过同一个OkHttpClient,连接池、缓存都能共用。要是每个Activity都new一个,那连接就不互通了,性能也会下降。
3.3.2 @ActivityRetainedScoped —— 配置变更不死
这个是Hilt为了方便ViewModel保留而设计的。屏幕旋转时Activity会重建,但@ActivityRetainedScoped的对象不会跟着销毁——它们被保留在新创建的Activity里。
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepo: UserRepository,
@ActivityRetainedScoped private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// analyticsTracker会在旋转后继续工作
// 不需要重新创建
}
你的场景:比如正在上传一个大文件的进度追踪器,屏幕转了一下就丢了进度,你想想用户得多崩溃。
3.3.3 @ActivityScoped —— 与Activity共存亡
和Activity生命周期严格绑定。Activity创建时生成实例,Activity销毁时实例跟着销毁。如果你在Activity里使用@ActivityScoped,它会在这个Activity的所有注入点中复用。
@ActivityScoped
class MusicPlayerManager @Inject constructor(
private val audioFocusHelper: AudioFocusHelper
) {
// Activity内音乐播放的管理器
// 每次Activity重建都会new一个
}
为什么不用@Singleton?因为音乐播放器有时只需要在当前页面工作,切到其他App页面时,跟当前Activity相关的播放状态就可以释放掉了。
3.3.4 @FragmentScoped —— 更精细的控制
适用于Fragment内部的临时依赖。比如一个只在FragmentA里使用的数据加载器,其他Fragment都不需要。
@FragmentScoped
class ImagePreloader @Inject constructor(
private val imageLoader: ImageLoader
) {
// 只在当前Fragment中预加载图片
// Fragment销毁后自动释放
}
经验之谈:我个人通常只在Fragment需要持有一些只在当前页面有用的对象时才用@FragmentScoped。大部分时候,把依赖提到Activity层就够用了。
3.4 作用域与组件层级的关系
别以为选了注解就完事了。你还得注意组件层级——注解要和对应的组件配套使用。
Hilt的组件层级是从上到下,逐层包含的:
- SingletonComponent(Application级别)—— 最顶层,所有组件都能用
- ActivityRetainedComponent —— 位于Activity的上一层
- ActivityComponent —— Activity级别
- FragmentComponent —— Fragment级别
- ViewComponent —— View级别
- ViewWithFragmentComponent —— 在Fragment内的View
- ServiceComponent —— Service级别
你想想看,每个上层组件都能访问下层组件的依赖吗?不行。下层组件可以访问上层组件的依赖,但上层组件不能访问下层组件的依赖——因为上层组件在生命周期上更早创建,下层组件可能还没创建呢。
// 正确:ActivityComponent可以访问Singleton提供的依赖
@ActivityScoped
class ActivityDataFetcher @Inject constructor(
private val apiClient: ApiClient // 来自SingletonComponent
)
// 错误:SingletonComponent不能访问ActivityScoped的依赖
// @Singleton
// class GlobalManager @Inject constructor(
// private val fetcher: ActivityDataFetcher // ❌ 编译报错
// )
3.5 作用域使用的思维导图
我把自己日常判断作用域的逻辑画成一张图。照着它走,一般不出错:
3.6 作用域常见陷阱总结
- 陷阱1:在@Singleton的对象里持有Activity的Context。这会导致Activity无法被回收。
- 陷阱2:Fragment里用@ActivityScoped,Fragment之间虽然共享实例,但如果在FragmentA修改了状态,FragmentB会受到影响——有时候这正是你想要的,但有时候不是。
- 陷阱3:过度使用@Singleton,导致对象不易清理。记住:Singleton是“永久”,不是“方便”。
好,总结一下。作用域这一块,其实没那么玄乎。核心就是:想清楚你的对象该活多久,然后对上号。和组件层级匹配好,Hilt自然会帮你管好生命周期。
还有一点我的小经验:建议先从无作用域开始(即每次创建新实例),跑通了再逐步加上作用域优化。一上来就加@Singleton,容易不小心埋下坑。