依赖注入
这一课你将能做到
- 解释依赖注入(DI)解决什么问题,以及控制反转(IoC)的含义
- 使用 Hilt 完成 ViewModel、Repository、网络层的注入配置
- 对比 Hilt 与 Koin 的优劣,知道在项目中如何选择
为什么要学
依赖注入是「组装代码的艺术」。没有 DI,你需要手动 new 出每一层对象,传递十几层参数,改一个构造函数要改十处。DI 框架自动完成这些,让你专注业务逻辑。对于 Web 前端,DI 就像 React 的 Context + Provider,但类型安全、编译期检查。
人话:DI 就是「不要自己找对象,让框架给你介绍」。你只需要声明「我需要一个 TrackRepository」,Hilt 自动把 Retrofit、Room、OkHttp 都组装好送过来。
面试中,「为什么需要 DI」、「Hilt 的作用域」、「Koin vs Hilt」是高频考点。
先建立图像
没有 DI(手动组装):
┌────────────────────────────────────────┐
│ MainActivity │
│ ↓ new TrackRepository() │
│ ↓ new Retrofit.Builder()... │
│ ↓ new OkHttpClient() │
│ ↓ new LoggingInterceptor() │
│ ↓ ...十几层 │
│ 改一个参数,全链路修改 │
└────────────────────────────────────────┘
有 DI(Hilt 自动组装):
┌────────────────────────────────────────┐
│ @HiltAndroidApp │
│ ↓ 自动扫描 │
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │@Singleton│ │@Singleton│ │@ViewModel│ │
│ │ Retrofit │→│TrackApi │→│TrackVm │ │
│ └─────────┘ └─────────┘ └────────┘ │
│ ↑ 注入 ↑ 注入 ↑ 注入 │
│ 改配置只改一处,全链路自动生效 │
└────────────────────────────────────────┘
跟着做
步骤 1:Hilt 环境配置
build.gradle.kts(项目级):
plugins {
id("com.google.dagger.hilt.android") version "2.51.1" apply false
}
build.gradle.kts(app 模块):
plugins {
id("com.google.dagger.hilt.android")
kotlin("kapt")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.51.1")
kapt("com.google.dagger:hilt-android-compiler:2.51.1")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
}
AndroidManifest.xml:
<application
android:name=".CodeRoadMapApp"
...>
Application 类:
@HiltAndroidApp
class CodeRoadMapApp : Application()
步骤 2:网络层 Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.coderoadmap.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideTrackApi(retrofit: Retrofit): TrackApiService {
return retrofit.create(TrackApiService::class.java)
}
}
步骤 3:数据库层 Module
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"coderoadmap.db"
).build()
}
@Provides
fun provideTrackDao(database: AppDatabase): TrackDao {
return database.trackDao()
}
}
步骤 4:Repository 注入
@Singleton
class TrackRepository @Inject constructor(
private val api: TrackApiService,
private val dao: TrackDao,
private val progressStore: ProgressDataStore
) {
// 与上一课相同,构造函数参数由 Hilt 自动注入
}
步骤 5:ViewModel 与 Compose 注入
@HiltViewModel
class TrackListViewModel @Inject constructor(
private val repository: TrackRepository
) : ViewModel() {
// ...
}
// Compose 中使用
@Composable
fun TrackListScreen(
viewModel: TrackListViewModel = hiltViewModel()
) {
// ...
}
Activity 需要 @AndroidEntryPoint:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CodeRoadMapTheme {
TrackListScreen()
}
}
}
}
步骤 6:Qualifier 区分同类型
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptor
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LoggingInterceptor
@Module
@InstallIn(SingletonComponent::class)
object InterceptorModule {
@AuthInterceptor
@Provides
fun provideAuthInterceptor(): Interceptor {
return Interceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer token")
.build()
chain.proceed(request)
}
}
@LoggingInterceptor
@Provides
fun provideLoggingInterceptor(): Interceptor {
return HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
}
}
// 使用
@Provides
fun provideOkHttpClient(
@AuthInterceptor auth: Interceptor,
@LoggingInterceptor logging: Interceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(auth)
.addInterceptor(logging)
.build()
}
步骤 7:Koin 快速对比(了解)
Koin 是纯 Kotlin 的轻量 DI,无注解处理器:
// 定义模块
val appModule = module {
single { OkHttpClient.Builder().build() }
single { Retrofit.Builder().client(get()).build() }
single { get<Retrofit>().create(TrackApiService::class.java) }
single { TrackRepository(get(), get(), get()) }
viewModel { TrackListViewModel(get()) }
}
// Application 中启动
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(appModule)
}
}
}
// Compose 中使用
@Composable
fun TrackListScreen(
viewModel: TrackListViewModel = koinViewModel()
) { }
讲清楚要点
-
DI 的核心是控制反转:对象不自己创建依赖,而是被动接收。这降低了耦合,提高了可测试性(可以注入 Mock 实现)。
-
Hilt 的作用域决定生命周期:@Singleton 全应用单例;@ActivityScoped 与 Activity 同生共死;@ViewModelScoped 与 ViewModel 绑定。选错作用域会导致内存泄漏或数据不同步。
-
Hilt 是编译期,Koin 是运行时:Hilt 用注解处理器生成代码,编译期报错,性能更好;Koin 用 DSL 运行时解析,启动稍慢但写法更简洁。大型项目推荐 Hilt,小型项目或快速原型可用 Koin。
-
@Binds vs @Provides:@Binds 用于接口绑定实现(更简洁,生成代码更少);@Provides 用于需要自定义创建逻辑的场景(如 Retrofit.Builder)。
-
AssistedInject 处理运行时参数:当 ViewModel 需要运行时参数(如 trackSlug)时,用 @AssistedInject 或 SavedStateHandle。
你可能会懵的地方
-
为什么 Hilt 需要 kapt?
Hilt 基于 Dagger,Dagger 用注解处理器生成工厂代码。kapt 是 Kotlin 的注解处理工具,把 Kotlin 注解转成 Java 能理解的格式。
-
@HiltViewModel 和 @AndroidEntryPoint 什么关系?
@HiltViewModel 标记 ViewModel 可被注入;@AndroidEntryPoint 标记 Android 组件(Activity/Fragment)可接收注入。两者配合完成 hiltViewModel() 的解析。
-
单元测试怎么替换依赖?
Hilt 提供 @TestInstallIn 替换 Module;或者不用 Hilt,直接手动 new ViewModel 传入 Mock Repository。后者更简单,推荐用于纯逻辑测试。
-
Koin 真的比 Hilt 差吗?
不是「差」,是「不同」。Koin 无 kapt、无生成代码、增量编译快,适合 Kotlin Multiplatform。Hilt 生态更成熟,与 Navigation、WorkManager 集成更好。
-
DI 会导致运行时性能下降吗?
Hilt 是编译期生成,运行时零反射,性能与手写 new 无异。Koin 有少量运行时开销,但对 App 启动时间影响通常在 10ms 以内。
错法对照
| 错法 | 问题 | 正解 |
|---|
在 @Provides 里做耗时操作 | 阻塞主线程,启动慢 | 只创建对象,不执行逻辑 |
滥用 @Singleton | 内存常驻,无法释放 | 按实际生命周期选作用域 |
忘加 @AndroidEntryPoint | 运行时崩溃 IllegalStateException | Activity/Fragment/Service 必须加 |
在 @Binds 方法里写逻辑 | @Binds 必须是抽象方法 | 需要逻辑时用 @Provides |
用 lateinit 注入后手动赋值 | 破坏 DI 语义,易出错 | 让框架管理,不要手动干预 |
卡点急救
-
编译报错「 cannot be provided without an @Inject constructor」:检查类是否有 @Inject 构造函数,或是否忘记在 Module 中提供该类型。
-
注入的 Repository 是 null:通常是 kapt 没跑成功。执行 Build → Clean Project → Rebuild Project,或检查 build.gradle 中 kapt 插件顺序。
-
多个 Module 提供同类型冲突:用 @Qualifier 区分,或检查是否误加了多个 @Provides 方法返回同类型。
面试常问
-
为什么需要依赖注入?
- 解耦:类不依赖具体实现,只依赖接口;2. 可测试:测试时注入 Mock;3. 可维护:修改实现不需要改调用方;4. 生命周期管理:框架自动处理对象创建和销毁。
-
Hilt 的 @Singleton 和 @ActivityScoped 区别?
@Singleton 全应用唯一,进程存活期间一直存在;@ActivityScoped 与 Activity 生命周期绑定,Activity 销毁时释放。用错会导致内存泄漏(如 @Singleton 持有 Activity Context)。
-
Dagger、Hilt、Koin 的关系?
Dagger 是 Google 的编译期 DI 框架,功能强但配置复杂。Hilt 是 Dagger 的 Android 封装,预定义组件和作用域,简化配置。Koin 是轻量级运行时 DI,纯 Kotlin,无注解处理,适合小型项目和 KMP。
自测三问
- 解释
@Provides 和 @Binds 的使用场景差异。
- 为什么
@HiltViewModel 需要配合 hiltViewModel() 或 viewModel() 使用?
- 设计一个场景:需要两个不同配置的 Retrofit 实例,如何实现?
小结一句话
依赖注入是「让框架组装对象」的艺术;Hilt 用编译期安全换取企业级可靠性,Koin 用简洁换取开发效率;选对工具,让架构真正落地。
延伸