Chapter 10 of 12
Key Chapter
~22 min read
Coroutines ⭐
Kotlin's answer to async programming — lighter than threads, cleaner than callbacks, sequential-looking code that's actually async.
The Problem — Java's Async Approaches
Java — Thread or AsyncTask
// Raw Thread — verbose, no lifecycle
new Thread(() -> {
String data = fetchFromNetwork(); // blocks thread
runOnUiThread(() -> {
textView.setText(data);
});
}).start();
// Callback hell:
api.getUser(id, user -> {
api.getPosts(user.id, posts -> {
api.getComments(posts.get(0).id, comments -> {
// deeply nested...
});
});
}); Kotlin Coroutines
// In ViewModel — sequential-looking async!
viewModelScope.launch {
// Background thread (non-blocking)
val user = withContext(Dispatchers.IO) {
api.getUser(id)
}
// Back on Main thread automatically
textView.text = user.name
}
// Chain calls — no pyramid of doom
viewModelScope.launch {
val user = api.getUser(id) // sequential
val posts = api.getPosts(user.id) // awaits user
val comments = api.getComments(posts[0].id)
// all sequential, no callbacks!
}suspend Functions
A suspend function can be paused and resumed without blocking a thread. It can only be called from another suspend function or a coroutine:
Kotlin
// Mark with 'suspend' — can pause without blocking
suspend fun fetchUser(id: Int): User {
delay(1000) // non-blocking wait
return api.getUser(id)
}
// Must be called from a coroutine or other suspend fun:
viewModelScope.launch {
val user = fetchUser(42) // suspends, doesn't block!
println(user.name)
}Dispatchers — Where Code Runs
| Dispatcher | Thread pool | Use for |
|---|---|---|
Dispatchers.Main | Main/UI thread | UI updates, LiveData, Compose state |
Dispatchers.IO | Large shared pool | Network requests, Room DB, file I/O |
Dispatchers.Default | CPU core pool | JSON parsing, sorting, heavy computation |
Dispatchers.Unconfined | Current thread | Testing only — avoid in production |
Kotlin — withContext to switch dispatchers
viewModelScope.launch { // starts on Dispatchers.Main
_uiState.value = UiState.Loading
val result = withContext(Dispatchers.IO) {
// Runs on background thread
repository.fetchData()
}
// Returns to Main automatically!
_uiState.value = UiState.Success(result)
}launch vs async
launch — fire and forget
// Returns Job — no result
val job = viewModelScope.launch {
doSomeWork()
}
job.cancel() // can cancel
job.join() // wait for completion async — parallel with result
// Returns Deferred<T> — has result
viewModelScope.launch {
// Run in parallel!
val userDeferred = async { api.getUser() }
val statsDeferred = async { api.getStats() }
// Await both results
val user = userDeferred.await()
val stats = statsDeferred.await()
// Both ran simultaneously!
}Coroutine Scopes in Android
Kotlin Android scopes
// In ViewModel — automatically cancelled when VM is cleared
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch { /* ... */ }
}
}
// In Composable — tied to composition
@Composable
fun MyScreen() {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch { /* ... */ }
}) { Text("Load") }
}
// In Fragment/Activity — tied to lifecycle
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}