Coroutines
Kotlin coroutines and Flow for asynchronous programming and reactive streams in Android
You are an expert in Kotlin coroutines and Flow for building Android applications with Kotlin.
## Key Points
- `Dispatchers.Main` — UI thread, for updating views and state.
- `Dispatchers.IO` — optimized for disk and network I/O.
- `Dispatchers.Default` — CPU-intensive work (sorting, parsing).
- Use `viewModelScope` in ViewModels and `lifecycleScope` in Activities/Fragments — never create unscoped `GlobalScope` coroutines.
- Switch dispatchers with `withContext` rather than launching new coroutines on different dispatchers.
- Use `StateFlow` with `SharingStarted.WhileSubscribed(5000)` to keep upstream alive briefly during configuration changes.
- Prefer `collectAsStateWithLifecycle()` in Compose over plain `collectAsState()` to respect lifecycle.
- Use `supervisorScope` or `SupervisorJob` when child failures should not cancel siblings.
- Always handle `CancellationException` correctly — rethrow it, never swallow it.
- Catching `Exception` broadly and accidentally swallowing `CancellationException`, breaking structured concurrency.
- Using `GlobalScope.launch` which leaks coroutines and ignores lifecycle.
- Collecting a `Flow` in `onCreate` without lifecycle awareness, causing collection to continue when the app is in the background.
## Quick Example
```kotlin
suspend fun fetchUser(userId: String): User {
return withContext(Dispatchers.IO) {
apiService.getUser(userId)
}
}
```
```kotlin
suspend fun processImage(bitmap: Bitmap): Bitmap {
return withContext(Dispatchers.Default) {
applyFilter(bitmap)
}
}
```skilldb get android-kotlin-skills/CoroutinesFull skill: 220 linesCoroutines and Flow — Android/Kotlin
You are an expert in Kotlin coroutines and Flow for building Android applications with Kotlin.
Core Philosophy
Kotlin coroutines bring structured concurrency to Android, meaning that concurrent work has a defined scope, a defined lifetime, and automatic cancellation when that scope ends. This is a fundamental shift from the old model of launching fire-and-forget AsyncTask or Thread instances that outlive their creators. When you launch a coroutine in viewModelScope, it is automatically cancelled when the ViewModel is cleared. When a lifecycleScope coroutine runs in a Fragment, it respects the Fragment's lifecycle. The structure is the safety net.
The dispatcher system (Main, IO, Default) is how coroutines avoid blocking threads. The key rule is simple: suspend functions should be main-safe. A function like fetchUser() should internally use withContext(Dispatchers.IO) for the network call, so the caller never needs to worry about which thread it runs on. This convention means you can call any suspend function from the main thread without fear, and the responsibility for thread-switching lives in the function that knows it needs it.
Flow is the reactive primitive for values that change over time. StateFlow holds the latest value and replays it to new collectors -- perfect for UI state. SharedFlow supports configurable buffering and replay -- useful for events. Raw Flow is cold, meaning it only runs when collected. Understanding these three types and when to use each is essential: StateFlow for "what is the current state," SharedFlow for "what happened," and Flow for "give me a stream of values on demand."
Anti-Patterns
-
Catching Exception broadly and swallowing CancellationException: A
catch (e: Exception)block catchesCancellationException, which breaks structured concurrency by preventing the coroutine from being properly cancelled. Always rethrowCancellationException, or use a more specific exception type in catch blocks. -
Using GlobalScope for any purpose:
GlobalScopecoroutines have no parent scope, no lifecycle awareness, and no automatic cancellation. They leak work that runs indefinitely, consuming resources and potentially updating UI state long after the screen is gone. UseviewModelScope,lifecycleScope, or a custom scope with aSupervisorJob. -
Collecting a Flow in onCreate without lifecycle awareness: Collecting a Flow in
onCreateoronCreateViewwithout usingrepeatOnLifecycle(Lifecycle.State.STARTED)means collection continues when the app is in the background, wasting resources and potentially causing crashes from UI updates in an inactive state. -
Using runBlocking on the main thread:
runBlockingblocks the current thread until the coroutine completes. On the main thread, this causes an ANR (Application Not Responding) if the blocked work takes more than a few seconds. It exists for bridging synchronous APIs in tests andmain()functions, not for production Android code. -
Creating new StateFlow on every recomposition: If a Composable function creates a
stateInconversion inline, a newStateFlowis created on every recomposition, losing state and restarting collection. HoiststateIncalls into the ViewModel where they are created once and survive recomposition.
Overview
Kotlin coroutines provide structured concurrency for asynchronous programming on Android. Combined with Flow for reactive streams, they replace callbacks, RxJava chains, and AsyncTask with sequential, readable code that respects Android lifecycle constraints.
Core Concepts
Suspend Functions
A suspend function can pause and resume without blocking a thread.
suspend fun fetchUser(userId: String): User {
return withContext(Dispatchers.IO) {
apiService.getUser(userId)
}
}
Coroutine Scopes and Structured Concurrency
Every coroutine runs in a scope. When a scope is cancelled, all its child coroutines are cancelled too.
class UserViewModel : ViewModel() {
fun loadUser(userId: String) {
viewModelScope.launch {
try {
val user = repository.fetchUser(userId)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
}
Dispatchers
Dispatchers.Main— UI thread, for updating views and state.Dispatchers.IO— optimized for disk and network I/O.Dispatchers.Default— CPU-intensive work (sorting, parsing).
suspend fun processImage(bitmap: Bitmap): Bitmap {
return withContext(Dispatchers.Default) {
applyFilter(bitmap)
}
}
Flow Basics
Flow is a cold asynchronous stream that emits values sequentially.
fun observeUsers(): Flow<List<User>> = flow {
while (true) {
val users = apiService.getUsers()
emit(users)
delay(30_000)
}
}
StateFlow and SharedFlow
StateFlow holds a single current value and replays it to new collectors. SharedFlow supports configurable replay and buffering.
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
val searchResults: StateFlow<List<Result>> = _query
.debounce(300)
.filter { it.length >= 2 }
.flatMapLatest { query -> repository.search(query) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun onQueryChanged(newQuery: String) {
_query.value = newQuery
}
}
Implementation Patterns
Parallel Decomposition
Run independent tasks concurrently with async and awaitAll.
suspend fun loadDashboard(): Dashboard = coroutineScope {
val userDeferred = async { repository.fetchUser() }
val ordersDeferred = async { repository.fetchOrders() }
val notificationsDeferred = async { repository.fetchNotifications() }
Dashboard(
user = userDeferred.await(),
orders = ordersDeferred.await(),
notifications = notificationsDeferred.await()
)
}
Retry with Exponential Backoff
suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: Exception) {
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong()
}
}
return block() // last attempt — let exception propagate
}
Flow Operators
val filteredItems: Flow<List<Item>> = repository.observeItems()
.map { items -> items.filter { it.isActive } }
.distinctUntilChanged()
.catch { e -> emit(emptyList()) }
.flowOn(Dispatchers.Default)
Collecting Flows in Compose
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> UserContent(state.user)
is UiState.Error -> ErrorMessage(state.message)
}
}
Cancellation-Safe Resource Management
suspend fun downloadFile(url: String, destination: File) {
withContext(Dispatchers.IO) {
val response = httpClient.get(url)
destination.outputStream().use { output ->
response.bodyAsChannel().copyTo(output)
ensureActive() // check cancellation before committing
}
}
}
Best Practices
- Use
viewModelScopein ViewModels andlifecycleScopein Activities/Fragments — never create unscopedGlobalScopecoroutines. - Switch dispatchers with
withContextrather than launching new coroutines on different dispatchers. - Use
StateFlowwithSharingStarted.WhileSubscribed(5000)to keep upstream alive briefly during configuration changes. - Prefer
collectAsStateWithLifecycle()in Compose over plaincollectAsState()to respect lifecycle. - Use
supervisorScopeorSupervisorJobwhen child failures should not cancel siblings. - Always handle
CancellationExceptioncorrectly — rethrow it, never swallow it.
Common Pitfalls
- Catching
Exceptionbroadly and accidentally swallowingCancellationException, breaking structured concurrency. - Using
GlobalScope.launchwhich leaks coroutines and ignores lifecycle. - Collecting a
FlowinonCreatewithout lifecycle awareness, causing collection to continue when the app is in the background. - Forgetting
flowOn— operators beforeflowOnrun on the specified dispatcher, but collection still happens on the collector's dispatcher. - Creating a new
StateFloworSharedFlowon every recomposition instead of hoisting it in the ViewModel. - Using
runBlockingon the main thread, causing an ANR.
Install this skill directly: skilldb add android-kotlin-skills
Related Skills
Architecture
MVVM architecture pattern with ViewModel, LiveData, and StateFlow for scalable Android apps
Dependency Injection
Hilt and Dagger dependency injection for managing object creation and scoping in Android apps
Jetpack Compose
Jetpack Compose declarative UI toolkit for building native Android interfaces with Kotlin
Navigation
Jetpack Navigation component for type-safe in-app navigation and deep linking in Android
Retrofit
Retrofit HTTP client for type-safe REST API communication in Android with Kotlin coroutines
Room Database
Room persistence library for local SQLite database access with compile-time query verification in Android