Hilt 从入门到精通:Android 依赖注入实战全指南
Hilt 从入门到精通:Android 依赖注入实战全指南
前言
依赖注入(Dependency Injection,简称 DI)是现代软件工程的核心设计模式之一。它的核心理念很简单:类不应该自己创建依赖,而应该让外部把依赖"注入"进来。
在 Android 开发中,手动实现 DI 需要写大量样板代码(Component、Module、Builder),这正是 Dagger 曾经的学习门槛。Hilt 是 Google 官方推荐的 Android DI 方案,它构建于 Dagger 之上,为 Android 框架类自动生成组件和预定义作用域,大幅减少了手动编写的代码量。
本文以 Android 官方文档为基准,结合实际开发场景,从零开始逐步深入到高级用法:安装配置、核心注解、模块与作用域、ViewModel 集成、Compose 适配、多模块项目、单元测试与插桩测试等。
第一部分:入门篇
1.1 什么是依赖注入
先看一个不使用 DI 的例子:
class UserRepository {
private val localDataSource = LocalDataSource() // 自己创建
private val remoteDataSource = RemoteDataSource() // 自己创建
private val service = Retrofit.Builder() // 自己创建
.baseUrl("https://api.example.com/")
.build()
.create(ApiService::class.java)
}
问题很明显:
UserRepository紧耦合了具体的实现类,难以替换(比如测试时想用 Mock 数据)- 创建依赖的逻辑散落在各处,重复且难以维护
- 无法方便地共享单例实例
使用依赖注入后:
class UserRepository @Inject constructor(
private val localDataSource: LocalDataSource,
private val remoteDataSource: RemoteDataSource,
private val service: ApiService
) {
// 不关心依赖怎么创建,只管使用
}
UserRepository 只声明自己需要什么,具体的创建和注入由 DI 框架(Hilt)在编译期自动生成代码来完成。
1.2 Hilt vs Dagger vs 手动 DI
| 方案 | 优点 | 缺点 |
|---|---|---|
| 手动 DI | 无依赖,完全可控 | 样板代码多,容易出错,难维护 |
| Dagger | 编译期校验,性能优秀,功能全面 | 学习曲线陡峭,Android 需要大量模板 |
| Hilt | 基于 Dagger,预定义 Android 组件,开箱即用 | 只能用于 Android,灵活性略低于纯 Dagger |
官方推荐:Google 官方文档明确表示,新项目应优先使用 Hilt。Dagger 与 Hilt 代码可以共存,旧项目可以逐步迁移。
1.3 安装与 Gradle 配置
项目根目录 build.gradle.kts:
plugins {
id("com.google.dagger.hilt.android") version "2.57.1" apply false
}
应用模块 app/build.gradle.kts:
plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.57.1")
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
}
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
注意:
- Hilt 使用 KSP(Kotlin Symbol Processing)作为注解处理器,比旧的 KAPT 更快
- 如果你的项目仍使用 Groovy 构建脚本,
ksp要改为kapt,依赖中的ksp改为kapt- 需要 Java 17 兼容
1.4 Application 入口
Hilt 必须在 Application 类上添加 @HiltAndroidApp 注解,这会触发 Hilt 的代码生成,创建应用级别的依赖容器(根组件):
@HiltAndroidApp
class MyApplication : Application() {
// 不需要写任何代码
}
这个注解做了两件事:
- 生成一个名为
Hilt_MyApplication的基类,包含 DI 容器 - 创建
SingletonComponent(单例组件),作为所有其他组件的父组件
1.5 注入到 Activity
在 Activity(或 Fragment、Service 等)上使用 @AndroidEntryPoint 注解,Hilt 会自动生成对应的 Hilt 基类并启用字段注入:
@AndroidEntryPoint
class LoginActivity : ComponentActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// analytics 在 super.onCreate() 之后就可以用了
analytics.trackScreenView("LoginScreen")
}
}
重要限制:
- 注入字段不能是
private- Activity 必须继承
ComponentActivity(或AppCompatActivity等子类)- 注入的字段在
super.onCreate()之后才可用
第二部分:依赖提供方式
2.1 构造函数注入(@Inject)—— 首选方式
对于你自己控制的类,最推荐的方式是在构造函数上加 @Inject:
class AnalyticsService @Inject constructor() {
fun trackEvent(name: String) { /* ... */ }
}
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) {
fun trackScreenView(screen: String) {
service.trackEvent("view_$screen")
}
}
Hilt 会自动解析构造函数参数,递归地构建整个依赖链。你只需要写一个 @Inject 注解,不需要任何 Module。
规则:一个类只能有一个
@Inject构造函数(通常是主构造函数)。
2.2 Hilt 模块(@Module & @InstallIn)
当遇到以下场景时,构造函数注入不够用:
- 接口:不能实例化接口
- 第三方库:无法给别人的代码加
@Inject - Builder 模式:需要复杂配置才能创建(如 Retrofit、OkHttp)
- 需要自定义创建逻辑:比如从配置文件读取参数
这时需要使用 Hilt 模块:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
@Module:标记这是一个 Hilt 模块@InstallIn(SingletonComponent::class):告诉 Hilt 这个模块安装在哪个组件上@Provides:标记提供依赖的方法,方法返回值就是提供的类型
2.3 @Binds —— 接口绑定
当一个接口有多个实现类时,使用 @Binds 更简洁(需要在抽象类中声明):
interface AnalyticsService {
fun trackEvent(name: String)
}
class FirebaseAnalyticsService @Inject constructor() : AnalyticsService {
override fun trackEvent(name: String) { /* Firebase 实现 */ }
}
class LocalAnalyticsService @Inject constructor() : AnalyticsService {
override fun trackEvent(name: String) { /* 本地日志实现 */ }
}
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Singleton
@Binds
abstract fun bindAnalyticsService(
impl: FirebaseAnalyticsService
): AnalyticsService
}
@Binds vs @Provides 的选择:
- 如果只需要返回一个已有的实例(比如构造函数注入的类),用
@Binds- 如果需要执行创建逻辑(如 Builder 模式、第三方库初始化),用
@Provides@Binds只能用在抽象方法上,所在的 Module 必须是abstract class@Provides用在普通方法上,所在的 Module 通常是object
2.4 多重绑定(Set 和 Map)
当需要向同一个类型注入多个实例时:
@Module
@InstallIn(SingletonComponent::class)
abstract class InterceptorModule {
@Singleton
@Binds
@IntoSet
abstract fun bindLoggingInterceptor(impl: LoggingInterceptor): Interceptor
@Singleton
@Binds
@IntoSet
abstract fun bindAuthInterceptor(impl: AuthInterceptor): Interceptor
}
// 注入处自动获取所有 Interceptor 的集合
class OkHttpProvider @Inject constructor(
private val interceptors: Set<@JvmSuppressWildcards Interceptor>
) {
fun provide(): OkHttpClient {
val builder = OkHttpClient.Builder()
interceptors.forEach { builder.addInterceptor(it) }
return builder.build()
}
}
Map 绑定使用 @IntoMap 配合 @StringKey、@ClassKey 等:
@Module
@InstallIn(ActivityComponent::class)
abstract class ViewModelModule {
@Binds
@IntoMap
@StringKey("login")
abstract fun bindLoginViewModel(vm: LoginViewModel): ViewModel
@Binds
@IntoMap
@StringKey("register")
abstract fun bindRegisterViewModel(vm: RegisterViewModel): ViewModel
}
2.5 限定符(@Qualifier)—— 同类型多个实例
当同一个类型需要多个不同实例(或配置)时,使用自定义限定符区分:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GeneralOkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object OkHttpModule {
@GeneralOkHttpClient
@Singleton
@Provides
fun provideGeneralOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
@AuthInterceptorOkHttpClient
@Singleton
@Provides
fun provideAuthOkHttpClient(interceptor: AuthInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
}
}
// 使用时通过限定符指定
class ApiService @Inject constructor(
@AuthInterceptorOkHttpClient private val client: OkHttpClient
)
Hilt 内置限定符:
@ApplicationContext— 注入 Application Context@ActivityContext— 注入 Activity Context(需要在 ActivityComponent 或子组件中)
class MyRepository @Inject constructor(
@ApplicationContext private val context: Context
) {
// context 是 Application Context,生命周期与 App 一致
}
第三部分:组件、作用域与生命周期
3.1 Hilt 预定义的组件
Hilt 为 Android 框架类自动生成组件,你只需要用 @InstallIn 指定即可:
| Android 类 | 生成组件 | 作用域注解 | 创建时机 | 销毁时机 |
|---|---|---|---|---|
Application |
SingletonComponent |
@Singleton |
onCreate() |
App 销毁 |
Activity(配置变更存活) |
ActivityRetainedComponent |
@ActivityRetainedScoped |
首次 onCreate() |
最终 onDestroy() |
Activity |
ActivityComponent |
@ActivityScoped |
onCreate() |
onDestroy() |
Fragment |
FragmentComponent |
@FragmentScoped |
onAttach() |
onDestroy() |
View |
ViewComponent |
@ViewScoped |
super() |
视图销毁 |
ViewModel |
ViewModelComponent |
@ViewModelScoped |
ViewModel 创建 | ViewModel 销毁 |
Service |
ServiceComponent |
@ServiceScoped |
onCreate() |
onDestroy() |
3.2 组件层级关系
SingletonComponent (Application)
├── ActivityRetainedComponent (ViewModel 宿主,跨配置变更)
│ ├── ViewModelComponent (ViewModel)
│ └── ActivityComponent (Activity)
│ └── FragmentComponent (Fragment)
│ └── ViewComponent (View)
└── ServiceComponent (Service)
子组件可以访问父组件提供的依赖,反之不行。例如 FragmentComponent 可以使用 ActivityComponent 和 SingletonComponent 提供的依赖。
3.3 作用域详解
默认情况下,每次请求依赖时 Hilt 都会创建新实例。添加作用域注解后,该实例在对应组件的生命周期内保持唯一(即局部单例):
// 整个 App 生命周期内只有一个实例
@Singleton
class AnalyticsService @Inject constructor()
// 每个 Activity 生命周期内只有一个实例
@ActivityScoped
class NavigationService @Inject constructor(
@ActivityContext private val context: Context
)
// 每个 ViewModel 生命周期内只有一个实例
@ViewModelScoped
class UserRepository @Inject constructor(
private val api: ApiService,
private val dao: UserDao
)
作用域使用建议:
- 默认不使用作用域(每次新建实例),这是最安全的
- 仅对有状态或创建成本高的对象使用作用域
- 作用域绑定的实例会持有整个组件生命周期的引用,小心内存泄漏
@ActivityRetainedScoped跨配置变更(如屏幕旋转)保持存活,适合存 ViewModel 相关的状态
3.4 ActivityRetainedComponent 的特殊性
ActivityRetainedComponent 是 Hilt 中比较特殊的组件:
- 它对应的是
ViewModel的生命周期(通过ViewModelProvider实现跨配置变更) - 它没有直接的 Android 类标记,而是由 Hilt 内部管理
- 安装在它上面的依赖会在屏幕旋转等配置变更时保留,不会重建
@Module
@InstallIn(ActivityRetainedComponent::class)
object ConfigModule {
@ActivityRetainedScoped
@Provides
fun provideUserSession(): UserSession {
// 屏幕旋转后仍保持登录状态
return UserSession()
}
}
第四部分:ViewModel 与 Compose 集成
4.1 @HiltViewModel
ViewModel 使用 Hilt 注入需要在类上添加 @HiltViewModel,构造函数使用 @Inject:
@HiltViewModel
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository,
private val analyticsService: AnalyticsService
) : ViewModel() {
private val _uiState = MutableStateFlow<LoginUiState>(LoginUiState.Loading)
val uiState: StateFlow<LoginUiState> = _uiState
fun login(username: String, password: String) {
viewModelScope.launch {
_uiState.value = LoginUiState.Loading
try {
val result = userRepository.login(username, password)
analyticsService.trackEvent("login_success")
_uiState.value = LoginUiState.Success(result)
} catch (e: Exception) {
analyticsService.trackEvent("login_failed")
_uiState.value = LoginUiState.Error(e.message ?: "登录失败")
}
}
}
}
4.2 Compose 中使用 hiltViewModel()
在 Compose 中获取 ViewModel 非常简单,只需要确保根 Activity 标记了 @AndroidEntryPoint:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
LoginScreen()
}
}
}
}
@Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is LoginUiState.Loading -> LoadingIndicator()
is LoginUiState.Success -> HomeScreen()
is LoginUiState.Error -> ErrorScreen((uiState as LoginUiState.Error).message)
}
}
关键:
hiltViewModel()会自动从 Hilt 的依赖图中获取 ViewModel- 只需要在根
ComponentActivity上标记@AndroidEntryPoint- Fragment 的宿主 Activity 不需要标记,只需 Fragment 本身标记即可
- 如果 ViewModel 有
SavedStateHandle参数,hiltViewModel()会自动注入
4.3 带 SavedStateHandle 的 ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val userId: String = savedStateHandle["userId"] ?: ""
init {
if (userId.isNotEmpty()) {
loadUser(userId)
}
}
}
// Compose 中传递参数
@Composable
fun UserScreen(userId: String) {
val viewModel: UserViewModel = hiltViewModel(
creationCallback = { factory: UserViewModel.Factory ->
factory.create(userId)
}
)
}
对于带额外参数的 ViewModel,推荐使用 @HiltViewModel + AssistedInject 的工厂模式:
@HiltViewModel(assistedFactory = UserViewModel.Factory::class)
class UserViewModel @Inject constructor(
private val repository: UserRepository,
@Assisted private val userId: String,
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(userId: String): UserViewModel
}
}
4.4 Navigation Compose 与 Hilt
使用 hiltViewModel() 与 Navigation Compose 配合时,默认按目标路由的 destination 作用域化 ViewModel:
NavHost(navController = navController, startDestination = "login") {
composable("login") {
val vm: LoginViewModel = hiltViewModel()
LoginScreen(vm)
}
composable("home") {
val vm: HomeViewModel = hiltViewModel()
HomeScreen(vm)
}
// 跨页面共享 ViewModel(按 activity 作用域)
composable("profile/{userId}") { backStackEntry ->
val parentEntry = remember {
navController.getBackStackEntry("home")
}
val sharedVm: HomeViewModel = hiltViewModel(parentEntry)
ProfileScreen(sharedVm)
}
}
第五部分:入口点(@EntryPoint)
5.1 什么是入口点
Hilt 只能自动注入到它支持的 Android 类(Activity、Fragment、Service 等)。对于 Hilt 不直接支持的类(如 ContentProvider、WorkManager Worker、自定义 View 的某些场景),需要使用入口点作为 Hilt 托管代码与未托管代码之间的桥梁。
5.2 在 ContentProvider 中使用
class ExampleContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
override fun onCreate(): Boolean {
val context = context ?: return false
val hiltEntryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
ExampleContentProviderEntryPoint::class.java
)
val analytics = hiltEntryPoint.analyticsService()
analytics.trackEvent("provider_created")
return true
}
}
关键匹配规则:
EntryPointAccessors的静态方法必须与@InstallIn中的组件级别匹配:
fromApplication()→SingletonComponentfromActivity()→ActivityComponentfromFragment()→FragmentComponent- 以此类推
5.3 在 WorkManager Worker 中使用
class SyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val repository: SyncRepository
) : Worker(appContext, workerParams) {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SyncWorkerEntryPoint {
fun syncRepository(): SyncRepository
}
override fun doWork(): Result {
val entryPoint = EntryPointAccessors.fromApplication(
applicationContext,
SyncWorkerEntryPoint::class.java
)
val repository = entryPoint.syncRepository()
return try {
repository.sync()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
5.4 在自定义 Application 基类中使用
如果你的 Application 继承了一个非 Hilt 的基类,在基类中获取依赖:
abstract class BaseActivity : AppCompatActivity() {
protected fun getAnalytics(): AnalyticsService {
val entryPoint = EntryPointAccessors.fromActivity(
this,
AnalyticsEntryPoint::class.java
)
return entryPoint.analyticsService()
}
@EntryPoint
@InstallIn(ActivityComponent::class)
interface AnalyticsEntryPoint {
fun analyticsService(): AnalyticsService
}
}
第六部分:多模块项目
6.1 多模块依赖规则
在多模块项目中,Hilt 代码生成需要遵循 Gradle 的依赖传递规则:
app
├── feature-login
│ ├── LoginModule (@Module)
│ └── LoginViewModel (@HiltViewModel)
├── feature-home
│ ├── HomeModule (@Module)
│ └── HomeViewModel (@HiltViewModel)
└── core-network
├── NetworkModule (@Module)
└── ApiService (@Inject)
规则:编译 Application 类的模块(通常是 app)必须在传递依赖中包含所有的 Hilt 模块和构造函数注入的类。
// app/build.gradle.kts
dependencies {
implementation(project(":feature-login"))
implementation(project(":feature-home"))
implementation(project(":core-network"))
}
6.2 模块的组织建议
推荐的模块组织方式:
project/
├── app/ — Application 类,入口 Activity
├── core/
│ ├── core-network/ — 网络相关 Module(Retrofit, OkHttp)
│ ├── core-database/ — 数据库相关 Module(Room)
│ └── core-common/ — 公共类型、限定符、作用域
├── feature/
│ ├── feature-login/ — 登录功能 Module(含自己的 ViewModel 和 Module)
│ └── feature-home/ — 首页功能 Module
└── data/
└── data-repository/ — Repository 层(可被多个 feature 共享)
每个 feature 模块可以有自己独立的 Hilt Module,安装在合适的组件上:
// feature-login 中
@Module
@InstallIn(ActivityComponent::class)
object LoginModule {
@Provides
fun provideLoginValidator(): LoginValidator = DefaultLoginValidator()
}
// core-network 中
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
}
6.3 功能模块间的依赖共享
如果 feature-home 需要使用 core-network 提供的 ApiService,只需要确保 feature-home 依赖 core-network:
// feature-home/build.gradle.kts
dependencies {
implementation(project(":core-network"))
}
// feature-home 中的 ViewModel 可以直接注入
@HiltViewModel
class HomeViewModel @Inject constructor(
private val api: ApiService // 来自 core-network
) : ViewModel()
第七部分:测试
7.1 插桩测试配置
Hilt 的插桩测试需要一个自定义的 Test Runner,将应用替换为 HiltTestApplication:
// androidTest/java/com/example/CustomTestRunner.kt
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
name: String?,
context: Context?
): Application {
return super.newApplication(
cl,
HiltTestApplication::class.java.name,
context
)
}
}
在 build.gradle.kts 中注册:
android {
defaultConfig {
testInstrumentationRunner = "com.example.CustomTestRunner"
}
}
7.2 UI 测试基础
所有使用 Hilt 的 UI 测试必须遵循以下配置:
@HiltAndroidTest
class SettingsScreenTest {
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<HiltTestActivity>()
@Before
fun init() {
hiltRule.inject()
}
@Test
fun settingsScreen_showsUserSettings() {
// 测试代码
}
}
注意:
@HiltAndroidTest负责为每个测试生成独立的 Hilt 组件HiltAndroidRule管理组件状态并执行注入HiltTestActivity是 Hilt 提供的空 Activity,用于 Compose 测试的宿主- 多个 Rule 时必须通过
order确保HiltAndroidRule最先执行
7.3 全局替换依赖(@TestInstallIn)
如果希望所有测试都使用假的依赖替代生产环境的绑定,使用 @TestInstallIn:
// androidTest 目录下
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [AnalyticsModule::class] // 替换生产环境模块
)
abstract class FakeAnalyticsModule {
@Singleton
@Binds
abstract fun bindAnalyticsService(
fake: FakeAnalyticsService
): AnalyticsService
}
这样在整个 androidTest 目录下,所有需要 AnalyticsService 的地方都会注入 FakeAnalyticsService。
7.4 单测试类替换(@UninstallModules)
如果只需要在单个测试类中替换依赖:
@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest {
@Module
@InstallIn(SingletonComponent::class)
abstract class TestModule {
@Singleton
@Binds
abstract fun bindAnalyticsService(
fake: FakeAnalyticsService
): AnalyticsService
}
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<HiltTestActivity>()
@Before fun init() { hiltRule.inject() }
@Test
fun settingsScreen_showsCorrectData() {
// 使用 FakeAnalyticsService 的测试
}
}
7.5 便捷绑定(@BindValue)
最简洁的测试方式——无需编写完整的测试 Module,直接用 @BindValue 将字段绑定到依赖图:
@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsScreenTest {
@BindValue @JvmField
val analyticsService: AnalyticsService = FakeAnalyticsService()
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<HiltTestActivity>()
@Before fun init() { hiltRule.inject() }
@Test
fun settingsScreen_displaysSettings() {
composeRule.setContent {
SettingsScreen()
}
composeRule.onNodeWithText("Settings").assertIsDisplayed()
}
}
三种测试方式的优先级:
@BindValue— 最简单,适合单个依赖替换@TestInstallIn— 适合全局替换(整个测试目录)@UninstallModules+ 测试 Module — 最灵活但最复杂
7.6 Robolectric 单元测试
如果使用 Robolectric,配置更加简单:
@HiltAndroidTest
@Config(application = HiltTestApplication::class)
class LoginViewModelTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@BindValue
@JvmField
val userRepository: UserRepository = FakeUserRepository()
@Before fun init() { hiltRule.inject() }
@Test
fun login_success_updatesState() = runTest {
val vm = LoginViewModel(userRepository, FakeAnalyticsService())
vm.login("test", "password")
// 断言 state
}
}
7.7 自定义 Application 基类的测试
如果你的测试 Application 需要继承非默认的基类(如 BaseApplication):
@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication
// Hilt 将生成 HiltTestApplication_Application,继承自 BaseApplication
第八部分:高级主题
8.1 Hilt 与 Retrofit + OkHttp 的完整集成
这是实际项目中最常见的组合:
// core-network 模块
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
return HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
}
@Singleton
@Provides
fun provideAuthInterceptor(): AuthInterceptor {
return AuthInterceptor()
}
@Singleton
@Provides
fun provideOkHttpClient(
logging: HttpLoggingInterceptor,
auth: AuthInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(logging)
.addInterceptor(auth)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Singleton
@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
Hilt 自动解析依赖链:ApiService 需要 Retrofit,Retrofit 需要 OkHttpClient,OkHttpClient 需要两个 Interceptor。你只需要声明 @Provides 方法,Hilt 会按照正确的顺序构建。
8.2 Hilt 与 Room 数据库集成
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Singleton
@Provides
fun provideDatabase(
@ApplicationContext context: Context
): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app-database"
).build()
}
@Singleton
@Provides
fun provideUserDao(db: AppDatabase): UserDao {
return db.userDao()
}
}
// Repository 直接注入
class UserRepository @Inject constructor(
private val userDao: UserDao,
private val api: ApiService
) {
suspend fun getUser(id: Long): User {
return userDao.getUser(id) ?: api.fetchUser(id).also {
userDao.insert(it)
}
}
}
8.3 Hilt 与 DataStore / SharedPreferences
@Module
@InstallIn(SingletonComponent::class)
object PreferencesModule {
@Singleton
@Provides
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
return context.createDataStore(name = "settings")
}
@Singleton
@Provides
fun provideUserPreferences(dataStore: DataStore<Preferences>): UserPreferences {
return UserPreferences(dataStore)
}
}
class UserPreferences @Inject constructor(
private val dataStore: DataStore<Preferences>
) {
val themeFlow = dataStore.data.map { prefs ->
prefs[PreferencesKeys.THEME] ?: "light"
}
suspend fun setTheme(theme: String) {
dataStore.edit { it[PreferencesKeys.THEME] = theme }
}
}
8.4 延迟注入(Lazy 和 Provider)
当依赖创建成本高或可能造成循环依赖时,使用 Lazy<T> 延迟初始化:
class ExpensiveOperation @Inject constructor(
private val dependency: Lazy<ExpensiveDependency>
) {
fun execute() {
// 只在需要时才创建 ExpensiveDependency
val dep = dependency.get()
dep.doWork()
}
}
使用 Provider<T> 每次获取新实例(即使有作用域绑定):
class RequestFactory @Inject constructor(
private val requestProvider: Provider<HttpRequest>
) {
fun createRequests(count: Int): List<HttpRequest> {
return List(count) { requestProvider.get() } // 每次都是新实例
}
}
8.5 多 Component 自定义扩展
如果预定义的组件不满足需求,可以自定义组件:
@DefineComponent(parent = SingletonComponent::class)
annotation class MyCustomComponent
@Module
@InstallIn(MyCustomComponent::class)
object MyCustomModule {
@Provides
fun provideCustomDependency(): CustomDependency = CustomDependency()
}
// 在代码中创建实例
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
@Inject lateinit var entryPoint: MyCustomEntryPoint
@EntryPoint
@InstallIn(MyCustomComponent::class)
interface MyCustomEntryPoint {
fun getCustomDependency(): CustomDependency
}
}
8.6 循环依赖的解决
如果 A 依赖 B,B 又依赖 A,Hilt 会在编译时报错。解决方法:
- 重构代码结构(推荐)—— 提取公共逻辑到第三个类 C
- 使用
Lazy<T>—— 延迟其中一个依赖的初始化
// 循环依赖
class A @Inject constructor(private val b: B)
class B @Inject constructor(private val a: A) // 编译错误
// 解决:使用 Lazy
class B @Inject constructor(private val a: Lazy<A>) {
fun doWork() {
a.get().doSomething() // 延迟到实际使用时
}
}
8.7 编译期代码生成原理
Hilt 在编译期通过注解处理器生成代码。对于每个被注解的类,Hilt 会生成对应的 Hilt_* 基类:
@HiltAndroidApp MyApplication → 生成 Hilt_MyApplication
@AndroidEntryPoint MainActivity → 生成 Hilt_MainActivity
@HiltViewModel LoginViewModel → 生成 Hilt_LoginViewModel
生成的代码在 build/generated/source/ksp/ 目录下。如果你遇到注入问题,查看生成的代码可以帮助理解 Hilt 是如何构建依赖图的。
第九部分:常见问题 FAQ
Q1: Hilt 和 Dagger 有什么区别?
Hilt 是 Dagger 的封装。Dagger 是一个通用的 Java/Kotlin DI 框架,而 Hilt 专门为 Android 做了预定义组件和注解,大幅减少了样板代码。底层仍然是 Dagger 在驱动。
Q2: 注入字段为什么不能是 private?
Hilt 通过字段注入(Field Injection)将依赖赋值到字段上。如果字段是 private,Hilt 生成的子类无法访问父类的私有字段。可以使用 @Inject lateinit var(Kotlin)或直接声明为非私有字段。
Q3: 什么时候用 @Module 什么时候用 @Inject 构造函数?
- 类是你自己控制的 → 首选
@Inject构造函数 - 接口、第三方库、需要 Builder 模式 → 用
@Module+@Provides或@Binds - 接口有多个实现 → 用
@Module+@Binds
Q4: Hilt 支持纯 Java 项目吗?
Hilt 专为 Android 设计,依赖于 Android 框架类的生命周期来管理组件。如果是纯 Java 后端项目,应该使用纯 Dagger 或其他 DI 框架。
Q5: @Singleton 和 @ActivityScoped 的区别?
@Singleton:整个 App 生命周期内只有一个实例,安装在SingletonComponent@ActivityScoped:每个 Activity 实例对应一个实例,安装在ActivityComponent- 作用域决定了实例的生命周期和共享范围
Q6: 如何在 Fragment 中注入?
与 Activity 完全一样,只需要在 Fragment 上添加 @AndroidEntryPoint:
@AndroidEntryPoint
class LoginFragment : Fragment() {
@Inject lateinit var viewModel: LoginViewModel
}
Fragment 会自动获取宿主 Activity 的组件,如果宿主 Activity 也是 @AndroidEntryPoint,则可以注入该 Activity 作用域内的依赖。
Q7: Hilt 会导致 App 启动变慢吗?
Hilt 的代码生成发生在编译期,不会在运行时通过反射创建对象。运行时开销极小(主要是依赖图的查找和赋值)。实际测试中,Hilt 对启动时间的影响通常不到 10ms。
Q8: 旧项目如何迁移到 Hilt?
官方推荐的迁移策略:
- 先引入 Hilt 依赖
- 在 Application 上添加
@HiltAndroidApp - 逐个 Activity 添加
@AndroidEntryPoint - 将现有的 Dagger Module 迁移到 Hilt Module(使用
@InstallIn) - Hilt 和 Dagger 可以共存,不必一次性全部迁移
附录:完整项目模板
// === 1. Application ===
@HiltAndroidApp
class MyApplication : Application()
// === 2. Network Module ===
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Singleton
@Provides
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// === 3. Database Module ===
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "app-db").build()
}
@Singleton
@Provides
fun provideUserDao(db: AppDatabase): UserDao = db.userDao()
}
// === 4. Repository ===
class UserRepository @Inject constructor(
private val userDao: UserDao,
private val api: ApiService
) {
suspend fun getUser(id: Long): User {
return userDao.getUser(id) ?: api.fetchUser(id).also { userDao.insert(it) }
}
}
// === 5. ViewModel ===
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository,
private val analytics: AnalyticsService
) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState
fun loadUser(id: Long) {
viewModelScope.launch {
try {
val user = repository.getUser(id)
analytics.trackEvent("user_loaded")
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message ?: "加载失败")
}
}
}
}
// === 6. Activity ===
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
UserScreen()
}
}
}
}
// === 7. Compose ===
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// UI 渲染...
}
// === 8. 测试 ===
@UninstallModules(NetworkModule::class)
@HiltAndroidTest
class UserRepositoryTest {
@BindValue @JvmField
val apiService: ApiService = FakeApiService()
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@Before fun init() { hiltRule.inject() }
@Test
fun getUser_returnsUserFromApiWhenNotInDb() = runTest {
// 测试代码
}
}
总结
Hilt 的核心价值在于:把 Dagger 的强大能力与 Android 开发的便捷性结合了起来。
掌握 Hilt,你只需要理解四个概念:
@HiltAndroidApp+@AndroidEntryPoint— 在 Android 框架类上启用注入@Inject构造函数 — 最推荐的依赖提供方式@Module+@InstallIn— 处理接口、第三方库、复杂创建逻辑- 作用域注解 — 控制实例的生命周期(
@Singleton、@ActivityScoped等)
从入门的接口注入到高级的自定义组件、测试替换、多模块组织,Hilt 用极简的设计覆盖了几乎所有 Android 依赖注入场景。希望这篇指南能帮助你从"会用"走向"精通"。