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

DispatcherThread poolUse for
Dispatchers.MainMain/UI threadUI updates, LiveData, Compose state
Dispatchers.IOLarge shared poolNetwork requests, Room DB, file I/O
Dispatchers.DefaultCPU core poolJSON parsing, sorting, heavy computation
Dispatchers.UnconfinedCurrent threadTesting 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)
        }
    }
}