Coroutines Basics in Android
A coroutine is a suspendable computation: a unit of work that can pause at a suspension point (like an I/O call) and resume later without blocking the underlying thread. On Android, coroutines are the standard mechanism for asynchronous work — network calls, database queries, file I/O — replacing older patterns like AsyncTask or raw callback chains. The suspend modifier marks a function as one that may suspend; calling a suspend function from regular code requires either another suspend function or a coroutine builder such as launch or async. Because coroutines are managed by the Kotlin runtime rather than the OS, thousands of them can run concurrently with far less overhead than an equivalent number of threads.
Cricket analogy: A bowler who can walk back to their mark and pause between deliveries without stopping the match clock is like a suspending function — it pauses at a natural break (an I/O call) and resumes later without blocking the whole game, unlike a rain delay that halts everything (a blocked thread).
Structured concurrency is the principle that every coroutine runs within a CoroutineScope, and a scope's lifetime bounds the lifetime of all coroutines launched in it. When the scope is cancelled — for example when a ViewModel is cleared or an Activity is destroyed — every coroutine launched in that scope is cancelled too, and cancellation propagates to any child coroutines they've launched. This eliminates a whole class of bugs where background work silently continues (and potentially crashes the app) after the UI component that started it is gone.
Cricket analogy: A fielding team's substitutes are only on the ground because the captain (the scope) is still leading that innings; the moment the captain is dismissed or the innings ends, every substitute fielder is called off too, cascading down through the whole fielding unit — like structured concurrency cancelling child coroutines.
Builders: launch vs. async
launch starts a coroutine that does not return a result to the caller — it's fire-and-forget from the caller's perspective, useful for side effects like updating a StateFlow. It returns a Job that can be used to cancel or join the coroutine. async starts a coroutine that computes and returns a value wrapped in a Deferred<T>; calling .await() on that Deferred suspends until the result is ready. Use async when you need to run multiple independent operations concurrently and combine their results, e.g. fetching a user profile and their settings in parallel rather than sequentially.
Cricket analogy: A fielder throwing the ball back to the keeper without needing to know what happens next is like launch — fire-and-forget for a side effect; a third umpire reviewing a run-out and returning a definite 'out' or 'not out' decision that the on-field umpire must wait for is like async/await, producing a result you need.
class ProfileViewModel(
private val userRepo: UserRepository,
private val settingsRepo: SettingsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
fun loadProfile(userId: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
coroutineScope {
val userDeferred = async { userRepo.fetchUser(userId) }
val settingsDeferred = async { settingsRepo.fetchSettings(userId) }
val user = userDeferred.await()
val settings = settingsDeferred.await()
_uiState.update { it.copy(user = user, settings = settings, isLoading = false) }
}
} catch (e: IOException) {
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
}
}
}Dispatchers and viewModelScope
A CoroutineDispatcher determines which thread pool a coroutine runs on. Dispatchers.Main runs on Android's UI thread and is where Compose recomposition and view updates must happen; Dispatchers.IO is a thread pool tuned for blocking I/O work like network or disk calls; Dispatchers.Default is tuned for CPU-intensive work like sorting large lists or parsing JSON. Well-behaved suspend functions (like Room and Retrofit's generated suspend APIs) already switch to the correct dispatcher internally, so you rarely need to specify withContext(Dispatchers.IO) yourself unless calling a blocking API directly. viewModelScope is a CoroutineScope built into every ViewModel that is automatically cancelled when onCleared() is called, using Dispatchers.Main.immediate by default — this is the standard scope for launching coroutines tied to a screen's state.
Cricket analogy: The on-field umpire (Dispatchers.Main) must personally make every live call the crowd sees; the groundstaff (Dispatchers.IO) handle pitch and boundary rope work off to the side; the statisticians crunching net run-rate scenarios (Dispatchers.Default) work independently — and a well-run broadcast (viewModelScope) auto-cancels its feed when the match ends.
Think of a dispatcher as choosing which lane on a highway a coroutine drives in — Main is the single-lane road reserved for UI updates, IO is a wide multi-lane road built for many cars waiting on external traffic (network/disk), and Default is a lane optimized for cars doing heavy work (CPU crunching) rather than waiting.
Launching a coroutine in GlobalScope inside a ViewModel or composable is a common mistake: it is never automatically cancelled, so the coroutine can keep running (and holding references) long after the screen is gone, causing leaks and wasted work. Always prefer a lifecycle-bound scope like viewModelScope or rememberCoroutineScope() in Compose.
- Coroutines are lightweight, suspendable units of work managed by the Kotlin runtime, not the OS.
- suspend functions can only be called from other suspend functions or from a coroutine builder like launch/async.
- Structured concurrency ties a coroutine's lifetime to its enclosing scope, so cancellation propagates automatically.
- launch is fire-and-forget (returns Job); async returns a Deferred<T> for a computed result.
- viewModelScope is the standard scope for ViewModel coroutines and is auto-cancelled in onCleared().
- Avoid GlobalScope in app code — it bypasses structured concurrency and can leak background work.
Practice what you learned
1. What does the `suspend` modifier indicate about a function?
2. Which coroutine builder should you use when you need to run two independent operations concurrently and combine their results?
3. What happens to coroutines launched in viewModelScope when the ViewModel's onCleared() is called?
4. Which dispatcher is appropriate for CPU-intensive work like sorting a very large list?
5. Why is using GlobalScope discouraged in an Android ViewModel or composable?
Was this page helpful?
You May Also Like
Kotlin Flow Fundamentals
Flow is Kotlin's asynchronous stream type for emitting multiple sequential values over time, built on coroutines, and central to reactive state management in Android apps.
Lifecycle-Aware Coroutines
Lifecycle-aware coroutine APIs like repeatOnLifecycle and lifecycleScope tie coroutine execution to Android component lifecycle states, avoiding wasted work and crashes from stale UI updates.
ViewModel Basics
What Android's ViewModel class does, how it survives configuration changes, and how to create and scope ViewModels correctly in a Compose app.
stateIn and shareIn
stateIn and shareIn convert cold Flows into hot, shareable StateFlow and SharedFlow instances, with SharingStarted policies controlling exactly when the underlying producer runs.