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 生命周期与依赖注入示意图

viewModelScope 生命周期与依赖注入 Activity / Fragment 生命周期感知 ViewModel viewModelScope SupervisorJob + Main 协程自动取消 Repository Hilt / Dagger 注入 Dispatchers.IO 执行 viewModelScope.launch 生命周期终止时 onCleared() → Job.cancel() 所有协程自动取消 依赖注入流程 ① 使用 @HiltViewModel 注解 ViewModel ② 构造函数注入 Repository 实例 ③ Repository 内部使用 withContext(Dispatchers.IO) 切换线程

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
            }
        }
    }
}