Android path · The process · Topic 4 of 12

Coroutines and Flow on Android

Not started

A coroutine is a computation that can pause at a suspend point without holding a thread. On Android that matters because the thread you start on is usually the main thread: while a coroutine waits for a response, the main Looper keeps drawing frames and handling touches. The code still reads top to bottom.

Why it matters: Coroutines let you write sequential code that leaves the main thread free while it waits. Scopes make sure that work stops when the screen or ViewModel it belongs to goes away.

  1. 1Suspending frees the thread

    A coroutine waiting on the network gives its thread back; withContext moves blocking work to another dispatcher and returns.

  2. 2Structured concurrency

    Every coroutine lives in a scope. Cancel the scope (a cleared ViewModel, a destroyed screen) and all its children are cancelled.

  3. 3Collect only while visible

    Use repeatOnLifecycle or collectAsStateWithLifecycle so flows stop collecting when the UI is in the background.

1

How it works

The explanation, with examples you can run.

Dispatchers decide the thread

class ArticleRepository(    private val api: ArticleApi,    private val io: CoroutineDispatcher = Dispatchers.IO,   // inject it so tests can replace it) {    suspend fun load(id: String): Article = withContext(io) {        val raw = api.fetch(id)            // blocking call: fine here, we're on IO        parse(raw)    }}
  • Dispatchers.Main: the main thread. viewModelScope and lifecycleScope use it (strictly, Dispatchers.Main.immediate).
  • Dispatchers.IO: a large pool for blocking I/O.
  • Dispatchers.Default: one thread per core, for CPU work like sorting or parsing.

The convention is that suspend functions are main-safe: the function that blocks is the one that switches. Libraries like Retrofit and Room already do this for their suspend APIs.

Structured concurrency

Coroutines are launched in a CoroutineScope, and child coroutines belong to their parent. Cancel the scope and everything in it is cancelled; if a child fails, by default the failure cancels its siblings and the parent too. On Android the scopes come with the objects that own the work:

class ArticleViewModel(private val repo: ArticleRepository) : ViewModel() {    private val _state = MutableStateFlow<ArticleState>(ArticleState.Loading)    val state: StateFlow<ArticleState> = _state     fun open(id: String) {        viewModelScope.launch {                       // cancelled in onCleared()            _state.value = try {                ArticleState.Loaded(repo.load(id))            } catch (e: IOException) {                ArticleState.Failed            }        }    }}

Avoid GlobalScope: nothing cancels it, and it can't be controlled in tests. Work that must outlive the screen goes to WorkManager or an application-level scope you create and inject. Cancellation is cooperative: suspending calls check for it, but a tight CPU loop should call ensureActive() now and then, and catching CancellationException without rethrowing breaks it.

Flow: values over time

A Flow is a cold stream: nothing runs until someone collects it. StateFlow always has a current value and is the usual way a ViewModel exposes UI state; SharedFlow broadcasts events to any number of collectors.

val articles: StateFlow<List<Article>> = dao.observeAll()     // a cold Flow from Room    .map { rows -> rows.map(::toArticle) }    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

WhileSubscribed(5_000) keeps the upstream alive for five seconds after the last collector leaves, long enough to survive a rotation without restarting the query.

Collecting safely

Collect in the UI only while it is visible:

// Compose (lifecycle-runtime-compose)val list by viewModel.articles.collectAsStateWithLifecycle() // ViewslifecycleScope.launch {    repeatOnLifecycle(Lifecycle.State.STARTED) {        viewModel.articles.collect { adapter.submitList(it) }    }}

Both stop collecting below STARTED and resume above it, so a backgrounded app isn't doing UI work for nobody.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A suspend function calls a blocking JDBC-style API directly. Is it main-safe?
  2. 2.What happens to coroutines launched in viewModelScope when the user rotates the screen?
  3. 3.Why is lifecycleScope.launch { flow.collect { ... } } in onCreate a problem?

0 of 3 right so far. Get all three to finish this topic.