第3章 Hilt核心注解详解

Hilt 的注解体系,说白了就是一套「谁可以注入、谁来提供、怎么绑定」的约定。刚接触时你可能觉得注解太多,记不住。别急,我当年在重构一个老项目时也踩过不少坑——比如把 @InstallIn 写错作用域,导致运行时死活拿不到依赖。嗯,这一节我把最常用的几个注解掰开揉碎讲清楚。

3.1 @HiltAndroidApp — 应用的「总开关」

所有 Hilt 项目都必须有一个 @HiltAndroidApp 注解的 Application 类。它的作用是生成一个「父组件」,所有其他组件都从它派生。

@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // 注意:这里还不能直接注入,因为组件还未完全初始化
    }
}
我曾经看到有人把 @HiltAndroidApp 写在 BaseActivity 上——那是错的。它只能用在 Application 类上,且整个应用只出现一次。

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。

核心原则:能用 @Inject 用 @Inject,搞不定再用 Module。

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 在哪个组件里生效。常见的有:

作用域组件生命周期
整个应用SingletonComponentApplication 创建到销毁
ActivityActivityComponentActivity 周期
FragmentFragmentComponentFragment 周期
ViewModelViewModelComponentViewModel 生命周期

举个我当年犯过的错:把数据库依赖放在 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。

千万别在 Application 的 onCreate 里用 EntryPointAccessors——此时 Hilt 组件还没初始化完毕,会抛空指针。至少等到第一个 Activity 的 super.onCreate 之后。

3.8 一张图理清所有注解的关系

下面这张图是我自己总结的,方便你快速回顾:

Hilt 核心注解关系图 @HiltAndroidApp Application 入口 @AndroidEntryPoint Activity / Fragment / Service / Broadcast @Inject (字段注入) Activity/Fragment 内部 @Module + @InstallIn 依赖提供者(工厂) @Binds(接口↔实现) @Provides(自定义逻辑) @HiltViewModel ViewModel 专用注入 @EntryPoint(非 Hilt 组件) 实线箭头:直接依赖关系;虚线箭头:间接关联或可选使用。

回顾一下:@HiltAndroidApp 是基石,@AndroidEntryPoint 让组件可注入,@Inject 标记注入点,@Module 配合 @InstallIn 提供依赖,@Binds 和 @Provides 是两种提供方式,@HiltViewModel 解决 ViewModel 注入,@EntryPoint 作为兜底。掌握了这些,大部分 Hilt 场景你都能应对。