Hilt 是 Android 官方推荐的依赖注入方案。它构建在 Dagger 之上,使用注解描述对象如何创建、依赖应该安装到哪个容器、实例是否需要复用,以及 Android Framework 创建的对象从哪里进入依赖图。

学习 Hilt 不能只停留在会写 @Inject@Module@Singleton。真正需要掌握的是下面这条主线:

1
2
3
4
5
6
7
Binding:对象怎么创建

Component:Binding 放在哪个依赖容器

Scope:同一容器中是否复用实例

Lifecycle:容器和对象存活多久

本文从 Hilt 如何使用注解开始,通过一条完整的网络层、Repository、ViewModel 依赖链,重点讲清 Component、Scope、生命周期、屏幕旋转、父子 Component 可见性以及 Repository 的作用域选择。

一、Hilt 如何使用注解

Hilt 的注解不是运行时“魔法”。构建时,Hilt/Dagger 会读取注解、建立并校验依赖图,然后生成 Factory、Component 和 Android 注入代码;运行时直接调用这些生成代码创建对象。

1
2
3
4
5
6
7
8
9
10
11
Kotlin 源码

KSP / Annotation Processor

校验注解与依赖图

生成 Factory、Component 等代码

和业务代码一起编译

运行时直接调用生成代码

Hilt 主要解决两件事:

  1. 根据 Binding 建立、校验并生成对象依赖图。
  2. 提供与 Application、Activity、Fragment、ViewModel、Service 生命周期对应的标准 Component。

没有 Hilt 时,创建登录页面的依赖可能需要手写:

1
2
3
4
5
6
7
8
9
10
val okHttpClient = OkHttpClient.Builder().build()

val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.build()

val userApi = retrofit.create(UserApi::class.java)
val repository = UserRepository(userApi)
val viewModel = LoginViewModel(repository)

它背后的依赖图是:

1
2
3
4
5
6
7
8
9
LoginViewModel

UserRepository

UserApi

Retrofit

OkHttpClient

Hilt 让开发者描述每个节点“如何创建”和“应该属于哪个生命周期”,再生成上面这段组装逻辑。

常用注解可以按职责分为四组:

职责 注解 作用
应用与 Android 入口 @HiltAndroidApp@AndroidEntryPoint@HiltViewModel 让 Application、Activity、Fragment、ViewModel 等进入 Hilt 依赖图
构造 Binding @Inject 声明构造函数或注入点
Module Binding @Module@Provides@Binds@InstallIn 提供第三方对象、绑定接口并指定 Component
实例复用 @Singleton@ActivityRetainedScoped@ViewModelScoped 指定同一 Component 实例内的复用范围

后续章节会沿着“创建规则、容器、复用范围、生命周期”依次拆解这些注解。

二、接入 Hilt

以下使用 Kotlin DSL 和 KSP。版本会持续更新,请以 Android 官方 Hilt 文档为准,不要长期复制某个固定版本号。

根项目 build.gradle.kts

1
2
3
4
plugins {
id("com.google.dagger.hilt.android") version HILT_VERSION apply false
id("com.google.devtools.ksp") version KSP_VERSION apply false
}

应用模块 build.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}

dependencies {
implementation("com.google.dagger:hilt-android:$HILT_VERSION")
ksp("com.google.dagger:hilt-android-compiler:$HILT_VERSION")
}

如果项目仍使用 kapt,可按照官方文档切换对应处理器配置;新旧方案不要在未确认项目现状时混用。

1. @HiltAndroidApp:应用级入口

1
2
@HiltAndroidApp
class App : Application()

并在 Manifest 中注册:

1
2
3
<application
android:name=".App"
... />

@HiltAndroidApp 会触发 Hilt 代码生成,并建立应用级依赖容器。它不是在说“Application 是单例”,而是在声明 Hilt 依赖图的应用入口。

2. @AndroidEntryPoint:Android 组件入口

