第3章 Hilt核心注解详解
Hilt 的注解体系,说白了就是一套「谁可以注入、谁来提供、怎么绑定」的约定。刚接触时你可能觉得注解太多,记不住。别急,我当年在重构一个老项目时也踩过不少坑——比如把 @InstallIn 写错作用域,导致运行时死活拿不到依赖。嗯,这一节我把最常用的几个注解掰开揉碎讲清楚。
3.1 @HiltAndroidApp — 应用的「总开关」
所有 Hilt 项目都必须有一个 @HiltAndroidApp 注解的 Application 类。它的作用是生成一个「父组件」,所有其他组件都从它派生。
@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// 注意:这里还不能直接注入,因为组件还未完全初始化
}
}
3.2 @AndroidEntryPoint — 让四大组件支持注入
@AndroidEntryPoint 标记 Activity、Fragment、Service、BroadcastReceiver。加了它,Hilt 才会自动帮你创建对应的组件,并把依赖注入进去。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// analytics 已经可用了
}
}
Fragment 也一样。但有个坑:如果你在 Fragment 的构造函数里写 @Inject constructor(),那这个 Fragment 就不能再被 Android 系统通过反射重建——怎么办?用 @AndroidEntryPoint 配合默认空构造,别自己加带参构造。
3.3 @Inject — 标记「我要注入」或「我能提供」
@Inject 有两种用法:
- 字段注入:在 Activity/Fragment 里写
@Inject lateinit var repo: Repository - 构造函数注入:在普通类上写
class Repository @Inject constructor(),告诉 Hilt 这个类可以被自动构造
我个人习惯尽量用构造函数注入,原因有二:一是字段不可空,二是不需要 lateinit。但注意,如果这个类需要上下文,就别硬用构造函数,而是用 @Provides 或 @Binds。
3.4 @Module 与 @InstallIn — 依赖的「工厂」
当你要注入的对象不是你写的(比如 Retrofit、OkHttp),或者构造方式需要参数,就需要一个 Module。Module 就像一个小车间,专门生产某种依赖。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}
@InstallIn 决定这个 Module 在哪个组件里生效。常见的有:
| 作用域 | 组件 | 生命周期 |
|---|---|---|
| 整个应用 | SingletonComponent | Application 创建到销毁 |
| Activity | ActivityComponent | Activity 周期 |
| Fragment | FragmentComponent | Fragment 周期 |
| ViewModel | ViewModelComponent | ViewModel 生命周期 |
举个我当年犯过的错:把数据库依赖放在 ActivityComponent 里,结果跳转 Activity 时重新创建了数据库实例,直接 OOM。记住:全局单例的东西一定放在 SingletonComponent。
3.5 @Binds vs @Provides — 两种提供方式的博弈
两者都写在 Module 里,但 @Binds 是抽象方法,用于把接口绑定到实现类;@Provides 是普通方法,可以写逻辑。
// 接口
interface Logger {
fun log(message: String)
}
// 实现
class FileLogger @Inject constructor() : Logger {
override fun log(message: String) { /* 写文件 */ }
}
// Module
@Module
@InstallIn(SingletonComponent::class)
abstract class LoggerModule {
@Binds
abstract fun bindLogger(fileLogger: FileLogger): Logger
}
@Binds 性能更好,因为它是编译时直接映射,没有方法调用开销。而 @Provides 还可以做参数注入、条件判断。我的经验:能用 @Binds 首选,除非你需要做冗长的初始化。
@Binds 的实现类本身有构造函数注入,记得在实现类上写 class FileLogger @Inject constructor()。
3.6 @HiltViewModel — ViewModel 的专用注入
ViewModel 生命周期长于 Activity 重建,Hilt 专门做了 @HiltViewModel 注解,配合 @Inject 使用。
@HiltViewModel
class MainViewModel @Inject constructor(
private val repo: Repository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ...
}
Activity 中直接通过 by viewModels() 拿到即可,无需手动传参。注意:@HiltViewModel 只能在 AndroidX 的 ViewModel 子类上使用。
3.7 @EntryPoint — 非 Hilt 组件的「后门」
有些场景,比如三方库的 ContentProvider、自定义 View 等,你不能加 @AndroidEntryPoint。这时用 @EntryPoint 定义一个接口,手动获取依赖。
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AnalyticsEntryPoint {
fun getAnalytics(): AnalyticsService
}
// 在 ContentProvider 里
val entryPoint = EntryPointAccessors.fromApplication(
context,
AnalyticsEntryPoint::class.java
)
val analytics = entryPoint.getAnalytics()
这个方式被我称为「急诊通道」。但你想想看,每次手动调用都增加了样板代码和运行时查找开销。所以能加注解的就别用 @EntryPoint。
3.8 一张图理清所有注解的关系
下面这张图是我自己总结的,方便你快速回顾:
回顾一下:@HiltAndroidApp 是基石,@AndroidEntryPoint 让组件可注入,@Inject 标记注入点,@Module 配合 @InstallIn 提供依赖,@Binds 和 @Provides 是两种提供方式,@HiltViewModel 解决 ViewModel 注入,@EntryPoint 作为兜底。掌握了这些,大部分 Hilt 场景你都能应对。