viewModelScope is a built-in CoroutineScope from the androidx.lifecycle library that is tied to the ViewModel lifecycle and automatically cancelled when the ViewModel is cleared. According to Google Android Developers, 2025, viewModelScope is the standard mechanism for launching coroutines in MVVM architecture, ensuring safe asynchronous operations without the risk of memory leaks. ViewModelScope uses Dispatchers.Main by default, and all IO operations within it must be executed via withContext.
Key Takeaways
viewModelScope is an extension property on the ViewModel interface, added in the lifecycle-viewmodel-ktx library (starting from version 2.1.0). It provides a ready-to-use CoroutineScope tied to the ViewModel lifecycle.
// Internal structure (simplified)
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) }
}
The scope is created lazily on first access and cached via setTag. It uses SupervisorJob, which means an exception in one child coroutine does not cancel the others. The default dispatcher is Dispatchers.Main.immediate, which executes code on the main thread without additional dispatching if already on the Main thread.
When the ViewModel leaves the lifecycle (Activity is finished or Fragment is removed), the system calls clear(), which triggers onCleared(). In this callback, viewModelScope cancels its Job, which recursively terminates all active coroutines. The mechanism is implemented through the Closeable interface, where the scope Job is registered as a resource for automatic closing.
The mechanism of binding viewModelScope to the ViewModel lifecycle is based on tagging and the onCleared callback. Let us go through it step by step.
When the ViewModel calls viewModelScope.launch { ... }, the getter checks if a scope is already stored under the JOB_KEY tag. If no scope exists, a new CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) instance is created. The scope is stored inside the ViewModel via an internal tag map.
All coroutines launched via viewModelScope.launch or viewModelScope.async become children of the scope’s SupervisorJob. They run on the main thread (unless a different dispatcher is specified via withContext). As long as the ViewModel is alive, coroutines can be active, suspended, or completed.
When the system destroys the ViewModel, ViewModel.clear() is called. Inside clear(), the following happens:
When the screen is rotated, the Activity is recreated, but the ViewModel survives (thanks to ViewModelStoreOwner). This means viewModelScope remains active and coroutines continue executing without interruption. After the Activity is recreated, the same ViewModel (and the same scope) is reused — data loading does not start from scratch.
MVVM (Model-View-ViewModel) is Google’s recommended architecture for Android applications. viewModelScope plays a central role in it as the executor of asynchronous operations.
| Layer | Component | viewModelScope role |
|---|---|---|
| UI | Activity / Fragment | Observes StateFlow/LiveData from ViewModel |
| ViewModel | ViewModel | Launches coroutines via viewModelScope, manages UI state |
| Repository | Repository | Exposes suspend functions called from viewModelScope coroutines |
| Data | DAO / Api | Executes actual requests (Room, Retrofit) |
The ViewModel launches coroutines via viewModelScope, inside which it calls suspend functions of the Repository. The result is transformed into StateFlow, which is observed by the UI layer. This design ensures clear separation of concerns and independent testability of each layer.
If coroutines were launched from a Fragment, they would be cancelled on screen rotation along with the Fragment destruction. ViewModel survives rotation, so coroutines launched in its scope continue execution. This is the key advantage of viewModelScope over lifecycleScope when loading data.
Let us look at three practical scenarios for using viewModelScope in an Android application with Kotlin.
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
}
}
}
In the init block, profile loading starts immediately. The coroutine runs on the main thread (by default). The repository uses withContext(Dispatchers.IO) for the network request inside its suspend function, so the ViewModel does not need to handle thread switching.
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 state is described using a sealed class UiState. The ViewModel updates the state on each change. The Fragment subscribes to StateFlow and reacts only to the current state, ignoring stale calls from previous rotations.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
On each new search query, the previous coroutine is cancelled. delay(300) implements debounce — the search executes only after 300 ms of inactivity. This reduces server load and prevents stale results.
Both scopes are provided by the AndroidX Lifecycle library but are tied to different lifecycles. The choice depends on the type of task.
| Characteristic | viewModelScope | lifecycleScope |
|---|---|---|
| Owner | ViewModel | LifecycleOwner (Activity/Fragment) |
| Cancelled on rotation | No (ViewModel survives) | Yes (Activity is recreated) |
| Default dispatcher | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Available in | ViewModel | Activity, Fragment, Service |
| Typical use case | Data loading, business logic | UI interactions, animations, chunks |
Google recommends using viewModelScope for all data loading and processing tasks. lifecycleScope should be used for operations tied to a specific UI lifecycle moment — for example, starting an animation on first screen appearance or subscribing to Location updates that should stop when leaving the screen.
Even in a well-documented Android API, developers make typical mistakes. Let us look at four of the most common issues.
The most insidious mistake is trying to update StateFlow or LiveData after the ViewModel has been cleared. Although viewModelScope is cancelled on onCleared(), a coroutine may execute code before the actual cancellation takes effect. Use isActive to check or rely on catch-block completion.
viewModelScope uses SupervisorJob internally, which isolates errors between coroutines. However, if you launch a coroutine with its own Job() inside viewModelScope.launch, that coroutine becomes a child of SupervisorJob but will not be protected from cancellation caused by errors in other coroutines.
Although viewModelScope has no hard limit, thousands of active coroutines can slow down the system. For long data lists, use Flow with collectLatest instead of creating separate coroutines for each item.
If GlobalScope is accidentally imported instead of viewModelScope, the coroutine will not be cancelled when the ViewModel is cleared. This leads to memory leaks and potential crashes. Always ensure coroutines are launched via viewModelScope, especially in Fragment subclasses.
Frequently Asked Questions
You cannot directly change the dispatcher of viewModelScope — it is hardcoded to Dispatchers.Main.immediate. However, inside a coroutine you can switch to another dispatcher via withContext. To change the dispatcher in tests, use TestDispatcher via a Rule.
Do not pass scope to the Repository — this breaks architectural principles. The Repository should expose suspend functions, and the ViewModel itself manages coroutines through viewModelScope. If the Repository requires a scope, reconsider the architecture in favor of Clean Architecture.
SupervisorJob ensures that an exception in one coroutine (e.g., a loading error in one of several independent requests) does not cancel the other coroutines. This matches the ViewModel scenario where different screens load independent data.
Yes, viewModelScope is available in any ViewModel regardless of the UI type (View System or Jetpack Compose). In Compose, coroutines are also launched via viewModelScope, while UI effects use LaunchedEffect and rememberCoroutineScope.
Calling viewModelScope.cancel() cancels the scope immediately — all active coroutines terminate with CancellationException. If viewModelScope.launch is called afterwards, a new scope is created automatically on the next access to the getter.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also