1
2
@AndroidEntryPoint
class MainActivity : AppCompatActivity()

Activity、Fragment、Service 等对象由 Android Framework 创建,开发者无法直接通过构造函数控制它们的实例化过程。@AndroidEntryPoint 让 Hilt 为这些框架对象生成注入代码。

在 Compose 中通常只需给承载界面的 ComponentActivity 添加 @AndroidEntryPoint,不需要给每个 Composable 添加 Hilt 注解。

3. @Inject constructor:告诉 Hilt 如何创建对象

1
2
3
class UserRepository @Inject constructor(
private val api: UserApi
)

它表达的不是“立即向这里注入对象”,而是:

当依赖图需要 UserRepository 时,可以调用这个构造函数;构造前还必须先解决 UserApi

Hilt 会递归查找每个构造参数的 Binding,直到依赖图闭合。常见编译错误 X cannot be provided without an @Inject constructor or an @Provides-annotated method,本质就是依赖图走到 X 时断开了。

4. 为什么构造函数参数可以是 private val

1
2
3
class UserManager @Inject constructor(
private val repository: UserRepository
)

Hilt 创建对象时调用的是构造函数:

1
val manager = UserManager(repository)

private 只限制外部访问 UserManager.repository,并不妨碍构造函数接收参数。因此普通业务类应优先使用构造函数注入,把依赖保持为私有实现细节。

字段注入则不同:

1
2
3
4
5
6
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var repository: UserRepository
}

Activity 已经由系统创建,Hilt 只能在之后从外部给字段赋值,所以 Hilt 注入字段不能声明为 private,否则会产生编译错误。

三、无法构造注入时:@Module@Provides@Binds

构造函数注入优先,但以下类型无法直接使用:

  • 接口没有可调用的构造函数。
  • Retrofit、OkHttp、Room 等第三方类无法修改源码。
  • 对象必须通过 Builder 或 Factory 创建。

这时需要 Hilt Module。

1. @Module@InstallIn

1
2
3
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule
  • @Module:该类型中包含对象创建或类型绑定规则。
  • @InstallIn:这些 Binding 安装到哪个 Component。

必须特别注意:

@InstallIn(SingletonComponent::class) 只表示 Binding 对应用级 Component 可见,不代表每次请求一定返回同一个实例。

是否复用由 Scope 决定。

2. @Provides:执行创建逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}

@Provides
@Singleton
fun provideRetrofit(
okHttpClient: OkHttpClient
): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

@Provides
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
}

@Provides 方法中:

  • 返回值表示它提供的类型。
  • 参数表示创建该类型所需的依赖。
  • 方法体表示具体创建过程。

Hilt 会先解决 OkHttpClient,再创建 Retrofit,最后创建 UserApi

3. @Binds:把接口绑定到实现

1
2
3
interface UserRepository {
suspend fun login()
}
1
2
3
4
5
6
7
8
class UserRepositoryImpl @Inject constructor(
private val api: UserApi
) : UserRepository {

override suspend fun login() {
api.login()
}
}
1
2
3
4
5
6
7
8
9
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

@Binds
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}

@Binds 方法的返回类型是依赖方请求的抽象类型,参数类型是实际实现。

可以用下面的顺序选方案:

1
2
3
4
5
6
7
8
9
10
11
自己的普通类

@Inject constructor

接口映射到实现

@Binds

第三方类、Builder、复杂创建过程

@Provides

四、完整依赖链:从网络层到 ViewModel

Repository:

1
2
3
4
5
6
7
8
class DefaultUserRepository @Inject constructor(
private val api: UserApi
) : UserRepository {

override suspend fun login() {
api.login()
}
}

ViewModel:

1
2
3
4
5
6
7
8
9
10
11
@HiltViewModel
class LoginViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {

fun login() {
viewModelScope.launch {
repository.login()
}
}
}

Activity 获取 ViewModel:

1
2
3
4
5
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {

private val viewModel: LoginViewModel by viewModels()
}

