🎯Comparing SupervisorScope
with viewModelScope
: Understanding Their Use Cases and Differences in Android
When working with Kotlin coroutines in Android development, managing asynchronous tasks effectively is essential. Two coroutine scopes that Android developers frequently encounter are SupervisorScope
and viewModelScope
. While both help manage coroutines, their behavior and use cases differ significantly. In this blog post, we'll explore these differences, focusing on how they handle exceptions, manage child coroutines, and why choosing the right scope for each task is crucial for building efficient, error-resilient applications.
Purpose and Use Cases
SupervisorScope
SupervisorScope
is designed for managing independent coroutines that should not affect each other if one of them fails. This scope is useful in scenarios where you want to run multiple concurrent tasks but don’t want the failure of one tasks to cancel all others. For instance, if your application needs to make multiple network requests to different APIs, and a failure in one request should not affect the others,SupervisorScope
is the right choice.
supervisorScope {
launch {
// Task 1
throw Exception("This will only cancel Task 1")
}
launch {
// Task 2
}
// Task 3
}
// Output
Task 1
... Exception
Task 2
Task 3
- A common use of
SupervisorScope
is when managing concurrent operations that are loosely related, such as performing different network or data-fetching operations in parallel without each task being dependent on the success of the others.
viewModelScope
- The
viewModelScope
is tied to the view model such that when the view model is cleaned up (i.e.onCleared
is called), the scope is cancelled. This ensures that when your view model goes away, so does all the coroutine work associated with it. This avoids wasted work and memory leaks.
viewModelScope.launch {
// Fetch data for the UI
}
- The
viewModelScope
uses theDispatchers.Main
coroutine dispatcher. ACoroutineDispatcher
controls how a coroutine runs, including what thread the coroutine code runs on.Dispatcher.Main
puts the coroutine on the UI or main thread. This makes sense as a default forViewModel
coroutines, because often, view models manipulate the UI.
if your UI is dependent on data-fetching or complex calculations, using viewModelScope
ensures that these tasks are automatically stopped when no longer needed, helping conserve resources and prevent memory leaks.
Cancellation behavior
SupervisorScope
- Within a
SupervisorScope
, if one child coroutine throws an exception, it does not cancel the other sibling coroutines. This is different from a regular coroutine scope, where a failure in one child would cancel all siblings. This behavior is useful for operations that should remain unaffected by the failure of other tasks. - However, if the
SupervisorScope
itself is canceled, all child coroutines are canceled regardless of individual status. Because all sibling coroutines are independent of each other. If one coroutine fails, the failure does not propagate to its siblings. - It is designed to avoid the default failure propagation behavior, which is why it’s often used for tasks that should run independently, even if one of them encounters an error.
viewModelScope
- In
viewModelScope
, cancellation is tied to theviewModel
lifecycle. If theviewModel
is cleared (e.g., when the user navigates away from a screen), all coroutines in this scope are canceled automatically. This ensures that ongoing work does not continue if the related UI is no longer present. viewModelScope
is indeed part of a structured concurrency model, but its foundation onSupervisorJob
modifies its behavior. In typical structured concurrency (likeCoroutineScope
with a regularJob
), an exception in any child coroutine would cancel all other sibling coroutines.
public val ViewModel.viewModelScope: CoroutineScope
get() {
...
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)
)
}
...
}
- The
CoroutineContext
here is set up with aSupervisorJob()
and aMain
dispatcher, but it lacks aCoroutineExceptionHandler
. (Read on to learn what CoroutineExceptionHandler is). By default, any unhandled exceptions will result in a stack trace being printed, which is then followed by the app crashing. To ensure better handling of uncaught exceptions and to monitor errors in production, it's advisable to define a customCoroutineContext
instead of relying solely on theviewModelScope
helper.
Implementation
SupervisorScope
SupervisorScope
is created using thesupervisorScope
function in Kotlin. It does not belong to any lifecycle component by default, so it’s often used inside custom coroutine builders or with lifecycle observers.
fun main() = runBlocking {
launch {
supervisorScope {
// Launch Task 1 inside supervisorScope
launch {
try {
println("Task 1 started")
// Simulate some work
for (i in 1..5) {
println("Task 1 is working... $i")
delay(500)
}
println("Task 1 completed")
} catch (e: Exception) {
println("Task 1 failed with exception: ${e.message}")
}
}
// Launch Task 2 inside supervisorScope (this one will throw an exception)
launch {
try {
println("Task 2 started")
// Simulate some work that will fail
delay(1000)
throw Exception("Simulated error in Task 2")
} catch (e: Exception) {
println("Task 2 failed with exception: ${e.message}")
}
}
// Launch Task 3 inside supervisorScope
launch {
try {
println("Task 3 started")
// Simulate some work
for (i in 1..5) {
println("Task 3 is working... $i")
delay(700)
}
println("Task 3 completed")
} catch (e: Exception) {
println("Task 3 failed with exception: ${e.message}")
}
}
}
}
}
viewModelScope
viewModelScope
is automatically available within anyviewModel
that imports thelifecycle-viewmodel-ktx
library. You simply callviewModelScope.launch
to start a coroutine in this scope.- When working with
viewModelScope
, if you need to prevent one coroutine’s failure from canceling others, you would need to handle errors explicitly. This can be done by usingtry-catch
blocks within each coroutine or by usingCoroutineExceptionHandler
to manage failures gracefully.
Let see this example:
- Task 1 and Task 3 perform some work in a loop, with delays to simulate ongoing tasks.
- Task 2 deliberately throws an exception after a delay, simulating a failure.
- Each
launch
block inviewModelScope
has its owntry-catch
block to handle exceptions locally.
class MyViewModel : ViewModel() {
fun startTasks() {
// Launch Task 1 in viewModelScope
viewModelScope.launch {
try {
println("Task 1 started")
// Simulate some work
for (i in 1..5) {
println("Task 1 is working... $i")
kotlinx.coroutines.delay(500)
}
println("Task 1 completed")
} catch (e: Exception) {
println("Task 1 failed with exception: ${e.message}")
}
}
// Launch Task 2 in viewModelScope (this one will throw an exception)
viewModelScope.launch {
try {
println("Task 2 started")
// Simulate some work that will fail
kotlinx.coroutines.delay(1000)
throw Exception("Simulated error in Task 2")
} catch (e: Exception) {
println("Task 2 failed with exception: ${e.message}")
}
}
// Launch Task 3 in viewModelScope
viewModelScope.launch {
try {
println("Task 3 started")
// Simulate some work
for (i in 1..5) {
println("Task 3 is working... $i")
kotlinx.coroutines.delay(700)
}
println("Task 3 completed")
} catch (e: Exception) {
println("Task 3 failed with exception: ${e.message}")
}
}
}
}
// Output
Task 1 started
Task 3 started
Task 2 started
Task 1 is working... 1
Task 3 is working... 1
Task 1 is working... 2
Task 2 failed with exception: Simulated error in Task 2
Task 3 is working... 2
Task 1 is working... 3
... and so on ...
Task 1 completed
Task 3 completed
Task 2’s failure doesn’t affect Task 1 or Task 3 — they continue working as expected. This approach is ideal for scenarios where tasks should fail independently without disrupting other ongoing tasks, such as multiple network requests in a UI component.
UncaughtExceptionHandler
Let’s talk aboutUncaughtExceptionHandler
, yesUncaughtExceptionHandler
is relevant to how coroutine exceptions are handled, especially in coroutine scopes like viewModelScope
or SupervisorScope
. Here’s how it ties in:
UncaughtExceptionHandler Basics
- In Java and Kotlin,
UncaughtExceptionHandler
is a handler that catches unhandled exceptions in threads. In coroutines, unhandled exceptions are managed differently depending on the coroutine’s context, scope, and parent-child relationships.
Uncaught Exceptions in viewModelScope
viewModelScope
uses structured concurrency, where all coroutines in the scope are linked to theViewModel
. If an exception is thrown in one coroutine and it’s uncaught (not managed by atry-catch
), the exception will propagate to the scope’s parent, which isviewModelScope
itself. This means all sibling coroutines will be canceled by default, unless the exception is handled explicitly within each coroutine.- If an exception is not caught, it bubbles up to the default
CoroutineExceptionHandler
. However, sinceviewModelScope
is lifecycle-aware, uncaught exceptions lead to a cleanup of the scope by canceling all coroutines, which makes an explicitCoroutineExceptionHandler
particularly useful if you want to handle failures without propagating cancellation.
SupervisorScope and Exception Handling
SupervisorScope
allows coroutines to run independently. If one coroutine withinSupervisorScope
throws an uncaught exception, it doesn’t automatically cancel its siblings. This behavior is controlled bySupervisorJob
, which essentially overrides the usual behavior of cascading cancellation within a structured concurrency model.- With
SupervisorScope
, if you add anUncaughtExceptionHandler
(throughCoroutineExceptionHandler
), you can manage exceptions without affecting other sibling coroutines, making it useful for tasks where partial failures are expected.
Using CoroutineExceptionHandler with viewModelScope and SupervisorScope
CoroutineExceptionHandler
can be attached to individual coroutines in bothviewModelScope
andSupervisorScope
to manage uncaught exceptions gracefully. For example, you can log errors or perform specific actions when a coroutine fails without canceling the entire scope.
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e("Error", "Caught exception: ${throwable.message}")
}
viewModelScope.launch(exceptionHandler) {
// Coroutine with exception handler
throw Exception("Test exception")
}
In this case, if an exception is thrown, it will be caught by the exceptionHandler
rather than propagating up and canceling other coroutines in viewModelScope
.
UncaughtExceptionHandler
(via CoroutineExceptionHandler
) plays a crucial role in managing unhandled exceptions in coroutines. In viewModelScope
, unhandled exceptions typically cancel the entire scope, but by adding a CoroutineExceptionHandler
, you can manage errors without canceling all coroutines. In SupervisorScope
, this isolation is even more direct, as sibling coroutines are designed not to affect each other’s execution upon failure.
Summary
In summary, SupervisorScope
provides independent failure handling, making it useful for concurrent operations where failure in one shouldn’t impact others. viewModelScope
, on the other hand, is ideal for managing coroutines tied to the lifecycle of a ViewModel
, especially for tasks related to UI that need to stop when the screen is no longer in use.
There are a few points to clarify regarding this:
SupervisorJob
A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children, so a supervisor can implement a custom policy for handling failures of its children. If a parent job is specified, then this supervisor job becomes a child job of parent and is cancelled when the parent fails or is cancelled. All this supervisor’s children are cancelled in this case, too.
- A failure of a child job that was created using launch can be handled via CoroutineExceptionHandler in the context.
- A failure of a child job that was created using async can be handled via Deferred.await on the resulting deferred value.
ViewModelScope is defined with SupervisorJob
, why does it still failed?
- In Kotlin coroutines, when you create a
SupervisorJob
, it usually isolates the failures of its child coroutines—meaning if one child fails, it doesn’t affect the other children within the sameSupervisorJob
. However, if you specify a parent job for thisSupervisorJob
, things work a bit differently because it now inherits the parent’s lifecycle and failure handling. - When the parent job fails or is cancelled, all of its child jobs — including the
SupervisorJob
—are cancelled. This cancellation is propagated downwards to all children in the job hierarchy, meaning that even the children of theSupervisorJob
will be cancelled.
References
Here are some insightful articles that explore the differences between SupervisorScope
and viewModelScope
, particularly in terms of exception handling, structured concurrency, and testing: