# Kotlin Coroutines

> When to activate: Kotlin coroutines, suspend functions, Flow, Channel, StateFlow, SharedFlow, structured concurrency, async, launch, coroutineScope

- Skill: `mattakushi432/kotlin-coroutines` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/kotlin-coroutines`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/kotlin-coroutines/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/kotlin-coroutines

---

# Kotlin Coroutines & Flow

## Basic Coroutine Builders

```kotlin
// launch — fire and forget
viewModelScope.launch {
    repository.refresh()
}

// async — concurrent work with result
val deferred: Deferred<User> = async { userApi.getUser(id) }
val user = deferred.await()

// Parallel decomposition
coroutineScope {
    val user = async { userRepo.findById(id) }
    val orders = async { orderRepo.findByUserId(id) }
    UserWithOrders(user.await(), orders.await())
}

// withContext — switch dispatcher
val result = withContext(Dispatchers.IO) {
    File("data.txt").readText()
}
```

## Structured Concurrency

```kotlin
class UserService(private val scope: CoroutineScope) {

    // Child coroutines are cancelled when scope is cancelled
    fun startSync() = scope.launch {
        while (isActive) {
            try {
                syncData()
                delay(60_000)
            } catch (e: CancellationException) {
                throw e  // always rethrow CancellationException
            } catch (e: Exception) {
                log.warn("Sync failed", e)
            }
        }
    }

    // supervisorScope — child failures don't cancel siblings
    suspend fun fetchBatch(ids: List<Long>): List<Result<User>> =
        supervisorScope {
            ids.map { id ->
                async {
                    runCatching { userApi.getUser(id) }
                }
            }.awaitAll()
        }
}
```

## Flow Basics

```kotlin
// Cold flow — executed per collector
fun userFlow(id: Long): Flow<User> = flow {
    while (true) {
        emit(userRepo.findById(id))
        delay(5_000)
    }
}

// Operators
userFlow(id)
    .filter { it.isActive }
    .map { it.toDto() }
    .catch { e -> emit(User.empty()) }
    .onEach { log.debug("Got user: $it") }
    .collect { render(it) }

// flowOn — upstream runs on given dispatcher
repository.observeAll()
    .map { transform(it) }
    .flowOn(Dispatchers.Default)
    .collect { updateUi(it) }
```

## StateFlow & SharedFlow

```kotlin
class SearchViewModel : ViewModel() {

    // StateFlow — always has value, replays last to new collectors
    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query.asStateFlow()

    // SharedFlow — hot, no initial value, configurable replay
    private val _events = MutableSharedFlow<UiEvent>(extraBufferCapacity = 16)
    val events: SharedFlow<UiEvent> = _events.asSharedFlow()

    val results: StateFlow<List<SearchResult>> = query
        .debounce(300)
        .distinctUntilChanged()
        .filter { it.length >= 2 }
        .flatMapLatest { searchRepo.search(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

    fun onQueryChange(q: String) { _query.value = q }

    fun onItemClick(item: SearchResult) {
        viewModelScope.launch {
            _events.emit(UiEvent.Navigate(item.id))
        }
    }
}
```

## Channel

```kotlin
// Channel for producer-consumer
val channel = Channel<Work>(capacity = 64)

// Producer
launch {
    workItems.forEach { channel.send(it) }
    channel.close()
}

// Consumer
launch {
    for (work in channel) {
        process(work)
    }
}

// callbackFlow — bridge callback APIs to Flow
fun locationUpdates(): Flow<Location> = callbackFlow {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            trySend(result.lastLocation ?: return)
        }
    }
    locationClient.requestUpdates(callback)
    awaitClose { locationClient.removeUpdates(callback) }
}
```

## Exception Handling

```kotlin
// CoroutineExceptionHandler — only for top-level launch
val handler = CoroutineExceptionHandler { _, exception ->
    log.error("Unhandled coroutine exception", exception)
}
scope.launch(handler) { riskyWork() }

// try/catch inside suspend functions
suspend fun safeLoad(id: Long): User? = try {
    api.getUser(id)
} catch (e: HttpException) {
    null
}

// runCatching — functional error handling
val result = runCatching { api.getUser(id) }
    .getOrElse { User.guest() }
```

## Testing Coroutines

```kotlin
class UserViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun `search updates results`() = runTest {
        val vm = SearchViewModel(fakeRepo)
        vm.onQueryChange("kotlin")
        advanceTimeBy(400) // past debounce
        assertEquals(expectedResults, vm.results.value)
    }
}

// MainDispatcherRule
class MainDispatcherRule : TestWatcher() {
    val dispatcher = StandardTestDispatcher()
    override fun starting(d: Description?) = Dispatchers.setMain(dispatcher)
    override fun finished(d: Description?) = Dispatchers.resetMain()
}
```

## Key Rules
- Always rethrow `CancellationException` — catching it silently breaks structured concurrency
- Use `viewModelScope`/`lifecycleScope` in Android; create `CoroutineScope(SupervisorJob() + Dispatchers.Default)` in plain Kotlin
- `StateFlow` for UI state; `SharedFlow` for one-time events; `Channel` for work queues
- `stateIn(WhileSubscribed(5000))` prevents restarting upstream on config change
- Never do blocking I/O on `Dispatchers.Main` — always `withContext(Dispatchers.IO)`