Compose 中可在对应导航目的地使用 hiltViewModel() 获取作用域正确的 ViewModel。

此时 Hilt 构建的依赖图是:

1
2
3
4
5
6
7
8
9
10
11
LoginViewModel

UserRepository 接口
↓ @Binds
DefaultUserRepository

UserApi
↓ @Provides
Retrofit
↓ @Provides
OkHttpClient

ViewModel 必须通过 AndroidX 的 ViewModel 获取机制创建,不能把 @HiltViewModel 直接当普通依赖字段注入,否则会绕过 ViewModelStore 所管理的实例与生命周期。

五、Hilt 最重要的心智模型

Hilt 中最容易混淆的是 Binding、Component、Scope 和 Lifecycle。

1. Binding:对象怎么来

以下三种写法都会贡献 Binding:

1
2
3
@Inject constructor  → 构造创建
@Provides → 执行方法创建
@Binds → 接口映射实现

2. Component:Binding 放在哪个容器

Component 可以理解为依赖容器。它持有可见的 Binding,并负责创建或缓存属于自己的 scoped 对象。

3. Scope:同一 Component 实例内是否复用

默认 Binding 是 unscoped。每次请求时,Hilt 可以创建一个新实例。

添加 Scope 后,同一个 Component 实例中的所有请求复用同一个对象。

4. Lifecycle:Component 存活多久

Component 的生命周期限定了其中 scoped 对象最长能存活多久。

最终可以压缩为一句:

Component 是容器,Scope 是容器内对象的复用规则,Lifecycle 是容器的存活时间。

六、Hilt Component 层级与作用域

简化后的核心层级如下:

1
2
3
4
5
6
7
8
9
10
11
SingletonComponent

├── ServiceComponent

└── ActivityRetainedComponent

├── ActivityComponent
│ │
│ └── FragmentComponent

└── ViewModelComponent

需要注意:ActivityComponentViewModelComponent 是兄弟 Component,它们共同继承 ActivityRetainedComponent,不是彼此的父子关系。

常见对应关系如下:

Component Scope 生命周期语义 常见对象
SingletonComponent @Singleton Application 进程范围 Retrofit、OkHttp、Room、全局会话
ActivityRetainedComponent @ActivityRetainedScoped 一个逻辑 Activity,可跨配置变化 多个 ViewModel 共享的流程状态
ViewModelComponent @ViewModelScoped 单个 ViewModel ViewModel 专属缓存、状态协作者
ActivityComponent @ActivityScoped 具体 Activity 实例 Navigator、Dialog 管理、Activity UI 协作者
FragmentComponent @FragmentScoped 具体 Fragment 实例 Fragment 专属协作者
ServiceComponent @ServiceScoped 具体 Service 实例 Service 专属对象

Scope 必须与 Binding 所在 Component 匹配。例如安装在 ActivityComponent 的 scoped Binding 应使用 @ActivityScoped,不能随意标记成 @Singleton

七、父子 Component 如何共享依赖

Hilt/Dagger 的可见性规则是:

1
2
3
子 Component 可以访问自己和祖先 Component 的 Binding
父 Component 不能访问子 Component 的 Binding
兄弟 Component 不能互相访问对方的 Binding

例如:

1
2
@ActivityRetainedScoped
class CheckoutSession @Inject constructor()
1
2
3
4
@ViewModelScoped
class CheckoutCache @Inject constructor(
private val session: CheckoutSession
)
1
2
3
4
@HiltViewModel
class CheckoutViewModel @Inject constructor(
private val cache: CheckoutCache
) : ViewModel()

它们实际分布在两层:

1
2
3
4
5
6
7
8
9
10
ActivityRetainedComponent

└── CheckoutSession #1

│ 祖先 Binding
ViewModelComponent

└── CheckoutCache #1

CheckoutViewModel

从生成代码的角度,可以把 scoped 对象理解为“缓存槽放在对应 Component 中”:

