Android Hilt 依赖注入最佳实践:从入门到架构

依赖注入(Dependency Injection)是现代 Android 架构的基石。从手写工厂到 Dagger 2 的编译期注入,再到 Hilt 的简化封装——这条路的终点是让开发者用最少的样板代码获得最可靠的依赖管理。Hilt 不是 Dagger 的替代品,而是站在 Dagger 肩膀上的「标准答案」,它把 Android 团队多年积累的最佳实践固化成了注解和约定。

本文从 DI 基础概念出发,逐步深入 Hilt 的每个核心机制,最终覆盖多模块架构、作用域管理、第三方库集成和测试策略。

一、依赖注入基础与 Hilt 架构

什么是依赖注入

一个类需要另一个类来完成工作,这种关系就是「依赖」。如果类 A 自己创建类 B 的实例,A 就和B的具体实现耦合了。依赖注入的核心思想是:不要自己创建依赖,让外部提供

// ❌ 没有 DI:直接创建依赖,紧耦合
class OrderViewModel {
    private val repository = OrderRepository(ApiService())  // 硬编码
}

// ✅ 有 DI:依赖通过构造函数注入
class OrderViewModel(
    private val repository: OrderRepository  // 外部提供
)

DI 的三种形式

形式 方式 Hilt 是否使用
构造函数注入 通过构造函数参数传入依赖 ✅ 主要方式
字段注入 通过 @Inject 标注字段,框架自动赋值 ✅ Android 入口点专用
方法注入 通过方法参数传入依赖 ❌ 极少使用

Hilt 组件层级树

Hilt 预定义了一组与 Android 生命周期对应的组件(Component),它们形成一棵层级树——子组件可以访问父组件的依赖:

SingletonComponent(Application 级别)
├── ActivityRetainedComponent(跨配置切换的 Activity 级别)
│   └── ViewModelComponent(ViewModel 级别)
├── ServiceComponent(Service 级别)
└── ActivityComponent(Activity 级别)
    └── FragmentComponent(Fragment 级别)
        └── ViewComponent(View 级别)
    └── ViewWithFragmentComponent(带 Fragment 的 View)
核心规则:子组件可以访问父组件中绑定的依赖,反之不行。例如 Fragment 可以访问 Activity 作用域的依赖,但 Activity 不能访问 Fragment 作用域的依赖。

二、核心注解速览

@HiltAndroidApp

标注 Application 子类,触发 Hilt 代码生成。这是 Hilt 的入口,每个项目必须且只能有一个

@HiltAndroidApp
class MyApplication : Application() {
    // Hilt 会自动生成 SingletonComponent
    // 所有 @Singleton 作用域的依赖都在这里初始化
}
别忘了在 AndroidManifest.xml 中注册
<application
    android:name=".MyApplication"
    ...>

@AndroidEntryPoint

标注支持注入的 Android 类。Hilt 会为这些类生成注入代码。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var repository: UserRepository  // 字段注入

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 此时 repository 已经被注入,可直接使用
        val user = repository.getCurrentUser()
    }
}

支持的 Android 类:

Android 类 对应组件 注入方式
Activity ActivityComponent 字段注入
Fragment FragmentComponent 字段注入
View ViewComponent 字段注入
Service ServiceComponent 字段注入
BroadcastReceiver ActivityComponent 字段注入

@Inject

两种用法:

// 1. 构造函数注入:告诉 Hilt 如何创建这个类的实例
class UserRepository @Inject constructor(
    private val apiService: ApiService,
    private val dao: UserDao
) {
    // Hilt 会自动解析 ApiService 和 UserDao 的依赖
}

// 2. 字段注入:在 @AndroidEntryPoint 标注的类中使用
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var repository: UserRepository
}
字段注入的限制@Inject 标注的字段必须是 lateinit var,不能是 private。Hilt 需要直接赋值。

@Module 和 @InstallIn

当类的构造函数不由你控制(第三方库、接口、抽象类),就需要用 Module 告诉 Hilt 如何提供实例。

@Module
@InstallIn(SingletonComponent::class)  // 安装到 Application 级别
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

@InstallIn 决定了 Module 的生命周期和可见范围:

