kotlin-coroutines
Skill18 starsChanged 4 months agoMITView on GitHub
Kotlin coroutines for Android/KMP — suspend functions, cancellation safety, structured concurrency, parallel execution, synchronization, scopes, blocking code wrapping, and cooperative cancellation. Use this skill whenever writing or reviewing coroutines, suspend functions, coroutine scopes, or any asynchronous Kotlin code that is NOT primarily about Flows. Trigger on phrases like "coroutine", "suspend", "async", "launch", "withContext", "Dispatchers", "ensureActive", "NonCancellable", "Mutex", "limitedParallelism", "coroutineScope", "supervisorScope", "viewModelScope", "application scope", "parallel execution", "blocking code", "runBlocking", "cancellation", or "yield".
3 other projects use a version of this.
Adopted by
Most recently taken up by
- full-stack-skills/kotlin-skillstheir own version, first seen Oct 13, 2026
- vitorpamplona/amethysttheir own version, first seen Oct 8, 2026
- javiercamarenatriguero/android-skillstheir own version, first seen Jul 2, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- vitorpamplona/amethyst★ 1.6ksame name
- javiercamarenatriguero/android-skills★ 8same name
- full-stack-skills/kotlin-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository67 other files it gives its agents.
- agent-onboardingSkill
- android-backgroundSkill
- android-cliSkill
- android-compose-architectureSkill
- android-compose-componentsSkill
- android-data-layerSkill
- android-di-koinSkill
- android-error-handlingSkill
- android-integration-testingSkill
- android-jacoco-setupSkill
- android-jvm-test-coverageSkill
- android-module-structureSkill
- android-navigationSkill
- android-presentation-mviSkill
- android-preview-figma-verifySkill
- android-ui-screenshot-verifySkill
- android-ui-testingSkill
- android-ui-verifySkill
- android-unit-testingSkill
- android-version-catalogSkill
- architecture-decision-recordSkill
- careful-planningSkill
- compose-adaptive-layoutsSkill
- figma-to-android-vectorSkill
- finalizeSkill
- git-commitSkill
- handoffSkill
- kmp-compose-architectureSkill
- kmp-compose-componentsSkill
- kmp-data-layerSkill
- kmp-di-koinSkill
- kmp-error-handlingSkill
- kmp-integration-testingSkill
- kmp-migration-analyzerSkill
- kmp-module-structureSkill
- kmp-navigationSkill
- kmp-presentation-mviSkill
- kmp-ui-testingSkill
- kmp-unit-testingSkill
- kotlin-flowsSkill
- kotlin-mutation-testingSkill
- openspec-artifact-review-lightSkill
- openspec-artifact-reviewSkill
- project-breakdownSkill
- react-data-layerSkill
- react-error-handlingSkill
- react-integration-testingSkill
- react-presentation-mviSkill
- react-project-structureSkill
- react-routingSkill
- react-screen-architectureSkill
- react-securitySkill
- react-server-stateSkill
- react-ui-componentsSkill
- react-unit-testingSkill
- speckit-parallelize-agent-teamsSkill
- speckit-parallelize-subagentsSkill
- spring-api-layerSkill
- spring-di-configurationSkill
- spring-integration-testingSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 3 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
When catching Exception or Throwable in a suspend context that contains multiple sequential suspend calls, always call …
- Very specific: line 43
If the catch block only contains a single suspend call or no suspend calls at all, ensureActive() is still good practice but less critical.
- Very specific: line 94
Never: use withContext(NonCancellable) for business logic or multi-step operations — it prevents the coroutine from being cancelled during …
- Distinctive: line 100
Rule: Never call blocking code directly in a suspend function or coroutine.
- Very specific: line 125
Never use
Thread.sleep()in a coroutine: Use delay() instead — it suspends without blocking the thread. - Very specific: line 131
Rule: Never execute two independent suspend functions sequentially.
- Distinctive: line 135
If one fails, all siblings are cancelled:
- Distinctive: line 153
If one fails, the others continue:
- Very specific: line 240
Use Dispatchers.IO.limitedParallelism(1) for I/O-bound confined work, Dispatchers.Default.limitedParallelism(1) for CPU-bound confined work.
- Very specific: line 254
Create in the Application class and inject via Koin:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Kotlin Coroutines (Android / KMP)
For Flow types, operators, stateIn/shareIn, backpressure, and lifecycle-aware collection, see kotlin-flows.
Cancellation Safety: ensureActive()
When catching Exception or Throwable in a suspend context that contains multiple sequential suspend calls, always call coroutineContext.ensureActive() as the first line of the catch block. This prevents silently swallowing CancellationException, which would leave the coroutine running after cancellation and cause inconsistent state.
Correct:
suspend fun syncData() {
try {
val data = remoteDataSource.fetch() // suspend call 1
localDataSource.insert(data) // suspend call 2
} catch (e: Exception) {
coroutineContext.ensureActive() // ← FIRST LINE — rethrows if cancelled
// Handle the actual error (network, DB, etc.)
logger.error("Sync failed", e)
}
}Wrong — swallows CancellationException:
suspend fun syncData() {
try {
val data = remoteDataSource.fetch()
localDataSource.insert(data)
} catch (e: Exception) {
// BUG: If cancellation happened between fetch() and insert(),
// CancellationException is caught here and silently swallowed.
logger.error("Sync failed", e)
}
}If the catch block only contains a single suspend call or no suspend calls at all, ensureActive() is still good practice but less critical.
NonCancellable and Atomic Operations
| Scenario | Approach |
| Cleanup work (close resource, flush log) | withContext(NonCancellable) { ... } |
| Multi-step suspend calls that must all complete | Inject applicationScope, launch in it |
| Multiple database writes that must be atomic | Room withTransaction { } |
Cleanup — NonCancellable:
suspend fun disconnect() {
withContext(NonCancellable) {
socket.sendCloseFrame()
socket.awaitClose()
}
}Multi-step atomic — application scope:
class SyncRepository(
private val applicationScope: CoroutineScope,
private val remoteDataSource: RemoteDataSource,
private val localDataSource: LocalDataSource
) {
suspend fun syncAndNotify() {
// Runs independently of the caller's lifecycle.
// If the ViewModel is cleared mid-sync, this still completes.
applicationScope.launch {
val data = remoteDataSource.fetch()
localDataSource.insert(data)
notificationService.notifyComplete()
}.join() // optional: suspend until done
}
}Database transaction — preferred over application scope for DB:
suspend fun moveNote(noteId: String, targetFolderId: String) {
db.withTransaction {
noteDao.removeFromCurrentFolder(noteId)
noteDao.insertIntoFolder(noteId, targetFolderId)
}
}Never use withContext(NonCancellable) for business logic or multi-step operations — it prevents the coroutine from being cancelled during that block, which is only appropriate for short cleanup work.
Blocking Code and Main Safety
Rule: Never call blocking code directly in a suspend function or coroutine. Always wrap it in a dedicated class that exposes a suspend function with the correct dispatcher.
| Work type | Dispatcher |
| File I/O, network, database, stream reads/writes | Dispatchers.IO |
| CPU-heavy (image compression, JSON parsing, sorting large lists) | Dispatchers.Default |
class FileReader(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
suspend fun readFile(path: Path): ByteArray = withContext(ioDispatcher) {
path.toFile().readBytes() // blocking call — safe inside withContext(IO)
}
}class ImageCompressor(private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default) {
suspend fun compress(bitmap: Bitmap, quality: Int): ByteArray = withContext(defaultDispatcher) {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
stream.toByteArray()
}
}Never use Thread.sleep() in a coroutine. Use delay() instead — it suspends without blocking the thread.
Parallel Execution
Rule: Never execute two independent suspend functions sequentially. Use async or separate launch blocks.
Fail-fast — coroutineScope + async
If one fails, all siblings are cancelled:
suspend fun loadDashboard(): Dashboard = coroutineScope {
val userDeferred = async { userRepository.getUser() }
val statsDeferred = async { statsRepository.getStats() }
val notificationsDeferred = async { notificationRepository.getRecent() }
Dashboard(
user = userDeferred.await(),
stats = statsDeferred.await(),
notifications = notificationsDeferred.await()
)
}Independent — supervisorScope + async
If one fails, the others continue:
suspend fun loadDashboard(): Dashboard = supervisorScope {
val userDeferred = async { userRepository.getUser() }
val statsDeferred = async {
try {
statsRepository.getStats()
} catch (e: Exception) {
coroutineContext.ensureActive()
null // stats are optional
}
}
Dashboard(
user = userDeferred.await(),
stats = statsDeferred.await()
)
}awaitAll for collecting results
suspend fun fetchAllPages(urls: List<String>): List<Page> = coroutineScope {
urls.map { url ->
async { api.fetchPage(url) }
}.awaitAll()
}Separate launch blocks — when return values aren't needed
viewModelScope.launch {
launch { analyticsTracker.trackScreenView("profile") }
launch { prefetchService.prefetchRelatedData(userId) }
// Both run in parallel, neither blocks the other
}Synchronization
| Need | Tool |
| Protect a critical section of suspend code | Mutex |
| Confine all execution (including non-suspend) to one thread | limitedParallelism(1) |
Mutex — for suspend-only critical sections
class InMemoryCache<K, V> {
private val mutex = Mutex()
private val map = mutableMapOf<K, V>()
suspend fun getOrPut(key: K, compute: suspend () -> V): V {
mutex.withLock {
map[key]?.let { return it }
val value = compute()
map[key] = value
return value
}
}
}limitedParallelism(1) — for thread confinement
class TokenStorage(
private val prefs: SharedPreferences
) {
// Single-thread dispatcher — safe for non-suspend SharedPreferences calls
private val dispatcher = Dispatchers.IO.limitedParallelism(1)
suspend fun saveToken(token: String) = withContext(dispatcher) {
prefs.edit().putString("token", token).commit()
}
suspend fun getToken(): String? = withContext(dispatcher) {
prefs.getString("token", null)
}
}Use Dispatchers.IO.limitedParallelism(1) for I/O-bound confined work, Dispatchers.Default.limitedParallelism(1) for CPU-bound confined work.
Custom Scopes and Application Scope
Custom coroutine scopes should use SupervisorJob() so one child failure doesn't cancel the entire scope:
val myScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)Application scope — Android / KMP
Create in the Application class and inject via Koin:
class MyApp : Application() {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
}// Koin module
val appModule = module {
single<CoroutineScope>(named("applicationScope")) {
(androidApplication() as MyApp).applicationScope
}
}// Inject where needed
class SyncRepository(
@Named("applicationScope") private val applicationScope: CoroutineScope,
private val api: Api,
private val dao: NoteDao
) {
suspend fun syncNotes() {
applicationScope.launch {
val notes = api.fetchNotes()
dao.upsertAll(notes)
}.join()
}
}When to use application scope: Multi-step operations that must outlive the ViewModel or screen lifecycle (e.g., sync that started from a screen but must finish even if the user navigates away). If the multi-step work is purely database operations, prefer a Room transaction instead.
Cooperative Cancellation
ensureActive() — cheap check in loops
suspend fun processItems(items: List<Item>) {
for (item in items) {
ensureActive() // throws CancellationException if cancelled — cheap, no thread yield
processItem(item)
}
}yield() — when converting blocking code to suspend
suspend fun processLargeFile(file: File) = withContext(Dispatchers.IO) {
file.bufferedReader().useLines { lines ->
lines.forEach { line ->
yield() // checks cancellation AND yields the thread to other coroutines
parseLine(line)
}
}
}Pick ensureActive() for loops (cheaper — just checks a flag). Pick yield() when wrapping blocking code (yields the thread so other coroutines on the same dispatcher can run).
runBlocking
runBlocking is allowed only in:
- Tests —
runTestis preferred, butrunBlockingworks for simple cases - Plain Kotlin
main()function — entry point that bridges to suspend world - Contexts where coroutines can't be launched AND you're certain it's a background thread — e.g., a Retrofit
Interceptor(OkHttp guarantees this runs on its own thread pool)
// Retrofit interceptor — OK because OkHttp runs interceptors on background threads
class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = runBlocking { tokenProvider.getToken() }
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}NEVER use runBlocking:
- On the Android main thread — it blocks the UI
- Inside an existing coroutine — it blocks the thread and can cause deadlocks
- In any Android production code where a coroutine scope is available
Testing
See android-testing skill for full coroutine test patterns including runTest, Turbine, UnconfinedTestDispatcher, and Dispatchers.setMain().
Testability rule: Inject CoroutineDispatcher in any class that dispatches to a non-Main dispatcher and is unit-tested. This lets tests replace Dispatchers.IO / Dispatchers.Default with a test dispatcher:
class NoteRepository(
private val api: NoteApi,
private val dao: NoteDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun refreshNotes() = withContext(ioDispatcher) {
val notes = api.fetchNotes()
dao.upsertAll(notes)
}
}Common Mistakes
| Mistake | Fix |
Catching Exception without ensureActive() | Add coroutineContext.ensureActive() as first catch line |
| Sequential independent suspend calls | async + awaitAll or parallel launch |
Thread.sleep() in coroutine | delay() |
withContext(NonCancellable) for business logic | Use application scope or DB transaction |
runBlocking on main thread | viewModelScope.launch or proper scope |
GlobalScope.launch | Inject application scope via DI |
Checklist: Writing Coroutine Code
ensureActive()is the first line in everycatch(e: Exception)/catch(e: Throwable)in suspend context with sequential suspend calls- No blocking calls without
withContext(Dispatchers.IO)orwithContext(Dispatchers.Default) - Independent suspend calls run in parallel (
async/launch), never sequential NonCancellableused only for cleanup — not business logic- Multi-step DB operations use transactions, not application scope
- No
runBlockingon main thread or inside coroutines delay()used instead ofThread.sleep()in all coroutine contexts- Synchronization uses the correct tool (Mutex for suspend-only,
limitedParallelism(1)for thread confinement) - Custom scopes use
SupervisorJob()+ appropriate dispatcher - No
GlobalScope— use injected application scope - Loops call
ensureActive()for cooperative cancellation - Blocking-to-suspend conversions use
yield()for cooperative cancellation
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