1
2
3
4
@Singleton               → 缓存在 SingletonComponent
@ActivityRetainedScoped → 缓存在 ActivityRetainedComponent
@ActivityScoped → 缓存在 ActivityComponent
@ViewModelScoped → 缓存在 ViewModelComponent

这是一种便于理解的模型。实际生成类名和延迟初始化代码更复杂,但语义一致。

为什么长生命周期对象不能依赖短生命周期对象

下面这种依赖方向不成立:

1
2
3
4
@Singleton
class UserManager @Inject constructor(
private val navigator: ActivityNavigator
)
1
2
@ActivityScoped
class ActivityNavigator @Inject constructor()

UserManager 可能在整个 App 进程中存活,而 ActivityNavigator 只属于某个 Activity。当 Activity 销毁后,Singleton 仍然存活,依赖对象却已经过期。

因此应记住:

1
2
短生命周期对象可以依赖长生命周期对象
长生命周期对象不能依赖短生命周期对象

从 Component 结构上看,这也等价于“子级可以访问祖先,祖先不能反向访问子级”。

八、屏幕旋转时谁保留、谁重建

Activity 存在两个容易混淆的概念:

  • 具体 Activity 实例:旋转等配置变化时通常会销毁并重建。
  • 用户看到的逻辑 Activity:旋转前后仍然是同一个页面流程。

第一次创建:

1
2
3
4
5
6
7
8
9
ActivityRetainedComponent #A

├── ActivityComponent #1
│ ↓
│ MainActivity #1

└── ViewModelComponent #V

ViewModel #1

旋转后:

1
2
3
4
5
6
7
8
9
ActivityRetainedComponent #A  ← 保留

├── ActivityComponent #2 ← 重建
│ ↓
│ MainActivity #2 ← 重建

└── ViewModelComponent #V ← ViewModel 未清除时保留

ViewModel #1 ← 保留

所以:

1
2
3
4
5
6
7
8
屏幕旋转

Activity 重建
ActivityComponent 重建

ActivityRetainedComponent 保留
ViewModel 保留
ViewModelComponent 保留

Hilt 对外保证 ActivityRetainedComponent 跨配置变化存活。实现层面可以把它理解为借助 Activity 的 retained 状态与 AndroidX ViewModelStore 机制:配置变化时复用原来的 Store;宿主真正退出时清理 Store,并通知其中的 ViewModel 不再需要。

因此 @ActivityRetainedScoped 对象本身不需要知道:

  • 自己属于哪个 Activity。
  • Activity 是否正在旋转。
  • 自己何时应该销毁。

这些责任属于 Component 与 Android 生命周期基础设施,而不是普通业务对象。

@ActivityRetainedScoped@ViewModelScoped 的区别

假设一个 Activity 有 UserViewModelOrderViewModel

1
2
3
4
5
6
7
8
9
10
ActivityRetainedComponent

├── Session #1
│ ↑ ↑
│ │ │
├── UserViewModelComponent
│ └── Cache #1

└── OrderViewModelComponent
└── Cache #2
  • @ActivityRetainedScoped Session:同一逻辑 Activity 下的多个 ViewModel 可以共享。
  • @ViewModelScoped Cache:每个 ViewModel 拥有自己的实例。

它们都可能跨旋转保留,但粒度不同:一个属于逻辑 Activity,一个属于具体 ViewModel。

九、Scope 不会沿依赖链“传染”

假设:

1
2
@ViewModelScoped
class CheckoutCache @Inject constructor()
1
2
3
class CheckoutRepository @Inject constructor(
private val cache: CheckoutCache
)

CheckoutRepository 没有 Scope,因此在同一个 ViewModelComponent 中可能发生:

1
2
3
4
5
6
7
8
9
10
11
第一次请求 Repository

CheckoutRepository #1

CheckoutCache #1

第二次请求 Repository

CheckoutRepository #2

CheckoutCache #1

即:

1
2
Repository #1 != Repository #2
Repository #1.cache == Repository #2.cache

