/ Android  Hilt  依赖注入  Dagger  ViewModel  Repository  Clean Architecture  单元测试 

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

掌握这套模式后,你会发现代码可测试性、可维护性都有质的提升。依赖注入不只是框架的使用,更是一种面向接口编程的思维方式。

发布评论

热门评论区: