From kotlin
Design Kotlin coroutine and Flow APIs with explicit ownership, honest async semantics, and cancellation-safe behavior. Use this skill when the user asks to "use coroutines", "design a suspend API", "choose Flow vs suspend", "debug cancellation", "review Kotlin async code", or needs guidance on Kotlin coroutine and Flow patterns.
npx claudepluginhub ririnto/sinon --plugin kotlinThis skill uses the workspace's default tool permissions.
Design Kotlin coroutine and Flow code with honest async semantics, explicit ownership, and cancellation-safe behavior.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Design Kotlin coroutine and Flow code with honest async semantics, explicit ownership, and cancellation-safe behavior.
Minimum Kotlin version: 1.9 -- examples use kotlinx.coroutines APIs stable since 1.6 (limitedParallelism, callbackFlow), kotlinx.coroutines.test APIs from 1.7+ (runTest, TestDispatcher), and Flow operators available since 1.5. The kotlinx-coroutines library version is managed through the project's dependency catalog; use 1.10.2 or later for full API coverage shown here. Start with the smallest shape that matches the contract, then open a blocker reference only when scope, failure behavior, hot sharing, or concurrent mutation becomes the real problem.
suspend for one logical async result.Flow only when the contract delivers values over time.CoroutineScope.CancellationException propagate.Flow as cold and sequential unless sharing or buffering is chosen intentionally.StateFlow for current state and SharedFlow for events or broadcasts.launch for fire-and-forget work and async only when the caller awaits the result.CoroutineExceptionHandler only at a root scope or direct coroutine builder, never on child scopes.flow { } sequential and free of external context-switching calls.MutableStateFlow.update { } for atomic state transitions.GlobalScope, GlobalScope.launch, and detached work unless explicitly about background ownership.suspend for one-shot work and switch to Flow only if the contract is truly streaming.suspend vs FlowChoose suspend when the operation produces one logical answer and then completes.
suspend fun loadOrder(orderId: OrderId): Order = repository.load(orderId)
Choose Flow when the contract is ongoing observation, repeated updates, or incremental delivery over time.
fun observeOrders(): Flow<List<Order>> = repository.observeOrders()
launch vs asyncUse launch for fire-and-forget work where the caller does not need the result. The presenter pattern is the canonical example: the UI triggers an action and moves on.
class OrdersPresenter(private val presenterScope: CoroutineScope) {
fun refresh() {
presenterScope.launch {
repository.refresh()
}
}
}
Use async only when the caller must await and compose results from multiple parallel operations. Always call await; uncaught exceptions in orphaned async propagate as unhandled errors.
suspend fun loadOrderWithItems(orderId: OrderId): Pair<Order, List<Item>> =
coroutineScope {
val orderDeferred = async { repository.loadOrder(orderId) }
val itemsDeferred = async { repository.loadItems(orderId) }
orderDeferred.await() to itemsDeferred.await()
}
Flow vs hot state or event streamsUse ordinary Flow as the default streaming type. It is usually cold, so each collection starts the upstream work again unless you share it intentionally.
Use StateFlow when every collector should immediately see the latest state. StateFlow always conflates -- fast writers drop intermediate values so collectors see at most the most recent emission.
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
fun markLoaded(orders: List<Order>) {
_uiState.update { UiState.Success(orders) }
}
Use SharedFlow when the stream represents events or broadcasts and replay must be chosen explicitly. Configure buffer capacity and overflow policy to match the event volume.
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val events: SharedFlow<UiEvent> = _events
Keep launched work attached to a visible owner. The presenter pattern (shown above under "launch vs async") is the canonical form. For service classes that own periodic or lifecycle-independent work, inject the scope:
class OrderSyncService(private val syncScope: CoroutineScope) {
fun startPeriodicSync() {
syncScope.launch {
while (isActive) {
sync()
delay(60_000L)
}
}
}
}
Keep blocking I/O boundaries explicit with Dispatchers.IO.
import java.nio.file.Path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class CsvImporter {
suspend fun import(path: Path): ImportResult = withContext(Dispatchers.IO) {
parser.import(path)
}
}
Keep CPU-heavy computation boundaries explicit with Dispatchers.Default.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ReportGenerator {
suspend fun generate(rawData: RawData): Report = withContext(Dispatchers.Default) {
heavyComputation(rawData)
}
}
Build transform chains with basic operators. Keep chains readable by grouping related transforms together.
repository.observeOrders()
.filter { it.status == Status.ACTIVE }
.map { it.toDisplayModel() }
.distinctUntilChanged()
.collect { model -> render(model) }
Use flowOf(...) for constant flows, emptyFlow() for completed flows, and .asFlow() to convert collections:
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
val single = flowOf(OrderId("1"))
val none: Flow<Order> = emptyFlow()
val fromList = listOf(1, 2, 3).asFlow()
Use onEach to inject side effects (logging, metrics) into a Flow chain without breaking the reactive style:
orders
.onEach { order -> log.debug("Processing order ${order.id}") }
.map { it.toDisplayModel() }
.collect { model -> render(model) }
Use launchIn(scope) as the idiomatic alternative to scope.launch { flow.collect {} } for collecting a flow into an external scope (common in UI code):
viewModel.orders
.onEach { orders -> render(orders) }
.launchIn(viewModelScope)
Handle errors at the Flow level using catch and retry, not by wrapping collect in try/catch. The catch operator intercepts upstream exceptions before they reach the collector; retry re-subscribes the flow on failure.
repository.observeOrders()
.retry(3) { it is IOException }
.catch { e -> emit(FallbackOrderList) }
.collect { orders -> render(orders) }
If you are unsure, start here:
class OrderLoader(private val repository: OrderRepository) {
suspend fun load(orderId: OrderId): Order = repository.load(orderId)
}
Only add Flow, extra scopes, sharing, or buffering when the contract clearly needs them.
Check these pass/fail conditions before you stop:
suspend instead of a decorative streamlaunch is used for fire-and-forget and async only when the result is awaitedGlobalScope is not used anywhereCoroutineExceptionHandler is installed only at root scope or direct coroutine builderflow { } is sequential with no context-switching callsStateFlow and SharedFlow are chosen for clear state or event semantics; when using SharedFlow, imports include BufferOverflowcatch/retry operators instead of wrapping collect in try/catchflowOf/asFlow/emptyFlow() for constants, flow {} for custom logic, callbackFlow for callback bridgingonEach; collection into external scopes uses launchIn| Anti-pattern | Why it fails | Correct move |
|---|---|---|
returning Flow for a single result | the API looks reactive without changing the contract | use suspend |
| launching work without a visible owner | lifecycle and cancellation become ambiguous | attach work to an explicit parent scope |
swallowing CancellationException | structured cancellation silently breaks | let cancellation propagate and clean up in finally |
| sharing or buffering a flow by default | delivery semantics become harder to reason about | keep the flow cold and sequential until sharing is required |
| mutating shared state from multiple coroutines without a rule | race conditions become hidden design bugs | confine the state or protect it deliberately |
using GlobalScope | escapes structured concurrency; work cannot be cancelled as a group | inject CoroutineScope |
catching broad Exception in coroutine body | catches CancellationException and breaks cancellation | catch specific exceptions or rethrow CancellationException |
using async without await | uncaught exceptions propagate as unhandled errors | use launch for fire-and-forget |
installing CoroutineExceptionHandler on child scope | child handlers do not catch sibling failures | install only at root scope or direct builder |
calling withContext inside flow { } | violates context-preservation invariant of Flow | move the context switch to flowOn() |
assuming StateFlow emits every value | StateFlow conflates fast updates; intermediate values are dropped | use SharedFlow if every value matters |
Return:
Open these only when the named blocker is the real issue.
| Open this when... | Read... |
|---|---|
you need coroutineScope, supervisorScope, explicit launch ownership, or dispatcher boundaries | ./references/scope-ownership-and-dispatchers.md |
| you are debugging cancellation, timeouts, failure propagation, or cleanup semantics | ./references/cancellation-timeouts-and-failures.md |
you need to justify Flow, choose StateFlow or SharedFlow, or shape hot sharing and buffering | ./references/flow-selection-hot-sharing-and-buffering.md |
you are coordinating mutable state across coroutines, or need fan-in/fan-out, Channel handoff, work queues, or select expressions | ./references/shared-state-and-concurrency.md |
| you are writing or debugging tests for coroutine or Flow code | ./references/testing.md |
Use this skill for coroutine structure, suspend versus Flow, cancellation-aware async design, hot or cold stream choices, and shared-state decisions directly caused by coroutine usage.
Do not use this skill as the primary source for general Kotlin language modeling, Kotlin test structure, framework-specific reactive APIs, Android architecture guidance, or JVM runtime internals.