Android Hilt 依赖注入实战:从零搭建 Clean Architecture 注入体系
为什么要在 Android 项目中引入 Hilt?
依赖注入(Dependency Injection,DI)是现代 Android 开发中几乎无法绕开的话题。手动管理对象依赖不仅繁琐,还容易在 Activity/Fragment 生命周期中引发内存泄漏。Google 官方推荐的 Hilt 框架基于 Dagger2,通过编译期代码生成,在保持高性能的同时大幅降低了上手门槛。
本文以一个真实的“用户登录 + 数据加载”功能为线索,带你从零搞建一套符合 Clean Architecture 的 Hilt 注入体系,覆盖以下场景:
ViewModel 中注入 Repository
Repository 中注入 Retrofit 和 Room
多模块项目中的跨模块注入
单元测试中替换真实依赖
第一步:添加依赖与基础配置
在项目根目录 build.gradle 中添加 Hilt 插件:
// 根目录 build.gradle
plugins {
id 'com.google.dagger.hilt.android' version '2.51.1' apply false
}在 app 模块 build.gradle 中:
plugins {
id 'com.google.dagger.hilt.android'
id '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"
}接着给 Application 类加上 @HiltAndroidApp 注解,这是 Hilt 的入口,不能省略:
@HiltAndroidApp class MyApp : Application()
别忘了在 AndroidManifest.xml 中指定 android:name=".MyApp"。
第二步:构建 Network 和 Database 模块
Hilt 通过 @Module + @InstallIn 来声明如何提供依赖。网络层和数据库层通常作用域为整个应用生命周期,使用 SingletonComponent:
@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)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
}数据库模块同理:
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}踩坑提示:UserDao 不加 @Singleton,因为 Room 会自己管理 DAO 的生命周期,重复包装反而可能引发问题。
第三步:注入 Repository 和 ViewModel
Repository 层负责整合网络和本地数据源,使用构造函数注入:
class UserRepository @Inject constructor(
private val userApi: UserApi,
private val userDao: UserDao
) {
suspend fun login(username: String, password: String): Result<User> {
return try {
val user = userApi.login(LoginRequest(username, password))
userDao.insertUser(user)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
fun getCachedUser(): Flow<User?> = userDao.getUser()
}ViewModel 中使用 @HiltViewModel + @Inject constructor,无需手动创建 ViewModelFactory:
@HiltViewModel
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _loginState = MutableStateFlow<LoginState>(LoginState.Idle)
val loginState: StateFlow<LoginState> = _loginState.asStateFlow()
fun login(username: String, password: String) {
viewModelScope.launch {
_loginState.value = LoginState.Loading
val result = userRepository.login(username, password)
_loginState.value = if (result.isSuccess) {
LoginState.Success(result.getOrNull()!!)
} else {
LoginState.Error(result.exceptionOrNull()?.message ?: "登录失败")
}
}
}
}在 Activity/Fragment 中获取 ViewModel 只需:
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
}注意 Activity 必须加 @AndroidEntryPoint,否则 Hilt 无法注入。
第四步:单元测试中替换依赖
Hilt 对测试的支持非常完善,通过 @TestInstallIn 可以替换正式模块:
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NetworkModule::class]
)
@Module
object FakeNetworkModule {
@Provides
@Singleton
fun provideUserApi(): UserApi = FakeUserApi()
}测试类:
@HiltAndroidTest
class LoginViewModelTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var userRepository: UserRepository
@Before
fun init() {
hiltRule.inject()
}
@Test
fun testLoginSuccess() = runTest {
val viewModel = LoginViewModel(userRepository)
viewModel.login("test_user", "password123")
assertTrue(viewModel.loginState.value is LoginState.Success)
}
}常见问题与最佳实践
作用域选择:Activity 内的依赖用
ActivityComponent,避免Singleton持有 Activity 引用造成泄漏@Qualifier 区分同类型:若有多个 OkHttpClient,用
@Named或自定义 Qualifier 区分懒加载:用
Lazy<T>包装不需要立即初始化的依赖,减少启动耗时多模块项目:每个 feature 模块可以有自己的
@InstallIn(ActivityComponent::class)模块,Hilt 会自动合并kapt 编译慢?:升级到 KSP 可以显著提升编译速度,Hilt 已支持 KSP
掌握这套模式后,你会发现代码可测试性、可维护性都有质的提升。依赖注入不只是框架的使用,更是一种面向接口编程的思维方式。
发布评论
热门评论区: