22、依赖注入:Hilt集成Compose、ViewModel注入、Repository模式
依赖注入,说白了就是「我要什么,你给我什么」,而不是我自己去 new。在 Android 开发里,这东西几乎是大型项目的标配。我刚开始接触时也觉得麻烦,不就是 new 个对象嘛,至于搞这么复杂?直到项目膨胀到几十个类互相依赖,改一个构造参数要全局搜索替换……嗯,那感觉,谁用谁知道。
Jetpack Compose 出来之后,依赖注入的方式也变了。以前我们靠 Dagger 的 Activity/Fragment 注入,现在有了 Hilt 和 Compose 的深度集成。今天我们就来聊聊,怎么在 Compose 项目里优雅地玩转 Hilt。
为什么是 Hilt?
Hilt 是 Google 基于 Dagger 封装的依赖注入框架。它简化了 Dagger 的配置,提供了标准化的组件和作用域。我个人习惯用 Hilt 而不是原生 Dagger,原因很简单:少写很多模板代码。
你想想看,Dagger 里你要自己定义 Component、Subcomponent、Module……Hilt 直接给你封装好了。你只需要加几个注解,就能跑起来。
- 标准化组件:Application、Activity、Fragment、ViewModel 都有对应的 Hilt 组件
- 自动生成 Dagger 配置:不需要手写 Component
- 与 Jetpack 深度集成:ViewModel、Navigation、Compose 都有官方支持
Hilt 的基本配置
首先,在项目级 build.gradle 里添加 Hilt 插件:
// 项目级 build.gradle
plugins {
id 'com.google.dagger.hilt.android' version '2.48' apply false
}
然后在 app 模块的 build.gradle 里:
// app/build.gradle
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
// ...
}
dependencies {
implementation "com.google.dagger:hilt-android:2.48"
kapt "com.google.dagger:hilt-compiler:2.48"
// Compose 集成
implementation "androidx.hilt:hilt-navigation-compose:1.1.0"
}
最后,在 Application 类上加上 @HiltAndroidApp:
@HiltAndroidApp
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化工作
}
}
嗯,这里要注意:@HiltAndroidApp 会触发 Hilt 的代码生成。如果你看到编译报错,先检查一下这个注解有没有加。
ViewModel 注入:最常用的场景
在 Compose 里,我们通常用 ViewModel 管理 UI 状态。Hilt 可以帮我们自动注入 ViewModel 的依赖。
先定义一个 ViewModel,用 @HiltViewModel 注解:
@HiltViewModel
class MainViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
val user = userRepository.getUser(userId)
_uiState.update { it.copy(user = user) }
}
}
}
data class MainUiState(
val user: User? = null,
val isLoading: Boolean = false
)
然后在 Compose 里获取这个 ViewModel:
@Composable
fun MainScreen(
userId: String,
viewModel: MainViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
// 使用 uiState 渲染 UI
}
这里有个细节:hiltViewModel() 是 Compose 专用的函数,它会自动从 Hilt 的 ViewModelComponent 里获取实例。不需要手动传参,Hilt 会帮你搞定所有依赖。
hiltViewModel() 报错,检查一下是否导入了 androidx.hilt:hilt-navigation-compose 这个库。我踩过这个坑,折腾了半小时才发现是依赖没加。
Repository 模式:数据层的依赖注入
Repository 模式是 Android 架构的核心。它把数据来源(网络、本地数据库)封装起来,ViewModel 只跟 Repository 打交道。
先定义 Repository 接口和实现:
// 接口
interface UserRepository {
suspend fun getUser(userId: String): User
suspend fun saveUser(user: User)
}
// 实现
class UserRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao
) : UserRepository {
override suspend fun getUser(userId: String): User {
// 先从本地缓存读取
val cachedUser = userDao.getUser(userId)
if (cachedUser != null) return cachedUser
// 本地没有,从网络获取
val remoteUser = apiService.getUser(userId)
userDao.insertUser(remoteUser)
return remoteUser
}
override suspend fun saveUser(user: User) {
userDao.insertUser(user)
apiService.updateUser(user)
}
}
然后通过 Hilt Module 告诉 Hilt 怎么提供这些依赖:
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideApiService(): ApiService {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
@Provides
@Singleton
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
@Singleton
fun provideUserRepository(
apiService: ApiService,
userDao: UserDao
): UserRepository {
return UserRepositoryImpl(apiService, userDao)
}
}
这里我用了 @Singleton 作用域,表示这些对象在整个应用生命周期内只有一个实例。对于 ApiService 和 Database 来说,这是合理的。
@Singleton。如果一个对象只在一个屏幕里使用,用 @ViewModelScoped 或 @ActivityScoped 更合适。我曾经把一个 ViewModel 级别的 Repository 标记为 Singleton,结果导致内存泄漏——因为 Repository 持有 Activity 的 Context,而 Activity 被销毁后 Context 还在。
Compose 中的依赖注入流程
我把整个流程画了一张图,方便你理解:
这张图展示了完整的依赖链:Hilt 容器 → Module → Repository → ViewModel → Compose。每一层都通过注解自动连接,不需要手动 new 对象。
作用域管理:别让对象活太久
Hilt 提供了几种作用域,控制对象的生命周期:
| 作用域 | 生命周期 | 适用场景 |
|---|---|---|
@Singleton |
整个应用 | ApiService、Database、SharedPreferences |
@ViewModelScoped |
ViewModel 生命周期 | Repository(如果只在一个 ViewModel 里用) |
@ActivityScoped |
Activity 生命周期 | Activity 级别的依赖 |
@FragmentScoped |
Fragment 生命周期 | Fragment 级别的依赖(Compose 里用得少) |
我个人建议:能用 ViewModelScoped 就别用 Singleton。Singleton 的对象会一直活着,直到应用被杀死。如果它持有 Context 或 Activity 引用,很容易造成内存泄漏。
实战:一个完整的例子
我们来写一个完整的用户信息展示页面。从网络获取数据,缓存到本地,展示在 Compose 里。
1. 数据层
// ApiService.kt
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: String): User
}
// UserDao.kt
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUser(userId: String): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
}
// UserRepository.kt
interface UserRepository {
suspend fun getUser(userId: String): User
}
class UserRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao
) : UserRepository {
override suspend fun getUser(userId: String): User {
val cached = userDao.getUser(userId)
if (cached != null) return cached
val remote = apiService.getUser(userId)
userDao.insertUser(remote)
return remote
}
}
2. ViewModel 层
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val user = userRepository.getUser(userId)
_uiState.update { it.copy(user = user, isLoading = false) }
} catch (e: Exception) {
_uiState.update { it.copy(error = e.message, isLoading = false) }
}
}
}
}
data class UserUiState(
val user: User? = null,
val isLoading: Boolean = false,
val error: String? = null
)
3. Compose UI 层
@Composable
fun UserScreen(
userId: String,
viewModel: UserViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
when {
uiState.isLoading -> CircularProgressIndicator()
uiState.error != null -> Text("Error: ${uiState.error}")
uiState.user != null -> UserCard(user = uiState.user!!)
}
}
@Composable
fun UserCard(user: User) {
Card(
modifier = Modifier.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = user.name, style = MaterialTheme.typography.h5)
Text(text = user.email, style = MaterialTheme.typography.body1)
Text(text = user.phone, style = MaterialTheme.typography.body2)
}
}
}
你看,整个流程下来,没有一行代码是手动 new 对象的。Hilt 负责创建 Repository,Repository 负责管理数据来源,ViewModel 负责处理业务逻辑,Compose 只负责渲染。各层职责清晰,测试也方便——你只需要 mock Repository 接口就行。
hiltViewModel() 时传了参数,结果发现每次重组都会创建新的 ViewModel。后来才明白,hiltViewModel() 默认使用 SavedStateHandle 来保持状态,不需要手动传参。如果你需要传参,用 ViewModelProvider.Factory 或者 Navigation 的 navBackStackEntry 来获取。
总结
Hilt 集成 Compose 的核心就三点:
- @HiltViewModel 注解 ViewModel,让 Hilt 管理它的依赖
- @Module + @Provides 告诉 Hilt 如何创建 Repository、ApiService 等对象
- hiltViewModel() 在 Compose 里获取 ViewModel,不需要手动传参
说白了,Hilt 就是帮你把「谁依赖谁」这件事理清楚。项目小的时候你可能觉得它多余,但项目一旦膨胀到几十个类,你就会发现——嗯,真香。
公众号:蓝海资料掘金营,微信deep3321