上一篇 下一篇 回到顶部 目录 返回首页
目录

Hilt 从入门到精通:Android 依赖注入实战全指南

发表于
更新于
2 54.9~70.6 分钟 24717

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() {
    // 不需要写任何代码
}

这个注解做了两件事:

  1. 生成一个名为 Hilt_MyApplication 的基类,包含 DI 容器
  2. 创建 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 可以使用 ActivityComponentSingletonComponent 提供的依赖。

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 不直接支持的类(如 ContentProviderWorkManager 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()SingletonComponent
  • fromActivity()ActivityComponent
  • fromFragment()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()
    }
}

三种测试方式的优先级

  1. @BindValue — 最简单,适合单个依赖替换
  2. @TestInstallIn — 适合全局替换(整个测试目录)
  3. @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 需要 RetrofitRetrofit 需要 OkHttpClientOkHttpClient 需要两个 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 会在编译时报错。解决方法:

  1. 重构代码结构(推荐)—— 提取公共逻辑到第三个类 C
  2. 使用 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?

官方推荐的迁移策略:

  1. 先引入 Hilt 依赖
  2. 在 Application 上添加 @HiltAndroidApp
  3. 逐个 Activity 添加 @AndroidEntryPoint
  4. 将现有的 Dagger Module 迁移到 Hilt Module(使用 @InstallIn
  5. 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,你只需要理解四个概念:

  1. @HiltAndroidApp + @AndroidEntryPoint — 在 Android 框架类上启用注入
  2. @Inject 构造函数 — 最推荐的依赖提供方式
  3. @Module + @InstallIn — 处理接口、第三方库、复杂创建逻辑
  4. 作用域注解 — 控制实例的生命周期(@Singleton@ActivityScoped 等)

从入门的接口注入到高级的自定义组件、测试替换、多模块组织,Hilt 用极简的设计覆盖了几乎所有 Android 依赖注入场景。希望这篇指南能帮助你从"会用"走向"精通"。