@InstallIn 参数 生命周期 典型用途
SingletonComponent::class Application 存活期间 Retrofit、Room、OkHttpClient
ActivityRetainedComponent::class Activity 跨配置切换 跨旋转保持的数据
ViewModelComponent::class ViewModel 存活期间 ViewModel 专属依赖
ActivityComponent::class Activity 存活期间 Activity 级别的 Presenter
FragmentComponent::class Fragment 存活期间 Fragment 级别的 Adapter
ViewComponent::class View 存活期间 自定义 View 的依赖
ServiceComponent::class Service 存活期间 Service 的 Worker

三、@Binds vs @Provides、@Qualifier 自定义限定符

@Binds:接口绑定到实现

当你有一个接口和多个实现时,用 @Binds 告诉 Hilt 绑定哪一个。

interface Logger {
    fun log(message: String)
}

class FileLogger @Inject constructor(
    private val fileWriter: FileWriter
) : Logger {
    override fun log(message: String) { fileWriter.write(message) }
}

@Module
@InstallIn(SingletonComponent::class)
abstract class LoggerModule {

    @Binds
    @Singleton
    abstract fun bindLogger(impl: FileLogger): Logger
    // 注意:@Binds 方法必须是 abstract,参数是实现类,返回值是接口
}

@Provides:手动提供实例

当你需要构造逻辑(Builder 模式、工厂方法、第三方库对象)时,用 @Provides

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun provideRoomDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "app-database"
        ).build()
    }

    @Provides
    fun provideUserDao(db: AppDatabase): UserDao = db.userDao()
}

@Binds vs @Provides 对比

特性 @Binds @Provides
方法类型 abstract concrete(可写在 object class)
Module 类型 abstract class objectclass
适用场景 接口 → 实现绑定 需要构造逻辑的对象
性能 略优(编译期直接映射) 运行时调用方法
能否访问其他依赖 ❌ 不能 ✅ 可以通过参数注入
混合使用:一个 Module 可以同时包含 @Binds@Provides,只需将 Module 声明为 abstract class@Provides 方法用伴生对象标注:
@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {

    @Binds
    abstract fun bindRepository(impl: DefaultRepository): Repository

    companion object {
        @Provides
        fun provideApiKey(): String = BuildConfig.API_KEY
    }
}

@Qualifier:同一类型多个绑定

当同一类型需要提供不同实例时(比如两个不同的 Retrofit、两个不同的 String),用 @Qualifier 区分。

// 1. 定义限定符注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthRetrofit

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApiRetrofit

// 2. 在 @Provides 中标注
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @AuthRetrofit
    @Provides
    @Singleton
    fun provideAuthRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl("https://auth.example.com")
        .build()

    @ApiRetrofit
    @Provides
    @Singleton
    fun provideApiRetrofit(): Retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com")
        .build()
}

// 3. 注入时指定限定符
class AuthRepository @Inject constructor(
    @AuthRetrofit private val retrofit: Retrofit
)
预定义限定符:Hilt 提供了 @ApplicationContext@ActivityContext 来区分两种 Context,不需要自己定义:
@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    fun provideCacheDir(@ApplicationContext context: Context): File {
        return context.cacheDir
    }
}

四、ViewModel 注入

@HiltViewModel 基本用法

Hilt 通过 @HiltViewModel 让 ViewModel 的构造函数注入变得极为简洁:

@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    val userId: String? = savedStateHandle["userId"]

    val userInfo: StateFlow<UiState<User>> = repository.getUser(userId)
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState.Loading)
}
// Activity / Fragment 中获取
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {

    private val viewModel: UserViewModel by viewModels()
    // Hilt 自动处理 ViewModelProvider.Factory
}

SavedStateHandle 的用途

SavedStateHandle 是 ViewModel 的「安全箱」,用于保存进程被系统回收后需要恢复的数据:

@HiltViewModel
class DetailViewModel @Inject constructor(
    private val repository: ItemRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    // 从 Navigation 参数中获取 itemId
    private val itemId: String = savedStateHandle["itemId"]!!

    // 保存临时编辑状态
    fun saveDraft(draft: String) {
        savedStateHandle["draft"] = draft
    }

    fun getDraft(): String? {
        return savedStateHandle["draft"]
    }
}

Fragment 中共享 ViewModel

@AndroidEntryPoint
class ListFragment : Fragment() {

    // 各 Fragment 独立的 ViewModel
    private val viewModel: ListViewModel by viewModels()

