在写代码时,一个类经常需要用到其他类。比如 ArticleViewModel 需要 ArticleRepository,ArticleRepository 又需要 ArticleDao 和 ApiService。
最直接的写法是在构造函数里自己创建:
class ArticleViewModel {
private val repository = ArticleRepository(
ArticleDao(database),
ApiService.create()
)
}这种做法有几个问题:
ApiService,如果各自创建,就不是同一个实例。依赖注入的思路很简单:不要让类自己创建依赖对象,而是从外部传进来。
class ArticleViewModel(
private val repository: ArticleRepository
) {
// 直接用 repository,不关心它怎么来的
}但手写依赖注入时,需要在 Application 或 Activity 里手动层层构造对象,代码量会快速膨胀。这就是 DI 框架的价值——帮你自动完成对象的创建和传递。
Android 上常见的 DI 方案有 Dagger、Koin 和 Hilt。
Hilt 的核心优势:
在项目根 build.gradle 中:
plugins {
id 'com.google.dagger.hilt.android' version '2.51' apply false
}在 app 模块 build.gradle 中:
plugins {
id 'com.android.application'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation "com.google.dagger:hilt-android:2.51"
kapt "com.google.dagger:hilt-android-compiler:2.51"
}如果项目使用 KSP 替代 kapt:
plugins {
id 'com.google.devtools.ksp'
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation "com.google.dagger:hilt-android:2.51"
ksp "com.google.dagger:hilt-android-compiler:2.51"
}Hilt 需要一个继承自 Application 的类,并标注 @HiltAndroidApp:
@HiltAndroidApp
class MyApplication : Application()别忘了在 AndroidManifest.xml 中声明:
<application
android:name=".MyApplication"
... >@HiltAndroidApp 会触发 Hilt 的代码生成,创建一个应用级别的依赖容器。这是整个 Hilt 配置里唯一需要手动改 Application 的地方。
Activity、Fragment、Service、BroadcastReceiver 需要用 @AndroidEntryPoint 标记后才能使用注入:
@AndroidEntryPoint
class ArticleActivity : AppCompatActivity() {
@Inject
lateinit var repository: ArticleRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// repository 已经可以使用了
}
}Fragment 也一样:
@AndroidEntryPoint
class ArticleFragment : Fragment() {
@Inject
lateinit var repository: ArticleRepository
}@AndroidEntryPoint 的含义是:这个 Android 组件需要 Hilt 提供依赖。Hilt 会在合适的生命周期回调中完成注入。
注意:如果 Fragment 的宿主 Activity 没有标 @AndroidEntryPoint,Fragment 的注入会报错。Hilt 的依赖关系是沿着组件层级向上传递的。
对于普通的 Kotlin 类,用 @Inject 标记构造函数即可让 Hilt 知道如何创建它:
class ArticleRepository @Inject constructor(
private val apiService: ApiService,
private val articleDao: ArticleDao
) {
suspend fun getArticles(): List<Article> {
val remote = apiService.fetchArticles()
articleDao.insertAll(remote.map { it.toEntity() })
return remote
}
}当 Hilt 需要创建 ArticleRepository 时,它会自动去找 ApiService 和 ArticleDao 的创建方式,层层解析,直到所有依赖都满足。
如果某个依赖找不到,编译时就会报错,而不是等到运行时崩溃。这是 Hilt(基于 Dagger)最实用的特性之一。
不是所有类都能用 @Inject constructor 标记。比如第三方库的类、接口实现、需要复杂初始化逻辑的对象。这时需要用 Module 来提供。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}几个关键点:
@Module 表示这是一个提供依赖的模块。@InstallIn(SingletonComponent::class) 表示这个 Module 安装在全局单例容器中,整个应用共享。@Provides 标注的方法就是创建对象的方式。@Singleton 表示全局只创建一次,所有注入的地方共享同一个实例。方法的参数就是依赖。Hilt 会自动解析:provideRetrofit 需要 OkHttpClient,Hilt 会先调用 provideOkHttpClient(),拿到结果后传给 provideRetrofit()。
如果接口只有一个实现,可以用 @Binds 代替 @Provides,代码更简洁:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindArticleRepository(
impl: ArticleRepositoryImpl
): ArticleRepository
}@Binds 只能用在抽象类和抽象方法上。它告诉 Hilt:当需要 ArticleRepository 接口时,提供 ArticleRepositoryImpl 实现。
使用 @Binds 的前提是实现类的构造函数标了 @Inject:
class ArticleRepositoryImpl @Inject constructor(
private val api: ApiService,
private val dao: ArticleDao
) : ArticleRepository {
// ...
}Room 的 Database 和 DAO 通常也用 Module 提供:
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app.db"
).build()
}
@Provides
fun provideArticleDao(database: AppDatabase): ArticleDao {
return database.articleDao()
}
}@ApplicationContext 是 Hilt 内置的 Qualifier,用来注入 Application 的 Context。如果需要 Activity 的 Context,用 @ActivityContext。
这样配置后,任何类的构造函数里只要写 private val dao: ArticleDao,Hilt 就会自动创建并注入。
ViewModel 的注入方式稍有不同。不能直接用 @Inject constructor 配合 @AndroidEntryPoint,而是要用 @HiltViewModel:
@HiltViewModel
class ArticleViewModel @Inject constructor(
private val repository: ArticleRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ArticleUiState())
val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()
fun loadArticles() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(loading = true)
try {
val articles = repository.getArticles()
_uiState.value = ArticleUiState(articles = articles)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
loading = false,
errorMessage = "加载失败"
)
}
}
}
}在 Activity 或 Fragment 中获取 ViewModel 时,不需要任何额外配置:
@AndroidEntryPoint
class ArticleActivity : AppCompatActivity() {
private val viewModel: ArticleViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.loadArticles()
}
}Compose 中使用也很自然:
@Composable
fun ArticleScreen(
viewModel: ArticleViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 根据 uiState 渲染 UI
}hiltViewModel() 来自 androidx.hilt:hilt-navigation-compose,需要额外添加依赖:
implementation "androidx.hilt:hilt-navigation-compose:1.2.0"Hilt 中不同的组件有不同的作用域:
| 作用域 | 组件 | 生命周期 | |---|---|---| | @Singleton | SingletonComponent | 整个应用 | | @ActivityScoped | ActivityComponent | Activity 存活期间 | | @FragmentScoped | FragmentComponent | Fragment 存活期间 | | @ViewModelScoped | ViewModelComponent | ViewModel 存活期间 | | @ServiceScoped | ServiceComponent | Service 存活期间 |
一般规则:
@Singleton,全局共享。@HiltViewModel,内部依赖默认跟随 ViewModel 生命周期。不要在 SingletonComponent 的 Module 中注入 Activity Context。全局单例的寿命远超 Activity,持有 Activity Context 会导致内存泄漏。
有时接口有多个实现,Hilt 不知道该注入哪一个。比如项目中有正式环境和测试环境两套 ApiService:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ProdApi
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TestApi在 Module 中标注:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
@ProdApi
fun provideProdApi(retrofit: Retrofit): ApiService {
return retrofit.create(ProdApiService::class.java)
}
@Provides
@Singleton
@TestApi
fun provideTestApi(retrofit: Retrofit): ApiService {
return retrofit.create(TestApiService::class.java)
}
}注入时指定要哪个:
class DataRepository @Inject constructor(
@ProdApi private val apiService: ApiService
)Hilt 内置了两个常用 Qualifier:@ApplicationContext 和 @ActivityContext,不需要自己定义。
整理一下典型项目中 Hilt 的目录组织:
com.example.app/
├── MyApplication.kt // @HiltAndroidApp
├── di/
│ ├── NetworkModule.kt // OkHttpClient, Retrofit, ApiService
│ ├── DatabaseModule.kt // Room Database, DAO
│ └── RepositoryModule.kt // @Binds 接口绑定
├── data/
│ ├── remote/
│ │ └── ApiService.kt
│ ├── local/
│ │ ├── AppDatabase.kt
│ │ └── ArticleDao.kt
│ └── repository/
│ ├── ArticleRepository.kt // 接口
│ └── ArticleRepositoryImpl.kt // @Inject constructor
├── ui/
│ ├── ArticleActivity.kt // @AndroidEntryPoint
│ └── ArticleViewModel.kt // @HiltViewModel每个文件只做一件事,依赖关系清晰:
NetworkModule 提供 ApiService。DatabaseModule 提供 ArticleDao。RepositoryModule 把 ArticleRepositoryImpl 绑定到 ArticleRepository 接口。ArticleViewModel 构造函数注入 ArticleRepository。ArticleActivity 通过 by viewModels() 拿到 ViewModel。整个过程没有任何手动 new 或工厂方法。Hilt 在编译期生成了所有必要的胶水代码。
Hilt 对测试支持很好。可以针对测试替换某些依赖:
@UninstallModules(NetworkModule::class)
@HiltAndroidTest
class ArticleRepositoryTest {
@Inject
lateinit var repository: ArticleRepository
@Test
fun testGetArticles() = runTest {
val articles = repository.getArticles()
assertTrue(articles.isNotEmpty())
}
}也可以用 @TestInstallIn 在测试时替换整个 Module:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NetworkModule::class]
)
object FakeNetworkModule {
@Provides
@Singleton
fun provideApiService(): ApiService {
return FakeApiService()
}
}这样测试时用的是假接口实现,不需要真实网络请求。
忘记加 @AndroidEntryPoint。 Activity 或 Fragment 里用 @Inject 之前必须先标这个注解,否则注入不会生效,变量为 null。
Module 忘记 @InstallIn。 @Module 必须配合 @InstallIn 指定安装到哪个组件,否则编译报错。
在 SingletonComponent 中使用 Activity Context。 全局单例的生命周期远超 Activity,持有 Activity Context 会泄漏。需要 Context 时用 @ApplicationContext。
ViewModel 没有用 @HiltViewModel。 直接在 ViewModel 构造函数写 @Inject 配合 by viewModels() 不行。ViewModel 必须用 @HiltViewModel 标注。
循环依赖。 A 依赖 B,B 依赖 A,Hilt 在编译期会检测到并报错。遇到时需要重新审视架构设计,通常意味着职责划分有问题。
第三方库对象忘记提供 Module。 像 Gson、Picasso、Firebase 这些第三方库的实例不能 @Inject constructor,必须通过 @Module + @Provides 提供。
kapt/ksp 增量编译问题。 偶尔遇到编译不通过但代码没问题的情况,Clean + Rebuild 通常能解决。
Hilt 的核心用法可以归纳为:
@HiltAndroidApp 放在 Application 上,启动 Hilt。@AndroidEntryPoint 放在需要注入的 Android 组件上。@Inject constructor 标记普通类的构造函数。@Module + @InstallIn + @Provides 提供不能直接构造的对象。@Binds 简洁地绑定接口和实现。@HiltViewModel 配合 by viewModels() 或 hiltViewModel() 注入 ViewModel。@Singleton 控制全局单例,其他作用域按需使用。@Qualifier 区分同类型的多个实现。依赖注入不是为了让代码"看起来高级",而是为了解耦、方便测试、统一对象管理。Hilt 把这件事的门槛降到了很低,大部分 Android 项目都可以直接用起来。
下一篇会聊 Paging 3 分页加载,它和 Hilt、ViewModel、Flow 配合后,列表页的数据加载会变得非常清晰。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。