Hilt核心概念:组件、模块、入口点、绑定
这一章我们聊聊 Hilt 的四个基本概念:组件、模块、入口点和绑定。说实话,刚接触 Hilt 时我也被这些术语搞得有点晕——它们听起来像是架构设计里的空洞词汇,但一旦你在真实项目里写过几个类,就会发现它们其实就藏在你的代码结构里。
组件(Component)——Hilt 的容器
组件是 Hilt 的核心容器。它负责管理依赖对象的生命周期,并决定哪些对象可以共享同一个实例。说白了,每个 Android 组件(Activity、Fragment、Service 等)都会对应一个 Hilt 组件。
我印象很深,有次做性能优化,发现一个 SharedPreferences 对象被反复创建了十几遍。后来才发现,是我把它绑定到了 ActivityComponent,而不是 SingletonComponent。那时候才真正理解组件范围和生命周期的关系。
Hilt 预定义了以下几类组件:
| 组件名称 | 绑定范围 | 生命周期 |
|---|---|---|
| SingletonComponent | 全局 | Application 存在期间 |
| ActivityComponent | 单个 Activity | Activity 存活期间 |
| FragmentComponent | 单个 Fragment | Fragment 存活期间 |
| ServiceComponent | 单个 Service | Service 存活期间 |
| ViewComponent | 单个 View | View 存在期间 |
@Singleton 注解绑定到 SingletonComponent。如果只在某个 Activity 内需要唯一实例,用 @ActivityScoped。模块(Module)——组装依赖的工厂
模块说白了就是告诉 Hilt:「这个对象要怎么创建?」通常我们把它写成一个类,用 @Module 注解标记,再加上 @InstallIn 指定它属于哪个组件。
举个实际例子:你需要注入第三方的网络库 OkHttpClient,不能直接在构造器上加 @Inject,因为类不是你写的。这时候就用模块来提供实例:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}
这里 @Provides 告诉 Hilt:「这个函数返回的 OkHttpClient 就是你要的依赖」。
还有一种方式叫 @Binds,用于把接口和实现类绑定起来。我记得有次接手一个遗留项目,里面用接口定义了多个数据源实现,通过 @Binds 就可以在模块里把具体实现注入进去,而不用改调用方的代码。这个模式在写单元测试时特别有用——你只需要换一个模块实现就能替换实现。
入口点(Entry Point)——Hilt 的门户
入口点就是 Hilt 和普通 Android 类进行交互的边界。如果你有一个类不是 Android 组件(比如一个普通的 Kotlin 类),但你希望 Hilt 能往它里面注入依赖,就需要定义一个入口点接口。
举个例子,我做过一个自定义 View,它需要依赖 AnalyticsService。自定义 View 不能被 Hilt 直接支持(因为构造方式不同),所以需要这样写:
class MyCustomView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyViewEntryPoint {
fun analyticsService(): AnalyticsService
}
private val analyticsService: AnalyticsService by lazy {
EntryPointAccessors.fromApplication(
context.applicationContext,
MyViewEntryPoint::class.java
).analyticsService()
}
}
嗯,这里要注意的是,入口点必须对接一个组件——也就是 @InstallIn 里写的那个组件。如果你在 Activity 里使用入口点,就用 EntryPointAccessors.fromActivity;在 Fragment 里就用 fromFragment。这点我在项目中踩过坑,记错了作用域导致注入失败。
绑定(Binding)——依赖和提供者的映射
绑定是 Hilt 内部的一张「对应表」。它记录了「每当你需要某个类型时,应该去哪里找实例」。绑定可以是 @Inject 注解的构造器,也可以是模块里的 @Provides 或 @Binds 方法。
实际上,你写的每一个 @Module 里的函数,都会在编译期生成一个对应的绑定对象。Hilt 会把它们整理成一个图,然后在运行期根据依赖链按需创建实例。
我建议大家画一张依赖图来理解绑定关系——虽然 Hilt 在编译期已经做了静态检查,但人为梳理绑定关系能帮你提前发现循环依赖。比如:
- 如果 A 依赖 B,B 又依赖 C,C 再依赖 A —— 这就是循环绑定,Hilt 编译会直接报错
- 如果 A 和 B 都依赖同一个接口,但有两个不同的实现 —— 需要
@Qualifier区分同名绑定
下面这个 SVG 图展示了四个概念之间的协作关系:组件是容器,模块提供绑定,入口点是外部访问点,绑定连接了需求和供应。
小结
现在你应该能理清这四者的关系了:
- 组件 是生命周期容器,管理作用域
- 模块 是依赖的组装车间,告诉 Hilt 怎么造对象
- 入口点 是外部自定义类与 Hilt 之间的桥梁
- 绑定 是依赖的供需合同,最终决定实例从哪来
我个人习惯在开始一个新模块前,先把上下文画出来——哪个组件、需要哪些模块、有哪些入口点。看起来像是多花了5分钟,但后期排查注入问题会节省好几个小时。下个章节我们会深入每个概念的使用细节,到时候拿真实代码一步步演示。