    // 与 Activity 共享的 ViewModel
    private val sharedViewModel: SharedViewModel by activityViewModels()
}

@AndroidEntryPoint
class DetailFragment : Fragment() {

    // 同一 Activity 下的 Fragment 共享同一个 SharedViewModel
    private val sharedViewModel: SharedViewModel by activityViewModels()
}
HiltViewModel 的优势:不需要手动创建 ViewModelProvider.Factory,构造函数中的所有依赖 Hilt 自动解析。SavedStateHandle 也由 Hilt 自动注入,不需要额外配置。

@HiltNavGraphViewModel:Navigation 场景下的 ViewModel

在 Navigation Graph 中,ViewModel 的作用域默认是 NavGraph 级别的。Hilt 提供了 @HiltNavGraphViewModel 来配合 Navigation 场景:

// 1. 在 NavGraph 中定义目标
@HiltNavGraphViewModel
class OrderViewModel @Inject constructor(
    private val orderRepository: OrderRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    private val orderId: String? = savedStateHandle["orderId"]
    // ...
}

Navigation 参数传递

// 1. 定义 NavGraph 路由(带参数)
composable(
    route = "detail/{itemId}",
    arguments = listOf(navArgument("itemId") { type = NavType.StringType })
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getString("itemId")
    // itemId 会自动通过 SavedStateHandle 注入到 ViewModel
}

// 2. ViewModel 中通过 SavedStateHandle 获取
@HiltViewModel
class DetailViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    val itemId: String = savedStateHandle["itemId"] ?: ""
}

// 3. 导航时传参
navController.navigate("detail/${item.id}")

NavGraph 作用域的 ViewModel

// 在 Navigation Graph 范围内共享 ViewModel
// 适合多页面共享状态的场景(如多步注册流程)
@HiltViewModel
class CheckoutViewModel @Inject constructor(
    private val cartRepository: CartRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    // 购物车状态在 checkout 子图中共享
}

// Fragment 中获取 NavGraph 作用域的 ViewModel
val checkoutViewModel: CheckoutViewModel by hiltNavGraphViewModels(R.id.checkout_nav_graph)
ViewModel 作用域选择
  • by viewModels() — 当前 Fragment 独享
  • by activityViewModels() — 同一 Activity 下所有 Fragment 共享
  • by hiltNavGraphViewModels(R.id.nav_graph) — 同一 NavGraph 下共享

六、多模块项目中的 Hilt

模块划分原则

app/
├── app/                    # 主模块(Application 入口)
├── core/                   # 核心模块(网络、数据库、工具类)
│   ├── core-network/
│   ├── core-database/
│   └── core-common/
└── feature/                # 功能模块
    ├── feature-auth/
    ├── feature-home/
    └── feature-profile/

各模块的 Module 安装策略

// core-network 模块
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides @Singleton
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit = /* ... */
}

// core-database 模块
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides @Singleton
    fun provideAppDatabase(@ApplicationContext ctx: Context): AppDatabase = /* ... */
}

// feature-auth 模块
@Module
@InstallIn(ViewModelComponent::class)
object AuthModule {
    @Provides
    fun provideAuthRepository(api: AuthApi): AuthRepository = /* ... */
}

@EntryPoint:在非 Hilt 管理的类中获取依赖

有时候你需要在 Hilt 不直接管理的类(如 ContentProvider、WorkManager Worker、第三方回调)中获取依赖。@EntryPoint 就是你的后门。

// 1. 定义 EntryPoint 接口
@EntryPoint
@InstallIn(SingletonComponent::class)
interface UserRepositoryEntryPoint {
    fun userRepository(): UserRepository
}

// 2. 在非 Hilt 管理的类中使用
class SyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        // 通过 EntryPoint 获取依赖
        val entryPoint = EntryPointAccessors.fromApplication(
            applicationContext,
            UserRepositoryEntryPoint::class.java
        )
        val repository = entryPoint.userRepository()

        repository.syncData()
        return Result.success()
    }
}

Feature 模块隔离

最佳实践:Feature 模块只暴露接口,实现类在模块内部。主 app 模块通过 @Binds 将接口绑定到具体实现。这样 feature 模块之间互不依赖,只依赖 core 模块的接口。
// feature-auth 模块暴露接口
interface AuthRepository {
    suspend fun login(email: String, password: String): Result<User>
}

