Hilt 依赖注入实战指南 —— 以 RichEditor 项目为例
本文基于 RichEditor(一款 Markdown First 富文本便签 App)的真实工程实践,系统性地介绍 Hilt 在多模块 Android 项目中的使用方式。如果你正在构建一个基于 Kotlin + Jetpack Compose + 模块化架构的项目,这篇文章会给你一个完整的参考路径。
目录
- 什么是 Hilt
- 为什么选择 Hilt
- 环境配置
- 核心概念与注解详解
- Application 入口配置详解
- Activity 注入详解
- Module 的两种写法:@Provides vs @Binds
- ViewModel 注入详解
- 多模块项目中的 Hilt 实践
- 依赖图总览
- 常见问题与排坑
- Hilt 测试支持
- 总结
1. 什么是 Hilt
1.1 Hilt 的前世今生
Hilt 是 Google 在 2020 年发布的 Android 专用依赖注入框架,其底层基于业界成熟的 Dagger。Google 选择 Dagger 而非自研的原因在于:Dagger 已经在 Square、Uber 等公司经过多年生产环境验证,其编译期代码生成机制提供了最强的类型安全保证。
Dagger (2012, Square)↓
Dagger 2 (2016, Google 接手维护)↓
Hilt (2020, Google 为 Android 量身定制)
Hilt 与 Dagger 的关系:
- Hilt 是 Dagger 2 的封装,专门为 Android 场景优化
- Dagger 2 需要手动管理 Component 和 SubComponent 的层级关系
- Hilt 自动处理 Android 组件(Application、Activity、Fragment 等)的生命周期绑定
- 代码量减少约 60%,学习曲线大幅降低
1.2 依赖注入的核心思想
依赖注入(Dependency Injection,简称 DI)是一种设计模式,其核心思想是将对象依赖的创建责任从对象本身转移到外部容器。
没有 DI 的代码(紧耦合):
class NoteRepository {private val dao = NoteDao() // NoteRepository 自己创建 DAOprivate val mediaImporter = AndroidMediaImporter() // 自己创建媒体导入器fun saveNote(note: Note) {dao.insert(note)}
}
问题:
NoteRepository与NoteDao具体实现强耦合- 无法替换为测试用的 Mock DAO
- 依赖创建逻辑分散在各个类中,难以管理
有 DI 的代码(松耦合):
class NoteRepository @Inject constructor(private val dao: NoteDao,private val mediaImporter: MediaImporter
) {fun saveNote(note: Note) {dao.insert(note)}
}
优势:
NoteRepository只声明「我需要什么」,不关心「如何创建」- 可以轻松注入 Mock DAO 进行单元测试
- 所有依赖的创建逻辑集中在 DI 容器中
1.3 Hilt 的工作原理
┌─────────────────────────────────────────────────────────────────┐
│ 编译期处理 │
│ │
│ 源代码 (.kt) Hilt Processor 生成的代码 │
│ ──────────────── ───────────────── ───────────── │
│ @HiltAndroidApp → 注解处理器扫描 → Hilt_AppComponent │
│ @AndroidEntryPoint → 生成绑定关系 → Hilt_MainActivity │
│ @HiltViewModel → → ViewModelFactory │
│ │
└─────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────┐
│ 运行时注入 │
│ │
│ App 启动 │
│ │ │
│ ↓ │
│ Hilt 初始化 SingletonComponent │
│ │ │
│ ↓ │
│ 当需要 NoteRepository 时: │
│ 1. 检查是否有缓存实例 │
│ 2. 没有则解析依赖图:NoteRepository → NoteDao → Database │
│ 3. 按依赖顺序创建实例 │
│ 4. 返回 NoteRepository 并缓存 │
└─────────────────────────────────────────────────────────────────┘
关键点:所有注入逻辑在编译期就已经确定,运行时只是按图索骥创建对象,没有任何反射带来的性能损耗。
2. 为什么选择 Hilt
2.1 与其他 DI 框架的对比
| 特性 | Hilt | Koin | Manual DI |
|---|---|---|---|
| 代码生成 | 编译期生成 | 运行时 DSL 注册 | 无 |
| 性能 | 无运行时开销 | 有运行时解析开销 | 无 |
| 类型安全 | 编译期检查 | 运行时可能出错 | 手动易出错 |
| Android 适配 | 官方推荐 | 需要手动适配 | 需要手动适配 |
| ViewModel 支持 | 原生支持 | 需要扩展 | 需要 Factory |
| 多模块支持 | 原生支持 | 支持但配置复杂 | 需要手动传递 |
2.2 RichEditor 选择 Hilt 的理由详解
理由一:多模块天然支持
RichEditor 项目结构:
app/ ← 入口模块
core/core-database/ ← 数据库定义core-datastore/ ← 偏好设置core-media/ ← 媒体处理core-markdown/ ← Markdown 渲染
data/data-note/ ← 便签数据层
feature/feature-note-list/ ← 便签列表页feature-note-detail/ ← 便签详情页feature-note-editor/ ← 便签编辑器feature-settings/ ← 设置页
传统做法:需要在 app 模块中手动汇总所有模块的依赖:
// app 模块需要知道所有子模块的依赖
object AppModule {fun provideMediaImporter() = AndroidMediaImporter()fun provideDataStore() = ...fun provideNoteDao() = ...// ... 数十个依赖
}
Hilt 做法:每个模块独立声明,Hilt 自动聚合:
// core-media 模块只需声明自己的依赖
@Module @InstallIn(SingletonComponent::class)
abstract class MediaModule {@Binds abstract fun bindMediaImporter(impl: AndroidMediaImporter): MediaImporter
}// core-datastore 模块独立声明
@Module @InstallIn(SingletonComponent::class)
object DataStoreModule {@Provides fun provideDataStore(...): DataStore<Preferences> = ...
}
@InstallIn(SingletonComponent::class) 是 Hilt 实现分布式依赖声明的关键。编译时,Hilt 会扫描所有模块,自动将它们安装到 SingletonComponent 中,无需在 app 模块手动汇总。
理由二:ViewModel 开箱即用
传统 ViewModel Factory 模式:
class NoteEditorViewModel(private val repository: NoteRepository
) : ViewModel() {// ...
}// 需要 Factory
class NoteEditorViewModelFactory(private val repository: NoteRepository
) : ViewModelProvider.Factory {@Suppress("UNCHECKED_CAST")override fun <T : ViewModel> create(modelClass: Class<T>): T {if (modelClass.isAssignableFrom(NoteEditorViewModel::class.java)) {return NoteEditorViewModel(repository) as T}throw IllegalArgumentException("Unknown ViewModel class")}
}// 在 Composable 中使用
@Composable
fun NoteEditorScreen(viewModel: NoteEditorViewModel = viewModel(factory = NoteEditorViewModelFactory(repository)
)) {// ...
}
Hilt 模式:
@HiltViewModel
class NoteEditorViewModel @Inject constructor(private val repository: NoteRepository
) : ViewModel() {// ...
}// Composable 中一行搞定
@Composable
fun NoteEditorScreen(viewModel: NoteEditorViewModel = hiltViewModel()
) {// ...
}
对比结果:
| 项目 | 传统 Factory | Hilt |
|---|---|---|
| 代码行数 | 15+ 行 | 注解 + 一行调用 |
| 类型安全 | 需要手动检查 | 编译期保证 |
| 导航参数 | 需要手动传递 | 自动通过 SavedStateHandle 注入 |
| 测试 | 需要 mock Factory | 直接 inject mock Repository |
理由三:编译期安全
// 如果缺少依赖,编译时报错
class NoteEditorViewModel @Inject constructor(private val noteRepository: NoteRepository, // ✓ Hilt 能解析private val markdownRenderer: MarkdownRenderer, // ✓ Hilt 能解析private val nonExistentService: NonExistentService // ✗ 编译报错!
) : ViewModel()
编译错误示例:
error: [Dagger/HiltProcessingError] com.example.richtext.NonExistentServicecannot be provided without an @Provides-annotated method.
这种机制让你在编译期发现所有依赖问题,而不是在用户使用 App 时崩溃。
理由四:与 Jetpack 深度集成
Hilt 提供了与主流 Jetpack 组件的官方集成:
| Jetpack 组件 | Hilt 集成库 | 提供的功能 |
|---|---|---|
| Navigation Compose | hilt-navigation-compose |
hiltViewModel() 自动携带 NavBackStackEntry |
| WorkManager | hilt-work |
@HiltWorker 自动注入 Worker |
| DataStore | (通过 @Provides) |
在 Module 中提供 DataStore<Preferences> 实例 |
| Room | (通过 @Provides) |
在 Module 中提供 Database 和 DAO 实例 |
| Compose | (通过 ViewModel) | @HiltViewModel + hiltViewModel() |
3. 环境配置
3.1 Version Catalog 详解
Gradle Version Catalog(从 Gradle 7.0 引入)是管理依赖版本的推荐方式。所有依赖版本集中在一个文件中,便于统一升级。
# gradle/libs.versions.toml[versions]
hilt = "2.51.1" # Hilt 版本定义
kotlin = "1.9.24" # Kotlin 版本
room = "2.6.1" # Room 版本
compose = "1.6.1" # Compose BOM 版本[libraries]
# Hilt 库定义
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }# Navigation Compose 集成
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.2.0"
}# Room(用于数据库模块)
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }[plugins]
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
android-application = { id = "com.android.application", version.ref = "androidx" }
android-library = { id = "com.android.library", version.ref = "androidx" }
为什么使用 version.ref 引用?
- 版本变更时只需修改
[versions]区块 - 避免同一版本在不同库中不一致
- 便于依赖升级审计
3.2 根 build.gradle.kts 配置详解
// build.gradle.kts (Project Root)
plugins {// apply false 表示这些插件只声明,不自动应用到当前项目// 这是 Version Catalog 的标准用法alias(libs.plugins.android.application) apply falsealias(libs.plugins.android.library) apply falsealias(libs.plugins.kotlin.android) apply falsealias(libs.plugins.kotlin.kapt) apply falsealias(libs.plugins.hilt.android) apply false
}
关键点:
apply false:这些插件将在子模块中实际应用,这里只是声明- Hilt 插件需要
kotlin-kapt配合,因为 Hilt 的注解处理器基于 kapt android.application和android.library只需要声明一次,子模块通过 alias 应用
3.3 App 模块 build.gradle.kts 详解
// app/build.gradle.kts
plugins {// 应用 Application 插件,生成清单文件和签名配置alias(libs.plugins.android.application)// Kotlin Android 插件alias(libs.plugins.kotlin.android)// 启用 kapt 注解处理器alias(libs.plugins.kotlin.kapt)// Hilt 插件alias(libs.plugins.hilt.android)
}android {namespace = "com.example.richtext"defaultConfig {applicationId = "com.example.richtext"// ...}
}dependencies {// Hilt Android 运行时库implementation(libs.hilt.android)// Hilt 注解处理器(编译时生成代码)// 注意:必须是 kapt,不能是 implementationkapt(libs.hilt.compiler)
}
重要配置说明:
| 配置项 | 必须 | 原因 |
|---|---|---|
kotlin-kapt 插件 |
✓ | Hilt 基于 kapt 处理注解 |
hilt-android 插件 |
✓ | 生成 Hilt 代码 |
implementation(hilt-android) |
✓ | 运行时代码 |
kapt(hilt-compiler) |
✓ | 编译时代码生成器 |
如果用 implementation(hilt-compiler) 会怎样?
- 编译时不会调用注解处理器
- 生成不了 Hilt 代码
- 运行时报错:
Component was not built
3.4 Library 模块配置详解
Library 模块(core-*、data-*、feature-*)的配置与 App 模块略有不同:
// core/core-markdown/build.gradle.kts
plugins {// Library 模块用 android.library,不是 android.applicationalias(libs.plugins.android.library)alias(libs.plugins.kotlin.android)alias(libs.plugins.kotlin.kapt)alias(libs.plugins.hilt.android)
}android {namespace = "com.example.richtext.core.markdown"
}dependencies {implementation(libs.hilt.android)kapt(libs.hilt.compiler)
}
feature 模块额外配置:
// feature/feature-note-editor/build.gradle.kts
plugins {alias(libs.plugins.android.library)alias(libs.plugins.kotlin.android)alias(libs.plugins.kotlin.kapt)alias(libs.plugins.hilt.android)
}dependencies {implementation(libs.hilt.android)kapt(libs.hilt.compiler)// feature 模块需要使用 Compose Navigation + Hilt 集成implementation(libs.hilt.navigation.compose)
}
4. 核心概念与注解详解
4.1 注解全解析
| 注解 | 作用 | 编译产物 | 注意事项 |
|---|---|---|---|
@HiltAndroidApp |
标记 Application 类,触发 Hilt 初始化 | 生成 Hilt_AppClass 基类 |
整个 App 只能有一个 |
@AndroidEntryPoint |
生成注入代码,使组件可接收注入 | 生成 Hilt_Component |
Activity/Fragment 需要此注解 |
@HiltViewModel |
标记 ViewModel 可被 Hilt 创建 | 生成 Factory | 必须包含 @Inject constructor |
@Inject |
标记构造参数或字段需要注入 | 无直接产物 | 构造注入优先于字段注入 |
@Module |
声明依赖提供模块 | 无直接产物 | 必须配合 @InstallIn |
@InstallIn |
指定 Module 安装位置 | 参与 Component 组装 | 常见值:SingletonComponent、ActivityComponent |
@Provides |
在 Module 中提供实例 | 注册到 Component 的 Provider 集合 | 必须是 object class |
@Binds |
接口到实现的绑定 | 注册到 Component 的 Binding 集合 | 必须是 abstract class |
@Singleton |
单例作用域 | 启用 Component 级别缓存 | 配合 @Provides 或 @Binds 使用 |
@ApplicationContext |
标记需要 Application Context | 内置类型映射 | Hilt 内置,无需配置 |
4.2 Component 层级体系
Hilt 定义了严格的 Component 层级,与 Android 组件生命周期对应:
┌─────────────────────────────────────────────────────────────────┐
│ SingletonComponent │
│ (Application) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ActivityRetainedComponent │ │
│ │ (Activity) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ ActivityComponent │ │ │
│ │ │ (Activity) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────────┐ │ │ │
│ │ │ │ FragmentComponent │ │ │
│ │ │ │ (Fragment) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ┌───────────────────────────────────┐ │ │ │ │
│ │ │ │ │ ViewComponent │ │ │ │ │
│ │ │ │ │ (View) │ │ │ │ │
│ │ │ │ └───────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────┘ │ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
各 Component 的生命周期:
| Component | 对应 Android 组件 | 实例生命周期 | 典型使用场景 |
|---|---|---|---|
| SingletonComponent | Application | App 存活期间 | Repository、Database、DataStore |
| ActivityRetainedComponent | Activity | Activity 配置变更后仍存活 | ViewModel Scoped 依赖 |
| ActivityComponent | Activity | Activity 存活期间 | Activity 级单例 |
| FragmentComponent | Fragment | Fragment 存活期间 | Fragment 级依赖 |
| ViewComponent | View | View 存活期间 | 自定义 View 注入 |
为什么通常用 SingletonComponent?
- 在 Compose 单 Activity 架构中,Activity/Fragment 的生命周期很短
- Repository 等数据层需要在 Activity 销毁后继续存活
- SingletonComponent 与 Application 生命周期一致,最适合数据层对象
4.3 @Inject 详解
@Inject 有两种使用方式:
4.3.1 构造注入(推荐)
@Singleton
class RoomNoteRepository @Inject constructor(private val noteDao: NoteDao,private val noteAssetDao: NoteAssetDao,private val mediaImporter: MediaImporter,
) : NoteRepository {// ...
}
原理:
- Hilt 检测到
@Inject注解的构造函数 - 分析构造参数:
NoteDao、NoteAssetDao、MediaImporter - 尝试解析这些类型:如果有
@Provides或其他@Inject constructor,则可以注入 - 生成类似以下代码:
// Hilt 生成的代码(伪代码)
fun provideRoomNoteRepository(noteDao: NoteDao,noteAssetDao: NoteAssetDao,mediaImporter: MediaImporter
): RoomNoteRepository {return RoomNoteRepository(noteDao, noteAssetDao, mediaImporter)
}
4.3.2 字段注入(需要场景)
@AndroidEntryPoint
class MainActivity : ComponentActivity() {@Injectlateinit var markdownRenderer: MarkdownRenderer // 字段注入override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// markdownRenderer 已经在 super.onCreate() 前被赋值Log.d("MainActivity", "MarkdownRenderer: $markdownRenderer")}
}
字段注入的执行时机:
@AndroidEntryPoint生成的代码会在super.onCreate()之前注入所有@Inject字段- 因此可以在
onCreate()中安全使用注入的字段
何时使用字段注入 vs 构造注入?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 可以修改的类(自己的代码) | 构造注入 | 更安全,依赖不可变 |
| 无法修改的类(第三方库) | 字段注入 | 无法修改构造函数 |
| Android 组件(Activity/Fragment) | 字段注入 | Android 组件由系统创建,无法构造注入 |
4.4 @ApplicationContext 详解
@Singleton
class AndroidMediaImporter @Inject constructor(@ApplicationContext private val context: Context,
) : MediaImporter {// Hilt 会自动注入 Application Context
}
@ApplicationContext 的原理:
- Hilt 内置了对
Context类型的特殊处理 - 标注
@ApplicationContext后,Hilt 注入的是context.applicationContext - 这比直接注入
Context更安全,避免内存泄漏
相关限定符:
| 限定符 | 提供类型 | 使用场景 |
|---|---|---|
@ApplicationContext |
Application Context | 通常的 Android API 调用,避免内存泄漏 |
@ActivityContext |
当前 Activity Context | 需要 Activity 能力(如启动 Activity、获取 Window)的场景 |
5. Application 入口配置详解
5.1 @HiltAndroidApp 的作用
@HiltAndroidApp
class RichNoteApplication : Application()
@HiltAndroidApp 的职责:
@HiltAndroidApp 注解的处理流程:1. 触发 Hilt 注解处理器
2. 生成 Hilt_RichNoteApplication 基类:// 生成的伪代码open class Hilt_RichNoteApplication : Application() {private lateinit var entryPointMap: Map<TypeLiteral, ?>override fun onCreate() {super.onCreate()// 1. 初始化 SingletonComponentval component = DaggerSingletonComponent.builder().application(this).build()// 2. 存储 EntryPoint 用于视图注入entryPointMap = componentMapOf(EntryPointAccessors::class.java to component)// 3. 缓存 component 供后续使用singletonComponent = component}}3. RichNoteApplication 继承 Hilt_RichNoteApplication
4. App 启动时自动初始化所有 @Singleton 依赖
5.2 生成的 Component 结构
// 生成的 SingletonComponent 伪代码
@Singleton
@Component(modules = [// 自动收集所有 @Module @InstallIn(SingletonComponent::class)MarkdownProviderModule::class,NoteDatabaseModule::class,NoteRepositoryModule::class,DataStoreProviderModule::class,DataStoreBindModule::class,MediaModule::class
])
interface SingletonComponent {// 所有 @Provides/@Binds 的 binding 方法
}
Hilt 如何收集所有 Module?
- 编译时扫描所有模块的 classpath
- 查找所有标注了
@Module和@InstallIn(SingletonComponent::class)的类 - 自动生成 Component 的
modules参数
5.3 Application 中的额外初始化
RichEditor 中的 Application 还做了额外的工作:
@HiltAndroidApp
class RichNoteApplication : Application() {override fun onCreate() {super.onCreate()INSTANCE = this}companion object {// 用于在非 Hilt 管理的代码中获取 Application 实例@JvmStaticlateinit var INSTANCE: RichNoteApplicationprivate set@JvmStaticfun getInstance(): RichNoteApplication = INSTANCE}
}
为什么要保留 INSTANCE?
某些场景可能需要在 Hilt 外部获取 Application:
- 第三方 SDK 初始化需要 Context
- 某些静态工具类需要 Application
更推荐的做法:尽量使用依赖注入,只有在无法注入时才使用 INSTANCE:
// 推荐:直接在需要的地方注入
class ThirdPartySDKWrapper @Inject constructor(@ApplicationContext private val context: Context
) {fun initialize() {ThirdPartySDK.init(context)}
}// 不推荐:使用 INSTANCE
fun someLegacyCode() {val app = RichNoteApplication.getInstance()ThirdPartySDK.init(app)
}
5.4 AndroidManifest.xml 配置
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"><applicationandroid:name=".RichNoteApplication"android:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:theme="@style/Theme.RichNote"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
android:name=".RichNoteApplication" 是必须的:
- 如果不声明,Hilt 无法初始化
- App 启动时不会创建 Hilt 组件
- 所有
@AndroidEntryPoint的类会崩溃
6. Activity 注入详解
6.1 @AndroidEntryPoint 的作用
@AndroidEntryPoint
class MainActivity : ComponentActivity() {// ...
}
@AndroidEntryPoint 生成的代码:
Hilt 通过字节码转换,使 MainActivity 实际上继承生成的 Hilt_MainActivity,继承链为:
MainActivity → Hilt_MainActivity → ComponentActivity
// 编译生成的伪代码(Hilt 字节码转换后的继承结构)
@Generated("Hilt")
abstract class Hilt_MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {// Hilt 在 super.onCreate() 之前注入所有 @Inject 字段injectMembers()super.onCreate(savedInstanceState)}private fun injectMembers() {// 从 SingletonComponent 获取/创建 MarkdownRendererval entryPoint = EntryPointAccessors.fromActivity(this, MainActivityEntryPoint::class.java)this.markdownRenderer = entryPoint.markdownRenderer()}
}// 你写的代码:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {@Inject lateinit var markdownRenderer: MarkdownRenderer// 经过 Hilt 字节码转换后,实际继承自 Hilt_MainActivity
}
6.2 注入时序图
MainActivity 生命周期与注入时序:┌─────────────────────────────────────────────────────────────┐
│ MainActivity │
├─────────────────────────────────────────────────────────────┤
│ │
│ onCreate(savedInstanceState: Bundle?) │
│ │ │
│ ├──┐ injectFields() ← 在这里完成注入 │
│ │ │ │
│ │ │ Hilt 解析依赖图: │
│ │ │ MarkdownRenderer │
│ │ │ └─ MarkwonMarkdownRenderer │
│ │ │ └─ Markwon │
│ │ │ └─ MarkwonBuilder.build() │
│ │ │ │
│ │ │ 如果已缓存,直接返回 │
│ │ │ 如果未创建,按依赖顺序创建 │
│ │ │ │
│ ├──┘ 注入完成 │
│ │ │
│ ▼ super.onCreate() │
│ │
│ setContent { ... } ← 可以安全使用 markdownRenderer │
│ │
│ onDestroy() │
│ │ │
│ ▼ super.onDestroy() │
│ │
└─────────────────────────────────────────────────────────────┘
6.3 字段注入的细节
@AndroidEntryPoint
class MainActivity : ComponentActivity() {// 1. 基本用法@Injectlateinit var markdownRenderer: MarkdownRenderer// 2. 如果需要延迟初始化(少见)@Injectlateinit var lazyService: Lazy<MediaImporter> // Lazy< T > 延迟访问// 3. 多实例(通过 Provider)@Injectlateinit var provider: Provider<NoteDao> // Provider< T > 按需创建
}
Lazy vs Provider:
| 类型 | 用途 | 示例 |
|---|---|---|
T |
立即注入 | @Inject lateinit var renderer: MarkdownRenderer |
Lazy<T> |
延迟访问(首次访问时才创建) | @Inject lateinit var service: Lazy<Service> |
Provider<T> |
多次创建/可选创建 | @Inject lateinit var provider: Provider<Dao> |
// Lazy 用法示例
class SomeClass @Inject constructor(private val lazyService: Lazy<HeavyService>
) {fun doSomething() {// 只在首次调用时才创建 HeavyServiceval service = lazyService.get()service.perform()}
}// Provider 用法示例
class SomeClass @Inject constructor(private val noteDaoProvider: Provider<NoteDao>
) {fun optionalOperation(shouldCreate: Boolean) {if (shouldCreate) {// 每次调用 get() 都创建新实例val dao = noteDaoProvider.get()dao.deleteAll()}}
}
7. Module 的两种写法:@Provides vs @Binds
7.1 @Provides 详解
7.1.1 基本语法
@Module
@InstallIn(SingletonComponent::class)
object MarkdownProviderModule {@Provides@Singletonfun provideMarkwon(@ApplicationContext context: Context): Markwon {return Markwon.builder(context).usePlugin(ImagesPlugin.create()).usePlugin(GlideImagesPlugin.create(context)).usePlugin(StrikethroughPlugin.create()).usePlugin(TaskListPlugin.create(context)).usePlugin(TablePlugin.create(context)).usePlugin(object : AbstractMarkwonPlugin() {override fun configureTheme(builder: MarkwonTheme.Builder) {builder.blockMargin(8)}}).build()}
}
@Provides 方法的规则:
- 必须位于
@Module注解的 class 或 object 中 - 必须返回提供类型的实例
- 方法名可以是任意名称(Hilt 按返回类型匹配)
- 可以有参数,参数会自动作为依赖解析
7.1.2 参数依赖解析
@Module
@InstallIn(SingletonComponent::class)
object NoteDatabaseModule {@Provides@Singletonfun provideRichNoteDatabase(@ApplicationContext context: Context // Hilt 自动提供): RichNoteDatabase {return Room.databaseBuilder(context,RichNoteDatabase::class.java,"rich_note.db").build()}// 参数 database 由上一个 @Provides 方法提供@Provides@Singletonfun provideNoteDao(database: RichNoteDatabase): NoteDao {return database.noteDao()}
}
依赖解析流程:
provideRichNoteDatabase 需要 Context→ 找到 @ApplicationContext,注入 Application Context→ 创建 RichNoteDatabaseprovideNoteDao 需要 RichNoteDatabase→ 由 provideRichNoteDatabase 提供→ 创建 NoteDao
7.2 @Binds 详解
7.2.1 基本语法
@Module
@InstallIn(SingletonComponent::class)
abstract class MediaModule {@Binds@Singletonabstract fun bindMediaImporter(impl: AndroidMediaImporter): MediaImporter
}
@Binds 方法的规则:
- 方法必须是
abstract - Module 类必须是
abstract class - 必须返回接口类型
- 参数必须是接口的实现类
- 方法名可以是任意名称(Hilt 按类型匹配)
7.2.2 @Binds vs @Provides 对比
| 特性 | @Binds | @Provides |
|---|---|---|
| Module 类型 | abstract class |
object |
| 方法类型 | abstract |
普通函数 |
| 返回值 | 接口类型 | 任意类型 |
| 使用场景 | 接口→实现映射 | 复杂构建逻辑 |
| 编译产物 | Binding | Provider |
| 性能 | 零开销 | 轻微开销 |
为什么 @Binds 效率更高?
// @Binds 生成的代码
// 直接返回 impl,无额外处理
fun bindMediaImporter(impl: AndroidMediaImporter): MediaImporter = impl// @Provides 生成的代码
// 需要调用方法体
fun provideMediaImporter(): MediaImporter = Markwon.builder(context)...build()
7.3 链式依赖详解
在实际项目中,经常遇到需要从已有实例提取子依赖的场景:
@Module
@InstallIn(SingletonComponent::class)
object NoteDatabaseModule {// 1. 提供 Database@Provides@Singletonfun provideRichNoteDatabase(@ApplicationContext context: Context): RichNoteDatabase {return Room.databaseBuilder(context,RichNoteDatabase::class.java,"rich_note.db").build()}// 2. 从 Database 提取 NoteDao@Provides@Singletonfun provideNoteDao(database: RichNoteDatabase): NoteDao {return database.noteDao()}// 3. 从 Database 提取 NoteAssetDao@Provides@Singletonfun provideNoteAssetDao(database: RichNoteDatabase): NoteAssetDao {return database.noteAssetDao()}
}
依赖图:
provideRichNoteDatabase(Context)│▼RichNoteDatabase│├──┬──► provideNoteDao()│ │ ││ │ ▼│ │ NoteDao│ │└──┴──► provideNoteAssetDao()│▼NoteAssetDao
Hilt 如何处理依赖顺序?
- Hilt 构建依赖图时使用拓扑排序
- 自动识别
provideNoteDao依赖provideRichNoteDatabase - 按正确顺序创建实例
7.4 同一文件的混合写法
如文档所述,@Binds 和 @Provides 不能放在同一个 Module 中,但可以放在同一文件:
// data/data-note/src/main/kotlin/.../di/NoteDataModule.kt// 第一部分:@Provides 用于 Database 和 DAO
@Module
@InstallIn(SingletonComponent::class)
object NoteDatabaseModule {@Provides@Singletonfun provideRichNoteDatabase(@ApplicationContext context: Context): RichNoteDatabase {return Room.databaseBuilder(context,RichNoteDatabase::class.java,"rich_note.db").fallbackToDestructiveMigration().build()}@Provides@Singletonfun provideNoteDao(database: RichNoteDatabase): NoteDao {return database.noteDao()}@Provides@Singletonfun provideNoteAssetDao(database: RichNoteDatabase): NoteAssetDao {return database.noteAssetDao()}
}// 第二部分:@Binds 用于 Repository
@Module
@InstallIn(SingletonComponent::class)
abstract class NoteRepositoryModule {@Binds@Singletonabstract fun bindNoteRepository(impl: RoomNoteRepository): NoteRepository
}
为什么一个文件两个 Module?
- 关注点分离:
NoteDatabaseModule负责数据库相关,NoteRepositoryModule负责 Repository - 便于维护:Database 变更只改第一个,Repository 变更只改第二个
- 逻辑清晰:@Provides 和 @Binds 本质上是不同的概念
7.5 第三方库注入示例
@Module
@InstallIn(SingletonComponent::class)
object ThirdPartyModule {// 1. 第三方类,无法修改构造函数@Provides@Singletonfun provideMarkwon(@ApplicationContext context: Context): Markwon {return Markwon.builder(context).usePlugin(ImagesPlugin.create()).usePlugin(GlideImagesPlugin.create(context)).usePlugin(StrikethroughPlugin.create()).usePlugin(TaskListPlugin.create(context)).usePlugin(TablePlugin.create(context)).build()}// 2. 配置类注入@Providesfun provideAppConfig(@ApplicationContext context: Context): AppConfig {return AppConfig.fromAssets(context, "config.json")}// 3. 多参数配置@Provides@Singletonfun provideOkHttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient {return OkHttpClient.Builder().addInterceptor(loggingInterceptor).connectTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build()}
}
8. ViewModel 注入详解
8.1 @HiltViewModel 的原理
@HiltViewModel
class NoteEditorViewModel @Inject constructor(savedStateHandle: SavedStateHandle,private val noteRepository: NoteRepository,
) : ViewModel() {// ...
}
@HiltViewModel 生成的代码:
// Hilt 生成的 Factory 伪代码
@dagger.Module
object Hilt_NoteEditorViewModel {@Providesfun provideFactory(savedStateHandleHolder: SavedStateHandleProvider,noteRepository: NoteRepository): NoteEditorViewModel {return NoteEditorViewModel(savedStateHandle = savedStateHandleHolder.get(),noteRepository = noteRepository)}
}// SavedStateHandle 的提供机制
@Module
@InstallIn(SingletonComponent::class)
object SavedStateHandleModule {@Providesfun provideSavedStateHandleProvider(): SavedStateHandleProvider {return SavedStateHandleProvider()}
}
8.2 SavedStateHandle 详解
SavedStateHandle 是 Hilt 内置支持的类型,用于在 ViewModel 中访问导航参数:
@HiltViewModel
class NoteEditorViewModel @Inject constructor(savedStateHandle: SavedStateHandle,private val noteRepository: NoteRepository,
) : ViewModel() {// 从导航参数获取 noteIdprivate val noteId: Long = savedStateHandle["noteId"] ?: -1L// 也可以使用 getLiveData/getFlowval noteTitle: String = savedStateHandle.get<String>("noteTitle") ?: ""// 更新导航参数fun updateTitle(newTitle: String) {savedStateHandle["noteTitle"] = newTitle// Note: 更改不会自动同步到 Navigation,需要配合 saveState 处理}
}
SavedStateHandle 与 Navigation 配合:
// Navigation 定义
@Composable
fun NavHost() {NavHost(navController) {composable(route = "note_editor/{noteId}",arguments = listOf(navArgument("noteId") { type = NavType.LongType })) { backStackEntry ->// SavedStateHandle 会自动从 NavBackStackEntry 获取参数NoteEditorScreen()}}
}// ViewModel 中使用(无需手动传递)
@HiltViewModel
class NoteEditorViewModel @Inject constructor(savedStateHandle: SavedStateHandle, // 自动携带 noteIdprivate val noteRepository: NoteRepository,
) : ViewModel() {private val noteId: Long = savedStateHandle["noteId"] ?: -1L// 自动获取导航参数,无需额外配置
}
8.3 hiltViewModel() 详解
@Composable
fun NoteEditorRoute(viewModel: NoteEditorViewModel = hiltViewModel()
) {val uiState by viewModel.uiState.collectAsStateWithLifecycle()// ...
}
hiltViewModel() 的执行流程:
hiltViewModel() 执行流程:1. 获取当前 Compose 上下文│▼
2. 从 LocalContext 获取 NavBackStackEntry│(如果没有 Navigation 环境,会抛出异常)▼
3. 获取 NavBackStackEntry 的 ViewModelStoreOwner│▼
4. 检查 ViewModelStore 中是否已有 NoteEditorViewModel 实例│├──┬── 已有 → 返回缓存实例│ │└──┴── 没有 → 使用 Hilt 生成的 Factory 创建│▼Hilt 自动注入:- SavedStateHandle(从 NavBackStackEntry 获取)- NoteRepository(从 SingletonComponent 获取)│▼返回新创建的 NoteEditorViewModel
8.4 ViewModel 作用域详解
@HiltViewModel
class NoteEditorViewModel @Inject constructor(// 这里注入的依赖与 ViewModel 生命周期一致// ViewModel 销毁时,依赖也随之释放private val noteRepository: NoteRepository,
) : ViewModel() {// NoteRepository 是 Singleton,但如果被标记为 @ActivityRetainedScoped,// 则会随 ViewModel 一起销毁
}
作用域层级:
| 依赖作用域 | 实例生命周期 | 适用场景 |
|---|---|---|
@Singleton |
App 存活期间 | Repository、Database、Config |
@ActivityRetainedScoped |
Activity 配置变更期间 | ViewModel 需要的数据 |
@ViewModelScoped (Hilt 2.48+) |
ViewModel 存活期间 | 临时数据 |
8.5 多依赖注入与 @Qualifier 示例
当同一类型(如 String)有多个实例需要注入时,必须使用 @Named 或自定义 @Qualifier 来区分:
// 1. 定义 @Named 提供方式
@Module
@InstallIn(SingletonComponent::class)
object AppConfigModule {@Provides@Named("appVersion")fun provideAppVersion(): String = BuildConfig.VERSION_NAME@Provides@Named("baseUrl")fun provideBaseUrl(): String = "https://api.example.com"
}// 2. 在 ViewModel 中通过 @Named 区分
@HiltViewModel
class SettingsViewModel @Inject constructor(// 1. SavedStateHandle - Hilt 内置savedStateHandle: SavedStateHandle,// 2. 自定义 Repository - 通过 @Binds 解析private val settingsRepository: SettingsRepository,// 3. 另一个 Repositoryprivate val noteRepository: NoteRepository,// 4. 相同类型需要 @Named 限定符区分@Named("appVersion") private val appVersion: String,
) : ViewModel() {// Hilt 自动解析所有依赖// 无需手动创建任何对象
}
自定义 @Qualifier 的写法(比 @Named 更类型安全):
// 定义 Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppVersion@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BaseUrl// 使用自定义 Qualifier
@Module
@InstallIn(SingletonComponent::class)
object AppConfigModule {@Provides@AppVersionfun provideAppVersion(): String = BuildConfig.VERSION_NAME@Provides@BaseUrlfun provideBaseUrl(): String = "https://api.example.com"
}// ViewModel 中使用
@HiltViewModel
class MyViewModel @Inject constructor(@AppVersion private val appVersion: String,@BaseUrl private val baseUrl: String,
) : ViewModel()
@Named vs 自定义 @Qualifier:
| 方式 | 优势 | 劣势 |
|---|---|---|
@Named("key") |
无需定义额外类 | 字符串容易拼错,IDE 无法检查 |
自定义 @Qualifier |
编译期类型安全,IDE 可跳转 | 需要定义注解类 |
---## 9. 多模块项目中的 Hilt 实践### 9.1 模块隔离原则RichEditor 的每个模块只声明自己的依赖,形成自治单元:
┌─────────────────────────────────────────────────────────────────┐
│ app 模块 │
│ 职责:装配入口 │
│ 包含:@HiltAndroidApp + @AndroidEntryPoint MainActivity │
└─────────────────────────────────────────────────────────────────┘
│
│ 依赖声明(通过 Module 里的 @Binds/@Provides)
▼
┌─────────────────────────────────────────────────────────────────┐
│ core 模块 │
│ │
│ core-markdown/ │
│ ├── 提供:Markwon、MarkdownRenderer │
│ └── DI 文件:MarkdownModule.kt │
│ │
│ core-datastore/ │
│ ├── 提供:DataStore、SettingsRepository │
│ └── DI 文件:DataStoreModule.kt │
│ │
│ core-media/ │
│ ├── 提供:MediaImporter │
│ └── DI 文件:MediaModule.kt │
│ │
│ core-database/ │
│ ├── 提供:RichNoteDatabase │
│ └── DI 文件:DatabaseModule.kt │
└─────────────────────────────────────────────────────────────────┘
│
│ 依赖声明(@Binds 接口,@Provides 实现)
▼
┌─────────────────────────────────────────────────────────────────┐
│ data 模块 │
│ │
│ data-note/ │
│ ├── 提供:NoteDao、NoteAssetDao、NoteRepository │
│ └── DI 文件:NoteDataModule.kt │
└─────────────────────────────────────────────────────────────────┘
│
│ 消费依赖(通过 @HiltViewModel)
▼
┌─────────────────────────────────────────────────────────────────┐
│ feature 模块 │
│ │
│ feature-note-editor/ │
│ ├── 消费:NoteRepository │
│ └── ViewModel:NoteEditorViewModel │
│ │
│ feature-note-list/ │
│ ├── 消费:NoteRepository │
│ └── ViewModel:NoteListViewModel │
│ │
│ feature-settings/ │
│ ├── 消费:SettingsRepository、NoteRepository │
│ └── ViewModel:SettingsViewModel │
└─────────────────────────────────────────────────────────────────┘
### 9.2 跨模块依赖链详解以 `NoteEditorViewModel` 为例,完整依赖链路:
feature-note-editor/NoteEditorViewModel
│
├── 依赖:NoteRepository
│ │
│ └── 由 data-note/NoteRepositoryModule 通过 @Binds 提供
│ │
│ └── 实现:RoomNoteRepository
│ │
│ ├── 依赖:NoteDao
│ │ │
│ │ └── 由 data-note/NoteDatabaseModule 通过 @Provides 提供
│ │ │
│ │ └── 来自:RichNoteDatabase
│ │ │
│ │ └── 由 data-note/NoteDatabaseModule 通过 @Provides 提供
│ │ │
│ │ └── 依赖:@ApplicationContext
│ │ │
│ │ └── Hilt 内置提供
│ │
│ ├── 依赖:NoteAssetDao
│ │ │
│ │ └── 与 NoteDao 同一依赖链
│ │
│ └── 依赖:MediaImporter
│ │
│ └── 由 core-media/MediaModule 通过 @Binds 提供
│ │
│ └── 实现:AndroidMediaImporter
│ │
│ └── 依赖:@ApplicationContext
│ │
│ └── Hilt 内置提供
│
└── 依赖:SavedStateHandle
│
└── Hilt 内置提供(从 Navigation 传递)
**整个链路中,你只需要写 NoteEditorViewModel 类的声明**:
```kotlin
@HiltViewModel
class NoteEditorViewModel @Inject constructor(savedStateHandle: SavedStateHandle,private val noteRepository: NoteRepository,
) : ViewModel()
其余所有对象的创建、依赖解析、缓存管理全部由 Hilt 自动完成。
9.3 @EntryPoint 详解(高级用法)
当需要在非 Hilt 管理的代码中访问依赖时,使用 @EntryPoint:
// 1. 定义 EntryPoint 接口
@EntryPoint
@InstallIn(SingletonComponent::class)
interface RichNoteEntryPoint {fun noteRepository(): NoteRepositoryfun settingsRepository(): SettingsRepository
}// 2. 在非 Hilt 代码中使用
class LegacyCode {fun someMethod(context: Context) {// 获取 EntryPointval entryPoint = EntryPointAccessors.fromApplication(context,RichNoteEntryPoint::class.java)// 访问依赖val repository = entryPoint.noteRepository()repository.getAllNotes()}
}
使用场景:
- 第三方 SDK 的回调
- ContentProvider
- 非 Hilt 管理的类
- 需要动态获取依赖的场景
为什么需要 EntryPoint?
- Hilt 管理的类通过
@AndroidEntryPoint自动获得注入 - 非 Hilt 管理的类无法直接访问 DI 容器
@EntryPoint是访问容器的「安全门禁」
9.4 模块间接口隔离
// core/core-media/src/main/kotlin/.../MediaImporter.kt
// 接口定义在 core 模块interface MediaImporter {suspend fun importMedia(uri: Uri): MediaResult
}// core/core-media/src/main/kotlin/.../AndroidMediaImporter.kt
// 实现也在 core 模块@Singleton
class AndroidMediaImporter @Inject constructor(@ApplicationContext private val context: Context,
) : MediaImporter {override suspend fun importMedia(uri: Uri): MediaResult {// Android 特定实现}
}// data/data-note/src/main/kotlin/.../RoomNoteRepository.kt
// data 模块依赖接口,不依赖具体实现@Singleton
class RoomNoteRepository @Inject constructor(private val noteDao: NoteDao,private val noteAssetDao: NoteAssetDao,private val mediaImporter: MediaImporter, // 只依赖接口
) : NoteRepository {override suspend fun saveNote(note: Note): Long {// 使用 mediaImporter,不关心具体实现}
}
这种设计的优势:
feature-note-editor只需要知道NoteRepository接口NoteRepository只需要知道MediaImporter接口- 具体实现可以随时替换(单元测试时替换为 Mock)
10. 依赖图总览
10.1 完整依赖图
┌──────────────────────────────────────────────────────────────────────────────┐
│ SingletonComponent │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Providers (@Provides) │ │
│ │ │ │
│ │ @ApplicationContext Context ──┬──► Markwon │ │
│ │ ├──► RichNoteDatabase ──┬──► NoteDao │ │
│ │ │ └──► NoteAssetDao│ │
│ │ ├──► DataStore<Preferences> │ │
│ │ └──► AndroidMediaImporter │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Bindings (@Binds) │ │
│ │ │ │
│ │ MediaImporter ◄─── AndroidMediaImporter │ │
│ │ MarkdownRenderer ◄─── MarkwonMarkdownRenderer ◄── Markwon │ │
│ │ SettingsRepository ◄─── DataStoreSettingsRepository │ │
│ │ NoteRepository ◄─── RoomNoteRepository │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ ViewModels (@HiltViewModel) │ │
│ │ │ │
│ │ NoteListViewModel │ │
│ │ └─► NoteRepository │ │
│ │ │ │
│ │ NoteEditorViewModel │ │
│ │ ├─► SavedStateHandle │ │
│ │ └─► NoteRepository │ │
│ │ │ │
│ │ NoteDetailViewModel │ │
│ │ ├─► SavedStateHandle │ │
│ │ └─► NoteRepository │ │
│ │ │ │
│ │ SettingsViewModel │ │
│ │ ├─► SettingsRepository │ │
│ │ └─► NoteRepository │ │
│ │ │ │
│ │ RecycleBinViewModel │ │
│ │ └─► NoteRepository │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
10.2 依赖流向图
依赖流向:从外部(Context)到内部(ViewModel)外部依赖│├── @ApplicationContext ──────────────────────────────────────┐│ │└── @Provides 配置 ││ │▼ │┌─────────────────────────────────────────────────────────────┐ ││ 基础设施层 │ ││ │ ││ RichNoteDatabase ─────────────────────────────► NoteDao │ ││ RichNoteDatabase ─────────────────────────► NoteAssetDao │ ││ DataStore ─────────────────────────────────► Settings │ ││ Markwon ───────────────────────────────────► Renderer │ ││ AndroidMediaImporter ─────────────────────► Importer │ │└─────────────────────────────────────────────────────────────┘ ││ ││ @Binds 接口绑定 │▼ │┌─────────────────────────────────────────────────────────────┐ ││ 数据层 │ ││ │ ││ NoteRepository ◄────── RoomNoteRepository ◄─────────────┐ │ ││ SettingsRepository ◄── DataStoreSettingsRepository ◄───┘ │ ││ MediaImporter ◄──────── AndroidMediaImporter ◄────────┘ │ ││ MarkdownRenderer ◄───── MarkwonMarkdownRenderer ◄────────┘ │ │└─────────────────────────────────────────────────────────────┘ ││ │▼ │┌─────────────────────────────────────────────────────────────┐ ││ 视图层 │ ││ │ ││ NoteListViewModel ─────────────────────────────────► 显示便签列表 │ ││ NoteEditorViewModel ─────────────────────────────────► 编辑便签 │ ││ NoteDetailViewModel ─────────────────────────────────► 查看便签 │ ││ SettingsViewModel ───────────────────────────────────► 设置页面 │ ││ RecycleBinViewModel ──────────────────────────────────► 回收站 │ │└─────────────────────────────────────────────────────────────┘ │
11. 常见问题与排坑
Q1: 编译报错 "xxx cannot be provided without an @Provides-annotated method"
详细排查步骤:
Step 1: 检查类型是否有 @Inject 构造函数class MyClass @Inject constructor()├── ✓ 有 → 进入 Step 2└── ✗ 没有 → 需要添加 @Inject 或提供 @ProvidesStep 2: 检查 Module 配置@Module@InstallIn(SingletonComponent::class) ← 检查这一行object MyModule {@Providesfun provideMyClass(): MyClass = MyClass() ← 检查方法注解}├── ✓ 配置正确 → 进入 Step 3└── ✗ 配置错误 → 修正 ModuleStep 3: 检查模块是否正确配置 Hiltbuild.gradle.kts:plugins {alias(libs.plugins.hilt.android) ← 检查}dependencies {implementation(libs.hilt.android) ← 检查kapt(libs.hilt.compiler) ← 检查(必须是 kapt)}├── ✓ 配置正确 → 进入 Step 4└── ✗ 配置错误 → 修正 build.gradleStep 4: 检查依赖是否在 classpath 中检查模块是否被正确依赖app/build.gradle.kts:dependencies {implementation(project(":data:data-note")) ← 检查}
Q2: 新增模块后注入失败
Checklist(确保每个模块都包含):
// build.gradle.kts (新增模块)
plugins {alias(libs.plugins.android.library) // ✓ 或 android.applicationalias(libs.plugins.kotlin.android)alias(libs.plugins.kotlin.kapt) // ✓ 必须alias(libs.plugins.hilt.android) // ✓ 必须
}android {namespace = "com.example.richtext.core.xxx"
}dependencies {implementation(libs.hilt.android) // ✓ 必须kapt(libs.hilt.compiler) // ✓ 必须(不是 implementation)
}
常见错误:
- 只加了
implementation(libs.hilt.android),忘记kapt(libs.hilt.compiler) - 把
kapt写成了implementation - Module 是
object却没有添加@Provides方法
Q3: @Binds 和 @Provides 混用报错
原因:Kotlin 不允许 abstract function in non-abstract class
@Module
@InstallIn(SingletonComponent::class)
object MyModule { // object = 具体类,不能有 abstract function@Binds // ✗ Binds 要求 abstract functionabstract fun bindService(impl: ServiceImpl): Service // 语法错误@Provides // ✓ Provides 可以在 object 中fun provideDatabase(): Database = ...
}
解决方案:同一文件中定义两个 Module:
@Module
@InstallIn(SingletonComponent::class)
object MyProviderModule { // 放 @Provides@Providesfun provideDatabase(): Database = ...
}@Module
@InstallIn(SingletonComponent::class)
abstract class MyBindModule { // 放 @Binds@Bindsabstract fun bindService(impl: ServiceImpl): Service
}
Q4: ViewModel 中获取导航参数失败
问题:SavedStateHandle["noteId"] 返回 null
原因:Navigation 和 Hilt 的 SavedStateHandle 集成需要额外配置
解决方案:
// 1. 确保使用了 hilt-navigation-compose
dependencies {implementation(libs.hilt.navigation.compose)
}// 2. NavHost 必须在 Hilt 管理的 Activity/Fragment 中
@AndroidEntryPoint
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {NavHost(...) // ← 使用 Compose Navigation}}
}// 3. ViewModel 正确接收 SavedStateHandle
@HiltViewModel
class NoteEditorViewModel @Inject constructor(savedStateHandle: SavedStateHandle, // ← 自动注入private val noteRepository: NoteRepository,
) : ViewModel() {// noteId 会自动从 Navigation arguments 获取private val noteId: Long = savedStateHandle["noteId"] ?: -1L
}
Q5: 为什么所有 Module 都用 SingletonComponent?
SingletonComponent vs 其他 Component:
@Module
@InstallIn(SingletonComponent::class) // ✓ 推荐
object MyModule { ... }@Module
@InstallIn(ActivityComponent::class) // 较少使用
object MyModule { ... }
| 场景 | 推荐 Component | 原因 |
|---|---|---|
| 数据层(Repository、Database) | SingletonComponent | 需要跨 Activity 存活 |
| 配置对象 | SingletonComponent | 全局配置 |
| UI 状态管理器 | ActivityRetainedComponent | 配置变更后仍需存活 |
| Activity 级临时对象 | ActivityComponent | 仅限单个 Activity |
什么情况下用非 Singleton Component?
- 需要在 Activity 销毁时清理依赖
- 依赖持有 Activity Context(应避免)
- 需要在每个 Activity 实例化一次
Q6: kapt vs ksp
性能对比:
| 指标 | kapt | ksp |
|---|---|---|
| 编译速度 | 较慢 | 较快(2-3 倍) |
| 增量编译 | 支持但效率一般 | 原生支持增量编译 |
| 内存占用 | 较高 | 较低 |
| Hilt 支持 | ✓ 完全支持 | ✓ 从 2.48 开始支持 |
迁移到 KSP:
// build.gradle.kts (Module level)
plugins {alias(libs.plugins.hilt.android)alias(libs.plugins.kotlin.kapt) // 移除alias(libs.plugins.kotlin.ksp) // 添加
}dependencies {implementation(libs.hilt.android)kapt(libs.hilt.compiler) // 改为 kspksp(libs.hilt.compiler) // ✓ 这样写
}
注意:迁移前检查依赖库的 ksp 支持:
// Room 的 KSP 支持
plugins {alias(libs.plugins.room) apply false // 先在 libs.versions.toml 确认 room-ksp 版本
}
Q7: 循环依赖怎么办
问题:A 依赖 B,B 依赖 A
class A @Inject constructor(val b: B)
class B @Inject constructor(val a: A) // ✗ 循环依赖
解决方案:
| 方案 | 示例 | 适用场景 |
|---|---|---|
使用 dagger.Lazy<T> |
class A @Inject constructor(val b: dagger.Lazy<B>) |
打破初始化顺序依赖 |
| 提取接口 | A 依赖 BInterface,B 实现 BInterface |
循环发生在实现细节 |
| 重新设计 | 将共享依赖提取到 C | 设计问题,需要重构 |
// 方案 1:使用 dagger.Lazy<T> 延迟初始化
class A @Inject constructor(val b: dagger.Lazy<B>) {fun doSomething() {b.get().method() // 首次访问时才创建 B}
}// 方案 2:提取接口
interface BInterface {fun method()
}class A @Inject constructor(val bInterface: BInterface)
class B @Inject constructor(val a: A) : BInterface// 方案 3:引入第三方 C
class A @Inject constructor(val c: C)
class B @Inject constructor(val c: C)
Q8: Scope 与内存泄漏
常见陷阱:@Singleton 对象持有 Activity Context
// ✗ 危险:Singleton 持有 Activity Context 会导致内存泄漏
@Singleton
class BadService @Inject constructor(@ActivityContext private val context: Context // Activity 销毁后仍被持有!
)// ✓ 安全:Singleton 使用 Application Context
@Singleton
class GoodService @Inject constructor(@ApplicationContext private val context: Context // Application 生命周期一致
)
规则:
@Singleton标记的类只能注入@ApplicationContext- 需要
@ActivityContext的类不应标记为@Singleton - 如果确实需要 Activity Context + 长生命周期,考虑使用
@ActivityRetainedScoped
各 Scope 的 Context 使用规则:
| Scope | 可用 @ApplicationContext | 可用 @ActivityContext |
|---|---|---|
@Singleton |
✓ | ✗(内存泄漏) |
@ActivityRetainedScoped |
✓ | ✗(配置变更后无效) |
@ActivityScoped |
✓ | ✓ |
@FragmentScoped |
✓ | ✓ |
Q9: 如何调试 Hilt 依赖
方法 1:查看生成的代码
build/generated/hilt/.../
├── component/
│ └── SingletonComponent.java ← 生成的 Component
├── modules/
│ ├── DataStoreModule_ProvideDataStoreFactory.java
│ └── NoteDatabaseModule_ProvideNoteDaoFactory.java
└── viewModels/└── NoteEditorViewModel_HiltComponents.java
方法 2:启用 Hilt 日志
@HiltAndroidApp
class MyApplication : Application() {override fun onCreate() {if (BuildConfig.DEBUG) {// Hilt 不提供日志 API,但可以通过 Dagger 的 internal 方式查看}super.onCreate()}
}
方法 3:使用 dagger-injector(仅调试用)
// 调试时获取实例
val component = EntryPointAccessors.fromApplication(appContext,SomeEntryPoint::class.java
)
val instance = component.getSomething()
12. Hilt 测试支持
Hilt 提供了完善的测试基础设施,可以轻松替换依赖进行单元测试和集成测试。
12.1 测试环境配置
// build.gradle.kts (app 或 feature 模块)
dependencies {// Hilt 测试库androidTestImplementation(libs.hilt.android.testing)kaptAndroidTest(libs.hilt.compiler)// 单元测试testImplementation(libs.hilt.android.testing)kaptTest(libs.hilt.compiler)
}
12.2 @HiltAndroidTest 基本用法
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class NoteRepositoryTest {// HiltAndroidRule 负责在测试前初始化 Hilt@get:Rulevar hiltRule = HiltAndroidRule(this)// 注入测试目标@Injectlateinit var noteRepository: NoteRepository@Beforefun setup() {// 必须调用 inject() 完成注入hiltRule.inject()}@Testfun testSaveNote() = runTest {val note = Note(title = "Test", content = "Hello")val id = noteRepository.saveNote(note)assertThat(id).isGreaterThan(0)}
}
12.3 @UninstallModules 替换依赖
在测试中替换真实模块为测试用模块:
@HiltAndroidTest
@UninstallModules(NoteRepositoryModule::class) // 移除生产环境的绑定
class NoteListViewModelTest {@get:Rulevar hiltRule = HiltAndroidRule(this)// 提供测试用的 Module@Module@InstallIn(SingletonComponent::class)abstract class TestNoteRepositoryModule {@Binds@Singletonabstract fun bindNoteRepository(impl: FakeNoteRepository): NoteRepository}@Injectlateinit var noteRepository: NoteRepository@Beforefun setup() {hiltRule.inject()}@Testfun testNoteList() {// noteRepository 此时是 FakeNoteRepositoryassertThat(noteRepository).isInstanceOf(FakeNoteRepository::class.java)}
}
12.4 @BindValue 快速替换单个依赖
当只需替换一个依赖时,@BindValue 比 @UninstallModules + 新 Module 更简洁:
@HiltAndroidTest
class NoteEditorViewModelTest {@get:Rulevar hiltRule = HiltAndroidRule(this)// 直接绑定 Mock 对象,覆盖 NoteRepository 的默认绑定@BindValueval noteRepository: NoteRepository = FakeNoteRepository()@Injectlateinit var viewModelFactory: ViewModelProvider.Factory@Testfun testViewModel() {// ViewModel 使用的是 FakeNoteRepository}
}
12.5 测试中的自定义 TestRunner
// 自定义 TestRunner(在 build.gradle.kts 中配置)
class HiltTestRunner : AndroidJUnitRunner() {override fun newApplication(cl: ClassLoader?,className: String?,context: Context?): Application {return super.newApplication(cl, HiltTestApplication::class.java.name, context)}
}
// build.gradle.kts
android {defaultConfig {testInstrumentationRunner = "com.example.richtext.HiltTestRunner"}
}
13. 总结
13.1 核心要点回顾
| 层级 | 职责 | Hilt 用法 | 关键文件 |
|---|---|---|---|
| app | 装配入口 | @HiltAndroidApp + @AndroidEntryPoint |
RichNoteApplication.kt, MainActivity.kt |
| core-* | 提供基础设施 | @Module + @Provides/@Binds + @Inject constructor |
各模块的 di/*.kt |
| data-* | 提供数据层 | @Module + @Provides(DB/DAO) + @Binds(Repository) |
NoteDataModule.kt, RoomNoteRepository.kt |
| feature-* | 消费依赖 | @HiltViewModel + hiltViewModel() |
各 ViewModel.kt |
13.2 Hilt 使用黄金法则
┌─────────────────────────────────────────────────────────────────┐
│ Hilt 使用黄金法则 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 法则 1:每个模块只管声明「我能提供什么」 │
│ → 模块自治,不需要汇总 │
│ │
│ 法则 2:接口绑定用 @Binds,具体构建用 @Provides │
│ → @Binds 用于接口,@Provides 用于复杂配置 │
│ │
│ 法则 3:实现类标注 @Inject constructor + @Singleton │
│ → 让 Hilt 知道如何创建 │
│ │
│ 法则 4:ViewModel 用 @HiltViewModel,Compose 用 hiltViewModel() │
│ → 配套使用,一行获取 │
│ │
│ 法则 5:编译期验证 = 运行时零惊吓 │
│ → 错误在编译时暴露,不是运行时崩溃 │
│ │
│ 法则 6:使用 @InstallIn 控制作用域 │
│ → SingletonComponent 最常用 │
│ │
└─────────────────────────────────────────────────────────────────┘
13.3 进阶学习路径
| 阶段 | 学习内容 | 推荐资源 |
|---|---|---|
| 入门 | 本文内容 + 官方 Codelab | Hilt 官方文档 |
| 进阶 | @EntryPoint、Component 定制 | Dagger 官方文档 |
| 精通 | 自定义 Scope、SubComponent | Hilt 源码分析 |
| 大师 | 编译插件开发、ASM 字节码 | Dagger 编译期源码 |
13.4 Hilt 的价值
Hilt 的价值不在于「让你少写几行代码」,而在于:
- 依赖关系显式化:所有依赖在代码中明确定义,不需要追踪
new调用链 - 依赖关系可追踪:依赖图在编译期构建完成,可以可视化查看
- 依赖关系可验证:编译期检查所有依赖是否满足,错误提前暴露
- 测试友好:轻松替换依赖为 Mock,无侵入测试
当项目膨胀到十几个模块、几十个依赖节点时,这种编译期的确定性就是工程质量的护城河。
本文对应项目:RichEditor · Hilt 2.51.1 · Kotlin 1.9.24 · Jetpack Compose
