viewModelScope — 是来自androidx.lifecycle库的内置CoroutineScope,它与ViewModel的生命周期绑定,并在其清理时自动取消。根据Google Android Developers, 2025,viewModelScope是MVVM架构中启动协程的标准机制,提供安全的异步操作,没有内存泄漏的风险。ViewModelScope默认使用Dispatchers.Main,其内部的所有IO操作都必须通过withContext执行。
要点
viewModelScope — 是ViewModel接口上的扩展属性(extension property),在lifecycle-viewmodel-ktx库(从2.1.0版本开始)中添加。它提供了一个与ViewModel生命周期绑定的现成CoroutineScope。
// 内部结构(简化版)
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope = this.getTag(JOB_KEY)
if (scope != null) return scope
return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { setTag(JOB_KEY, it) }
}
Scope在首次访问时惰性(lazy)创建,并通过setTag缓存。使用SupervisorJob,这意味着一个子协程中的异常不会取消其他协程。默认调度器是Dispatchers.Main.immediate,如果调用已经在Main上,它会在主线程上执行代码而无需额外调度。
当ViewModel离开生命周期(Activity完成或Fragment删除)时,系统调用clear(),触发onCleared()。在此回调中,viewModelScope取消其Job,递归地结束所有活跃协程。该机制通过Closeable接口实现,scope的Job注册为自动关闭的资源。
viewModelScope与ViewModel生命周期的绑定机制基于标记和onCleared回调。让我们逐步了解它是如何工作的。
当ViewModel执行viewModelScope.launch { ... }时,getter检查JOB_KEY标记下是否有保存的scope。如果scope尚未创建——则创建新的CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)实例。Scope通过内部标记映射存储在ViewModel内部。
通过viewModelScope.launch或viewModelScope.async启动的所有协程都是scope的SupervisorJob的子级。它们在主线程上工作(除非通过withContext指定了其他调度器)。只要ViewModel存活——协程可以是活跃的、挂起的或完成的。
当系统销毁ViewModel时,调用ViewModel.clear()。在clear()内部会发生以下情况:
在屏幕旋转时,Activity被重新创建,但ViewModel被保留(得益于ViewModelStoreOwner)。这意味着viewModelScope保持活跃,协程继续运行而不中断。Activity重新创建后,相同的ViewModel(和相同的scope)被重用——数据加载不会重新开始。
MVVM(Model-View-ViewModel)——是Google推荐的Android应用程序架构。viewModelScope在其中作为异步操作的执行者占据核心位置。
| 层 | 组件 | viewModelScope的作用 |
|---|---|---|
| UI | Activity / Fragment | 观察来自ViewModel的StateFlow/LiveData |
| ViewModel | ViewModel | 通过viewModelScope启动协程,管理UI状态 |
| Repository | Repository | 接收从viewModelScope协程调用的suspend函数 |
| Data | DAO / Api | 执行实际请求(Room, Retrofit) |
ViewModel通过viewModelScope启动协程,在其内部调用Repository的suspend函数。结果转换为StateFlow,由UI层观察。这种方案确保了清晰的职责分离和每个层的独立可测试性。
如果协程从Fragment启动,在屏幕旋转时会随着Fragment的销毁而被取消。ViewModel在旋转中存活,因此在其scope中启动的协程继续执行。这是viewModelScope在加载数据时相对于lifecycleScope的关键优势。
让我们看看在Kotlin的Android应用程序中使用viewModelScope的三个实际场景。
class ProfileViewModel(
private val repo: ProfileRepository
) : ViewModel() {
private val _profile = MutableStateFlow<Profile?>(null)
val profile: StateFlow<Profile?> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val result = repo.getProfile()
_profile.value = result
}
}
}
在init块中立即启动配置文件的加载。协程默认在主线程上执行。Repository在其suspend函数内部使用withContext(Dispatchers.IO)进行网络请求,因此ViewModel不关心线程切换。
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
fun fetchItems() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val items = repo.getItems()
_state.value = UiState.Success(items)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
UI状态通过sealed class UiState描述。ViewModel在每次更改时更新state。Fragment订阅StateFlow,只对当前状态做出反应,忽略重复旋转时的过时调用。
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
在每个新的搜索请求中,之前的协程被取消。delay(300)实现了防抖(debounce)——搜索仅在输入暂停300毫秒后执行。这减少了服务器负载并防止了过时的结果。
两个scope都由AndroidX Lifecycle库提供,但绑定到不同的生命周期。它们之间的选择取决于任务类型。
| 特性 | viewModelScope | lifecycleScope |
|---|---|---|
| 所有者 | ViewModel | LifecycleOwner(Activity/Fragment) |
| 旋转时取消 | 否(ViewModel保留) | 是(Activity重新创建) |
| 默认调度器 | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| 可用范围 | ViewModel | Activity, Fragment, Service |
| 典型场景 | 数据加载,业务逻辑 | UI交互,动画,snackbar |
Google建议使用viewModelScope处理所有与数据加载和处理相关的任务。lifecycleScope应应用于与UI生命周期特定时刻绑定的操作——例如在屏幕首次出现时启动动画,或在离开屏幕时应停止的Location更新订阅。
即使在文档良好的Android API中,开发人员也会犯典型错误。让我们看看四个最常见的问题。
最隐蔽的错误——在ViewModel清理后尝试更新StateFlow或LiveData。虽然viewModelScope在onCleared()时被取消,但协程可能会在实际取消时刻之前执行代码。使用isActive进行检查或依赖catch块的完成。
viewModelScope内部使用SupervisorJob,隔离协程间的错误。但是如果在viewModelScope.launch中使用自己的Job()启动协程,该协程会成为SupervisorJob的子级,但不会受到其他协程错误时取消的保护。
虽然viewModelScope没有严格限制,但数千个活跃协程可能会降低系统速度。对于长数据列表,使用Flow配合collectLatest,而不是为每个元素创建单独的协程。
如果意外导入GlobalScope而不是viewModelScope,协程在ViewModel清理时不会被取消。这会导致内存泄漏和潜在的崩溃。始终检查协程是否通过viewModelScope启动,特别是在继承的fragment中。
常见问题
不能直接更改viewModelScope的调度器——它被硬编码为Dispatchers.Main.immediate。但是在协程内部可以通过withContext切换到其他调度器。在测试中更改调度器,请通过Rule使用TestDispatcher。
不要将scope传递给Repository——这违反了架构原则。Repository应该提供suspend函数,而ViewModel自己通过viewModelScope管理协程。如果Repository需要scope——请重新审视架构,转向Clean Architecture。
SupervisorJob确保一个协程中的异常(例如,多个独立请求中一个的加载错误)不会取消其他协程。这符合ViewModel的场景,其中不同屏幕加载独立的数据。
是的,viewModelScope在任何ViewModel中都可用,无论UI类型(View System或Jetpack Compose)如何。在Compose中,协程也通过viewModelScope启动,UI效果使用LaunchedEffect和rememberCoroutineScope。
调用viewModelScope.cancel()立即取消scope——所有活跃的协程以CancellationException结束。如果之后调用viewModelScope.launch,新的scope将在下次访问getter时自动创建。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。