// feature-auth 模块内部实现
class AuthRepositoryImpl @Inject constructor(
    private val authApi: AuthApi
) : AuthRepository {
    override suspend fun login(email: String, password: String) = /* ... */
}

// feature-auth 模块内部 Module
@Module
@InstallIn(SingletonComponent::class)
abstract class AuthModule {
    @Binds @Singleton
    abstract fun bindAuthRepository(impl: AuthRepositoryImpl): AuthRepository
}

七、作用域管理

作用域注解与组件对应关系

作用域注解 对应组件 生命周期 典型用途
@Singleton SingletonComponent Application 全生命周期 Retrofit、Room、OkHttpClient
@ActivityRetainedScoped ActivityRetainedComponent Activity 跨配置切换 跨旋转保持的数据管理器
@ViewModelScoped ViewModelComponent ViewModel 存活期间 ViewModel 专属的 UseCase
@ActivityScoped ActivityComponent Activity 一次生命周期 Activity 级别的 Presenter
@FragmentScoped FragmentComponent Fragment 存活期间 Fragment 专属 Adapter
@ViewScoped ViewComponent View 存活期间 自定义 View 的依赖
@ServiceScoped ServiceComponent Service 存活期间 Service 的 Worker

作用域的本质

作用域注解的作用只有一个:同一组件实例内,标注了相同作用域的依赖只创建一次

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Singleton  // ✅ 整个 App 只创建一次
    @Provides
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    // ❌ 没有 @Singleton,每次注入都创建新实例
    @Provides
    fun provideGson(): Gson = Gson()
}
常见误解@Singleton 不等于全局单例!它的含义是「在 SingletonComponent 的生命周期内单例」。如果 @Module 安装在 ActivityComponent 中,@Singleton 不会生效(编译会报错)。作用域注解必须与 @InstallIn 的组件匹配。

作用域不匹配的编译错误

// ❌ 编译错误:@Singleton 只能用于 SingletonComponent
@Module
@InstallIn(ActivityComponent::class)
object BadModule {
    @Singleton  // 错误!应该用 @ActivityScoped
    @Provides
    fun providePresenter(): Presenter = Presenter()
}

// ✅ 正确:作用域与组件匹配
@Module
@InstallIn(ActivityComponent::class)
object GoodModule {
    @ActivityScoped
    @Provides
    fun providePresenter(): Presenter = Presenter()
}

无作用域 vs 有作用域

// 无作用域:每次请求都创建新实例
@Provides
fun provideFormatter(): DateFormatter = DateFormatter()

// 有作用域:同一组件内复用实例
@Singleton
@Provides
fun provideDatabase(@ApplicationContext ctx: Context): AppDatabase = /* ... */
什么时候该加作用域?
  • 创建成本高的对象(网络客户端、数据库)→ @Singleton
  • 需要保持状态的对象(缓存、计数器)→ 对应组件作用域
  • 无状态工具类(Gson、DateFormatter)→ 通常不需要作用域

八、第三方库集成

Retrofit

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Singleton
    @Provides
    fun provideOkHttpClient(
        @ApplicationContext context: Context,
        loggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .cache(Cache(context.cacheDir, 10 * 1024 * 1024))  // 10MB 缓存
        .connectTimeout(30, TimeUnit.SECONDS)
        .build()

    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com")
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService =
        retrofit.create(ApiService::class.java)
}

Room

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Singleton
    @Provides
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase =
        Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
            .addMigrations(MIGRATIONS)
            .build()

    @Provides
    fun provideUserDao(db: AppDatabase): UserDao = db.userDao()

    @Provides
    fun provideOrderDao(db: AppDatabase): OrderDao = db.orderDao()
}

DataStore

@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {

    @Singleton
    @Provides
    fun providePreferencesDataStore(
        @ApplicationContext context: Context
    ): DataStore<Preferences> = PreferenceDataStoreFactory.create(
        storageFile = { context.preferencesDataStoreFile("settings") }
    )

    // 如果使用 Proto DataStore
    @Singleton
    @Provides
    fun provideProtoDataStore(
        @ApplicationContext context: Context
    ): DataStore<UserPreferences> = DataStoreFactory.create(
        serializer = UserPreferencesSerializer,
        produceFile = { context.dataStoreFile("user_prefs.pb") }
    )
}

OkHttp @Interceptor 注入