依赖了 scoped 对象,不代表当前 Binding 自动拥有相同 Scope。每个 Binding 的复用规则独立决定。

如果 Repository 自身也必须在单个 ViewModel 内唯一,再显式添加:

1
2
3
4
@ViewModelScoped
class CheckoutRepository @Inject constructor(
private val cache: CheckoutCache
)

不要为了“看起来统一”而给整个依赖链机械地添加相同 Scope,应先判断每个对象是否真的需要实例唯一性。

十、Repository 应不应该加 Scope

Repository 只是架构角色名称,不天然等于 @Singleton@ViewModelScoped

判断时依次问三个问题:

1
2
3
1. Repository 内部有没有实例状态?
2. 这份状态应该在哪些消费者之间共享?
3. Repository 的创建或初始化是否真的昂贵?

1. 无状态 Repository:通常不加 Scope

1
2
3
4
5
6
7
8
9
class ProductRepository @Inject constructor(
private val api: ProductApi,
private val dao: ProductDao
) {

suspend fun getProduct(id: Long): Product {
return api.getProduct(id)
}
}

它只组合和转发调用,没有缓存、Flow、连接或共享状态。即使存在多个实例,行为通常也相同,因此不必为了 Repository 这个名字添加 Scope。

2. ViewModel 专属状态:@ViewModelScoped

1
2
3
4
@ViewModelScoped
class CheckoutRepository @Inject constructor(
private val cache: CheckoutCache
)

适合只服务于单个 ViewModel 的页面状态或流程缓存。

3. 同一逻辑 Activity 的多个 ViewModel 共享:@ActivityRetainedScoped

1
2
@ActivityRetainedScoped
class CheckoutSession @Inject constructor()

适合同一业务流程中多个 ViewModel 共享,而且旋转后不能丢失的状态。不同 Activity 实例不会因此变成全局共享。

4. App 级共享状态:@Singleton

UserRepository 如果管理以下内容,通常适合 @Singleton

  • 当前登录用户。
  • Token 与登录状态。
  • 用户资料缓存。
  • 全局权限或账号切换状态。
  • 需要被多个页面观察的同一条 StateFlow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Singleton
class UserRepository @Inject constructor(
private val api: UserApi,
private val userDao: UserDao
) {

private val _currentUser = MutableStateFlow<User?>(null)
val currentUser: StateFlow<User?> = _currentUser.asStateFlow()

suspend fun login() {
_currentUser.value = api.login()
}

fun logout() {
_currentUser.value = null
}
}

多个 ViewModel 会观察同一份状态:

1
2
3
4
5
LoginViewModel ──────┐
HomeViewModel ───────┤
ProfileViewModel ────┼── UserRepository #1
SettingsViewModel ───┘ ↓
currentUser

如果不加 Scope,则不同消费者可能拿到不同 Repository:

1
2
3
4
5
6
7
LoginViewModel

UserRepository #1 → user = 张三

ProfileViewModel

UserRepository #2 → user = null

5. @Singleton 的代价

@Singleton 不是免费的性能优化,它表达的是“把这个实例提升到应用级 Component 生命周期”。代价包括:

  • 对象及其缓存可能长期占用内存。
  • 可变状态的共享范围变大,并发与同步问题更明显。
  • 错误持有 Activity、View 或 Fragment 时,生命周期不匹配会更严重。
  • 测试中更容易出现全局状态相互影响。

尤其不要让 Singleton 持有 Activity:

1
2
3
4
@Singleton
class UserRepository @Inject constructor(
private val activity: Activity
)

正确做法是让长生命周期对象只依赖同级或更长生命周期对象;需要 Context 时,根据真实用途选择 @ApplicationContext 或把 Activity 相关行为留在 Activity 级对象中。

十一、常见误区与排查方式

误区 1:@InstallIn(SingletonComponent::class) 就是单例

