---
name: android-kotlin-coroutines-pro
description: "Idiomatic structured concurrency: coroutines, Flow, channels, error handling, testing — done right. Writes correct coroutine code that doesn't leak — scoped jobs, supervisorScope, hot/cold flows, backpressure, and Turbine tests."
license: MIT
---

# Kotlin Coroutines & Flow Pro

Structured concurrency is easy to get wrong: a coroutine launched in the wrong scope outlives the screen that started it, a thrown exception cancels siblings you wanted to keep alive, and a hot `StateFlow` collected without `repeatOnLifecycle` quietly leaks. This skill writes coroutine and Flow code that respects cancellation, scopes jobs correctly, and ships with deterministic tests. It's for Android and Kotlin backend engineers who want concurrency that behaves under failure, not just on the happy path.

## When to use it

- Wiring repository or use-case layers that combine network and DB streams.
- Deciding between `coroutineScope` and `supervisorScope`, or `launch` vs `async`.
- Converting callbacks to suspend functions with `suspendCancellableCoroutine`.
- Turning a cold `flow {}` into a shared hot stream (`shareIn` / `stateIn`) without leaking the upstream.
- Writing tests that don't sleep — using `runTest`, `StandardTestDispatcher`, and Turbine.

## What it does

It applies the real coroutines APIs idiomatically: cancellation-cooperative loops (`ensureActive`, `yield`, `isActive`), exception strategy via `CoroutineExceptionHandler` and `supervisorScope`, and structured teardown so child jobs join their parent. For Flow it knows the cold-vs-hot distinction, backpressure operators (`buffer`, `conflate`, `collectLatest`, `flowOn`), `callbackFlow` with `awaitClose`, and sharing with `SharingStarted.WhileSubscribed(5_000)`. Tests use `kotlinx-coroutines-test` (`runTest`, `advanceUntilIdle`, `TestDispatcher`) and Turbine's `flow.test { awaitItem(); awaitComplete() }`.

## Usage

```kotlin
fun userStream(id: String): Flow<User> = callbackFlow {
    val reg = api.observe(id) { trySend(it) }
    awaitClose { reg.cancel() }          // no leak on cancellation
}.flowOn(Dispatchers.IO)

// Turbine test
@Test fun emits() = runTest {
    repo.userStream("1").test {
        assertEquals(expected, awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}
```

## Tips & gotchas

- `withContext(Dispatchers.IO)` switches threads but does **not** create a new failure boundary; wrap independent children in `supervisorScope` if one failing shouldn't cancel the rest.
- `GlobalScope` is almost always a bug — pass an injected scope (`viewModelScope`, a custom `CoroutineScope`) so work is tied to a lifecycle.
- Catch *around* the Flow with `.catch {}`, not inside `collect` — a `try/catch` in the collector swallows `CancellationException` and breaks structured cancellation.
- Prefer `WhileSubscribed(5_000)` over `Eagerly`/`Lazily` for UI state so the upstream stops shortly after the last collector leaves.

## FAQ

**`launch` or `async`?** `launch` for fire-and-forget side effects; `async` only when you need a `Deferred` result and will `await` it. A lone `async` whose exception is never awaited can be lost.

**Why does my test hang?** You're likely on `StandardTestDispatcher` without advancing time — call `advanceUntilIdle()`, or use `UnconfinedTestDispatcher` for eager execution.

**Does it cover Compose?** Yes — `collectAsStateWithLifecycle()` and `LaunchedEffect`/`rememberCoroutineScope` patterns are included.