拦截器有时需要访问依赖(如 Token 刷新),需要单独提供:

// 1. 定义拦截器(构造函数注入)
class AuthInterceptor @Inject constructor(
    private val tokenManager: TokenManager
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer ${tokenManager.getToken()}")
            .build()
        return chain.proceed(request)
    }
}

// 2. 在 Module 中提供 OkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Singleton
    @Provides
    fun provideOkHttpClient(
        authInterceptor: AuthInterceptor,
        loggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(authInterceptor)         // 应用拦截器
        .addNetworkInterceptor(loggingInterceptor) // 网络拦截器
        .build()
}
拦截器顺序很重要addInterceptor 添加应用拦截器(先执行),addNetworkInterceptor 添加网络拦截器(后执行)。Auth 拦截器通常放在应用拦截器层,Logging 放在网络拦截器层。

集成模板汇总

@InstallIn 作用域 提供方式
Retrofit SingletonComponent @Singleton @Provides
OkHttpClient SingletonComponent @Singleton @Provides
Room Database SingletonComponent @Singleton @Provides
Room DAO SingletonComponent 无作用域 @Provides
DataStore SingletonComponent @Singleton @Provides
Gson / Moshi SingletonComponent 可选 @Singleton @Provides
CoroutineScope SingletonComponent @Singleton @Provides

九、测试最佳实践

替换模块:@TestInstallIn

测试时,你需要用假实现替换真实依赖(如把 Retrofit API 替换为内存实现)。@TestInstallIn 是官方推荐方式:

// 生产代码中的 Module
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
    @Provides @Singleton
    fun provideUserRepository(api: UserApi): UserRepository =
        RealUserRepository(api)
}

// 测试中替换为假实现
@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [RepositoryModule::class]  // 替换目标
)
object TestRepositoryModule {
    @Provides @Singleton
    fun provideUserRepository(): UserRepository =
        FakeUserRepository()  // 不依赖网络
}

单元测试:不启动 Hilt

纯单元测试不需要 Hilt。直接构造被测类,手动传入依赖:

@Test
fun `load user returns success`() = runTest {
    // 手动构造假依赖
    val fakeRepository = FakeUserRepository()
    fakeRepository.addUser(User(id = "1", name = "Test"))

    // 直接创建 ViewModel,不走 Hilt
    val viewModel = UserViewModel(fakeRepository, SavedStateHandle())

    // 验证
    val state = viewModel.userInfo.first()
    assertTrue(state is UiState.Success)
    assertEquals("Test", (state as UiState.Success).data.name)
}
原则:单元测试尽量不用 Hilt——直接构造对象更快、更可控。Hilt 测试适合集成测试和 UI 测试。

Hilt Android 测试

@HiltAndroidTest  // 启用 Hilt 测试
@RunWith(AndroidJUnit4::class)
class UserActivityTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)  // 必须添加

    @Inject
    lateinit var repository: UserRepository  // 注入依赖

    @Before
    fun init() {
        hiltRule.inject()  // 触发注入
    }

    @Test
    fun displaysUserName() {
        // 先通过注入的 repository 设置数据
        (repository as FakeUserRepository).addUser(
            User(id = "1", name = "Alice")
        )

        // 启动 Activity
        val scenario = ActivityScenario.launch(UserActivity::class.java)

        // 验证 UI
        onView(withText("Alice")).check(matches(isDisplayed()))
    }
}

@UninstallModules:临时卸载模块

@HiltAndroidTest
@UninstallModules(NetworkModule::class)  // 临时卸载网络模块
class OfflineTest {

    @Module
    @InstallIn(SingletonComponent::class)
    object TestNetworkModule {
        @Provides @Singleton
        fun provideRetrofit(): Retrofit = throw RuntimeException("不应访问网络")
    }

    // 测试离线场景
}

测试策略对比

测试类型 使用 Hilt? 替换策略 速度
纯单元测试 ❌ 不使用 手动构造假依赖 ⚡ 快
Hilt 集成测试 @HiltAndroidTest @TestInstallIn 🐢 较慢
临时替换单个模块 @UninstallModules 在测试类内定义替代 Module 🐢 较慢
端到端测试 ✅ 完整 Hilt 只替换外部服务 🐌 最慢

十、常见陷阱与速查表

陷阱 1:忘记 @HiltAndroidApp