不是。@InstallIn 决定可见范围,@Singleton 决定同一 Component 实例内是否复用。

误区 2:所有 Repository 都应该是 Singleton

不是。先判断实例状态及其共享范围。无状态、创建成本低的 Repository 通常不需要 Scope。

误区 3:依赖 Singleton 的对象也会自动变成 Singleton

不会。Scope 不沿依赖链传播。unscoped Repository 可以依赖同一个 Singleton API,但 Repository 自身仍可能有多个实例。

误区 4:ActivityRetainedComponent 就是 ActivityComponent 的长期版本

两者用途不同。ActivityComponent 绑定具体 Activity 实例;ActivityRetainedComponent 绑定跨配置变化的逻辑 Activity,并同时作为 ActivityComponentViewModelComponent 的父级。

误区 5:ViewModel 可以注入 @ActivityScoped Navigator

通常不可以。两者位于兄弟 Component,且 ViewModel 可能在 Activity 重建后继续存在,持有旧 Activity 级对象会产生生命周期错误。

误区 6:构造函数参数和注入字段都不能 private

构造函数参数可以是 private val;Hilt 字段注入不能是 private。两者是不同注入方式。

误区 7:为了减少 new,对所有对象都加 Scope

Scope 会增加生成代码与运行时管理成本,并延长对象存活时间。只有当实例唯一性影响正确性、同步,或已经测量出创建成本时才添加。

十二、一个实用的 Scope 决策表

需求 建议
无状态、轻量、多个实例行为一致 不加 Scope
单个 ViewModel 内必须共享同一实例 @ViewModelScoped
同一逻辑 Activity 的多个 ViewModel 共享并跨旋转保留 @ActivityRetainedScoped
严格绑定当前 Activity 实例或 Activity Context @ActivityScoped
整个 App 共享状态或资源 @Singleton

选择前再检查两条规则:

  1. 能使用更小的 Scope,就不要无理由扩大生命周期。
  2. 长生命周期对象不能依赖短生命周期对象。

十三、推荐学习与源码阅读路线

如果目标是理解 Hilt 而不只是会写几个注解,可以按以下顺序推进:

第一阶段:完成 Hilt 依赖图

  • @Inject constructor 串起三层普通类。
  • @Provides 接入 Retrofit。
  • @Binds 绑定 Repository 接口。

第二阶段:Component 与生命周期

  • 比较 Activity 旋转前后的 @ActivityScoped@ActivityRetainedScoped 实例。
  • 在一个 Activity 中创建两个 ViewModel,验证 @ViewModelScoped 不跨 ViewModel 共享。
  • 查看生成的 Component/Factory,确认 scoped 对象缓存在哪一层。

第三阶段:错误定位与生成代码

  • 主动制造缺失 Binding、循环依赖与 Scope 不匹配错误,阅读完整依赖链。
  • 比较 @Inject constructor@Provides@Binds 生成的代码。
  • 确认父 Component 的 Binding 如何被子 Component 使用。

当你能够把下面这段代码直接翻译成人话时,就已经掌握了 Hilt 的核心:

1
2
3
4
5
6
7
8
9
10
11
12
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
}
}

翻译结果是:

1
2
3
4
5
6
7
8
9
10
11
@Provides
→ 这是 Retrofit 的创建规则

@InstallIn(SingletonComponent::class)
→ 该 Binding 安装在应用级 Component

@Singleton
→ 同一个 SingletonComponent 中复用同一个 Retrofit

SingletonComponent 生命周期
→ 从 Application 创建到应用进程结束

总结

学习 Hilt 的关键不是背诵注解名称,而是始终追问四个问题:

1
2
3
4
这个类型的 Binding 怎么提供?
Binding 安装在哪个 Component?
是否需要 Scope?
Component 与对象分别活多久?

最终心智模型是:

注解负责声明,处理器负责建立依赖图,Component 负责承载 Binding,Scope 负责实例复用,Android 生命周期负责 Component 的创建与销毁。

参考资料