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 就可以在模块里把具体实现注入进去,而不用改调用方的代码。这个模式在写单元测试时特别有用——你只需要换一个模块实现就能替换实现。

⚠️ 注意:模块里的函数千万不能有副作用。别在里面做网络请求、数据库写入等操作,否则 Hilt 在编译期不会报错,但运行期可能莫名其妙出现空指针或状态不一致。

入口点(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 核心概念关系图 组件(Component) 模块(Module) 入口点(Entry Point) 绑定(Binding) @InstallIn 指定所属组件 @InstallIn 指定所属组件 提供依赖实例 @Provides/@Binds 创建绑定 通过入口点获取绑定 说明: • 一个组件可以包含多个模块,一个模块可以有多个绑定方法。 • 入口点是外部代码访问 Hilt 绑定的唯一通道。 • 绑定是最终的依赖映射,连接着需求(类型)和供应(实例)。

小结

现在你应该能理清这四者的关系了:

  • 组件 是生命周期容器,管理作用域
  • 模块 是依赖的组装车间,告诉 Hilt 怎么造对象
  • 入口点 是外部自定义类与 Hilt 之间的桥梁
  • 绑定 是依赖的供需合同,最终决定实例从哪来

我个人习惯在开始一个新模块前,先把上下文画出来——哪个组件、需要哪些模块、有哪些入口点。看起来像是多花了5分钟,但后期排查注入问题会节省好几个小时。下个章节我们会深入每个概念的使用细节,到时候拿真实代码一步步演示。