// ❌ 编译通过但运行时崩溃
class MyApplication : Application() {  // 缺少 @HiltAndroidApp
}

// ✅ 必须标注
@HiltAndroidApp
class MyApplication : Application() {
}

陷阱 2:在 @AndroidEntryPoint 类的父类中注入

// ❌ Hilt 不支持在父类中字段注入
open class BaseActivity : AppCompatActivity() {
    @Inject lateinit var repository: Repository  // 不会被注入!
}

@AndroidEntryPoint
class MainActivity : BaseActivity() {
    // repository 是 null
}

// ✅ 在 @AndroidEntryPoint 标注的类中注入
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var repository: Repository  // ✅ 正常注入
}
注意:如果父类也标注了 @AndroidEntryPoint,则父类的字段注入也是有效的。关键是 @AndroidEntryPoint 必须出现在字段注入所在的类或其父类上。

陷阱 3:作用域与 @InstallIn 不匹配

// ❌ 编译错误
@Module
@InstallIn(ActivityComponent::class)
object BadModule {
    @Singleton  // 错误!Singleton 只能用于 SingletonComponent
    @Provides fun provideX(): X = X()
}

// ✅ 作用域与组件匹配
@Module
@InstallIn(ActivityComponent::class)
object GoodModule {
    @ActivityScoped
    @Provides fun provideX(): X = X()
}

陷阱 4:接口没有 @Binds 却期望自动注入

// ❌ Hilt 不知道要绑定哪个实现
interface Repository { fun getData(): String }
class RealRepository @Inject constructor() : Repository { /* ... */ }

// 注入 Repository 会报错:没有绑定

// ✅ 使用 @Binds 告诉 Hilt
@Module
@InstallIn(SingletonComponent::class)
abstract class RepoModule {
    @Binds @Singleton
    abstract fun bindRepository(impl: RealRepository): Repository
}

陷阱 5:在 object class 中使用 @Binds

// ❌ @Binds 不能在 object class 中
@Module
@InstallIn(SingletonComponent::class)
object BadModule {
    @Binds  // 编译错误!@Binds 必须在 abstract 方法中
    fun bindRepo(impl: RealRepo): Repository = impl
}

// ✅ @Binds 在 abstract class 中
@Module
@InstallIn(SingletonComponent::class)
abstract class GoodModule {
    @Binds @Singleton
    abstract fun bindRepo(impl: RealRepo): Repository
}

陷阱 6:多模块中 Module 重复绑定

如果 core-networkfeature-auth 都绑定了 ApiService,编译会报重复绑定。解决方案:

// 方案1:只在 core 模块绑定,feature 模块直接使用
// core-network 模块
@Module
@InstallIn(SingletonComponent::class)
object CoreNetworkModule {
    @Provides @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService = retrofit.create()
}

// 方案2:用 @Qualifier 区分不同的 ApiService
@Qualifier
annotation class AuthApi

@Qualifier
annotation class MainApi

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @AuthApi @Provides @Singleton
    fun provideAuthApi(retrofit: Retrofit): ApiService = retrofit.create()

    @MainApi @Provides @Singleton
    fun provideMainApi(retrofit: Retrofit): ApiService = retrofit.create()
}

速查表

场景 做法 关键注解
Application 级别单例 @InstallIn(SingletonComponent::class) + @Singleton @HiltAndroidApp
Activity 中注入依赖 @AndroidEntryPoint + @Inject lateinit 字段不能 private
接口绑定实现 @Module + @Binds abstract class
第三方对象提供 @Module + @Provides object class
同一类型多个实例 自定义 @Qualifier @Qualifier + @Retention
ViewModel 注入 @HiltViewModel + @Inject constructor by viewModels()
非 Hilt 类获取依赖 @EntryPoint 接口 EntryPointAccessors
测试替换模块 @TestInstallIn(replaces = [...]) @HiltAndroidTest
单元测试 不用 Hilt,手动构造 直接传参
区分两种 Context @ApplicationContext / @ActivityContext 预定义限定符
Navigation 参数传递 SavedStateHandle 自动注入 @HiltViewModel
一句话总结:Hilt 的核心公式 = @HiltAndroidApp(入口)+ @AndroidEntryPoint(注入点)+ @Module/@InstallIn(绑定规则)+ @Scope(生命周期)。记住这条链路,其他都是衍生。