4. viewModelScope 配置:在 ViewModel 中引入协程作用域与依赖注入
4.1 原理:为什么 ViewModel 需要专属协程作用域
ViewModel 持有与 Activity/Fragment 生命周期绑定的协程作用域 viewModelScope。当 ViewModel 被清除(对应 onCleared())时,作用域内所有协程自动取消,避免内存泄漏和废弃后台任务。核心原理是将协程生命周期与 ViewModel 生命周期对齐,而不再依赖 Fragment 的 lifecycleScope。
- 自动取消:ViewModel 清除时自动调用
SupervisorJob.cancel(),停止所有子协程。 - 结构化并发:通过
viewModelScope.launch启动的协程可被统一管理。 - 与 LiveData 协同:在协程中通过
liveData { emit() }构建器将异步结果发布到 LiveData。
关键点:viewModelScope 是 AndroidX lifecycle-viewmodel-ktx 提供的扩展属性,底层使用 Dispatchers.Main.immediate,且 Job 为 SupervisorJob,使子协程异常不会扩散到兄弟协程。
4.2 依赖注入配置
在实际项目中,ViewModel 常通过 Hilt / Koin / Dagger 注入 Repository 和协程相关依赖。以下以 Hilt 为例展示完整配置。
4.2.1 添加依赖(build.gradle)
// 模块级 build.gradle
dependencies {
// ViewModel + viewModelScope
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
// Hilt
implementation "com.google.dagger:hilt-android:2.48"
kapt "com.google.dagger:hilt-android-compiler:2.48"
implementation "androidx.hilt:hilt-common:1.1.0"
kapt "androidx.hilt:hilt-compiler:1.1.0"
}
4.2.2 定义 Repository 接口与实现
// UserRepository.kt
interface UserRepository {
suspend fun fetchUser(): User
}
// UserRepositoryImpl.kt
class UserRepositoryImpl @Inject constructor(
private val apiService: ApiService
) : UserRepository {
override suspend fun fetchUser(): User {
return withContext(Dispatchers.IO) {
apiService.getUser()
}
}
}
4.2.3 注入 Repository 到 ViewModel
// UserViewModel.kt
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
private val _userLiveData = MutableLiveData<User>()
val userLiveData: LiveData<User> = _userLiveData
// 使用 viewModelScope 启动协程
fun loadUser() {
viewModelScope.launch {
try {
val user = repository.fetchUser()
_userLiveData.value = user
} catch (e: Exception) {
// 错误处理
_userLiveData.value = null
}
}
}
}
4.3 实现流程:viewModelScope + LiveData 混合使用
| 步骤 | 操作 | 关键代码/API |
|---|---|---|
| 1 | 在 ViewModel 中定义 LiveData 和挂起函数 | viewModelScope.launch { } |
| 2 | 在协程内调用 Repository 的挂起方法 | repository.fetchUser() |
| 3 | 将结果设置到 LiveData | _userLiveData.value = result |
| 4 | 设置异常处理与取消感知 | try/catch + isActive 检查 |
| 5 | Activity/Fragment 观察 LiveData | viewModel.userLiveData.observe(viewLifecycleOwner) { } |
4.4 内联 SVG:viewModelScope 生命周期与依赖注入示意图
4.5 调试与常见错误
- 忘记添加 lifecycle-viewmodel-ktx 依赖:编译时找不到 viewModelScope,报错
Unresolved reference: viewModelScope。 - Hilt 注入失败:确保 ViewModel 使用
@HiltViewModel注解,并且 Application 类已添加@HiltAndroidApp。 - 协程内访问 LiveData 导致主线程阻塞:不要在
viewModelScope.launch内部直接做耗时操作,应通过withContext切换调度器。 - 内存泄漏检查:使用 Android Profiler 观察 ViewModel 被销毁后协程是否停止,可通过
viewModelScope.isActive在 onCleared() 中打印日志。
常见陷阱:如果在
viewModelScope.launch 中启动了 async 构建器,需要手动处理 await() 异常,否则未捕获的异常会导致整个 scope 取消(虽然 SupervisorJob 可避免兄弟协程受影响,但 async 内部的异常仍然会传播到顶层)。
4.6 性能与稳定性优化
| 优化项 | 做法 | 收益 |
|---|---|---|
| 限制并发数量 | 使用 Semaphore 或自定义 ThreadPoolDispatcher 控制并行协程数 |
避免 IO 线程池耗尽,减少线程切换开销 |
合理使用 liveData { } 构建器 |
替代手动设置 LiveData,自动处理取消和重订阅 | 减少模板代码,防止协程泄漏 |
| 设置超时机制 | withTimeout(5000) { } 包裹网络请求 |
防止请求挂起无限期阻塞 LiveData |
| 监控取消状态 | 在协程内定期检查 isActive,提前退出 |
提升响应速度,避免无用计算 |
4.7 完整示例:带依赖注入的 UserViewModel
// UserViewModel.kt (完整版)
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
// 使用 liveData 构建器自动处理协程生命周期
val userLiveData: LiveData<User> = liveData {
emit(repository.fetchUser())
}
// 手动管理时使用 viewModelScope
fun refreshUser() {
viewModelScope.launch {
// 显示加载状态
_loadingLiveData.value = true
try {
withTimeout(10_000) {
val user = withContext(Dispatchers.IO) {
repository.fetchUser()
}
_userLiveData.value = user
}
} catch (e: TimeoutCancellationException) {
_errorLiveData.value = "请求超时"
} finally {
_loadingLiveData.value = false
}
}
}
}