Use when kotlin coroutines for structured concurrency including suspend functions, coroutine builders, Flow, channels, and patterns for building efficient asynchronous code with cancellation and exception handling.
Read-only skill
Additional assets for this skill
This skill cannot use any tools. It operates in read-only mode without the ability to modify files or execute commands.
Kotlin coroutines provide a powerful framework for asynchronous programming that is lightweight, expressive, and built on structured concurrency principles. Coroutines enable writing asynchronous code that looks and behaves like sequential code, eliminating callback hell and improving readability.
Unlike threads, coroutines are extremely lightweight—millions can run on limited resources. The coroutine framework includes suspend functions for non-blocking operations, builders for launching work, Flow for reactive streams, and comprehensive cancellation and exception handling mechanisms.
This skill covers coroutine fundamentals, builders, contexts, Flow, channels, and production patterns for Android development and server-side Kotlin.
Suspend functions are the building blocks of coroutines, enabling non-blocking operations that can be paused and resumed without blocking threads.
// Basic suspend function
suspend fun fetchUser(id: Int): User {
delay(1000) // Suspends without blocking
return User(id, "Alice")
}
data class User(val id: Int, val name: String)
// Calling suspend functions
suspend fun loadUserProfile(id: Int): UserProfile {
val user = fetchUser(id)
val posts = fetchPosts(user.id)
return UserProfile(user, posts)
}
suspend fun fetchPosts(userId: Int): List<Post> {
delay(500)
return emptyList()
}
data class Post(val id: Int, val title: String)
data class UserProfile(val user: User, val posts: List<Post>)
// Sequential vs concurrent execution
suspend fun loadDataSequential(): Pair<User, List<Post>> {
val user = fetchUser(1)
val posts = fetchPosts(1)
return user to posts
}
suspend fun loadDataConcurrent(): Pair<User, List<Post>> = coroutineScope {
val userDeferred = async { fetchUser(1) }
val postsDeferred = async { fetchPosts(1) }
userDeferred.await() to postsDeferred.await()
}
// Suspend functions with callbacks
suspend fun fetchData(url: String): String = suspendCoroutine { continuation ->
fetchDataWithCallback(url) { result, error ->
if (error != null) {
continuation.resumeWithException(error)
} else {
continuation.resume(result)
}
}
}
fun fetchDataWithCallback(
url: String,
callback: (String, Exception?) -> Unit
) {
// Simulated callback-based API
callback("data", null)
}
// Cancellable suspend functions
suspend fun downloadFile(url: String): ByteArray =
suspendCancellableCoroutine { continuation ->
val request = startDownload(url) { data, error ->
if (error != null) {
continuation.resumeWithException(error)
} else {
continuation.resume(data)
}
}
continuation.invokeOnCancellation {
request.cancel()
}
}
class DownloadRequest {
fun cancel() {}
}
fun startDownload(url: String, callback: (ByteArray, Exception?) -> Unit):
DownloadRequest {
return DownloadRequest()
}
// Using withContext for dispatcher switching
suspend fun saveToDatabase(user: User) {
withContext(Dispatchers.IO) {
// Database operation on IO dispatcher
println("Saving user: ${user.name}")
}
}
suspend fun updateUI(user: User) {
withContext(Dispatchers.Main) {
// UI update on main thread
println("Updating UI for: ${user.name}")
}
}
Suspend functions are marked with the suspend modifier and can only be called
from other suspend functions or coroutines, ensuring proper context.
Coroutine builders launch coroutines with different lifecycle and result handling semantics, enabling structured and unstructured concurrency.
// launch: fire-and-forget coroutine
fun launchExample() {
GlobalScope.launch {
val user = fetchUser(1)
println("User: ${user.name}")
}
}
// async: coroutine with result
fun asyncExample() {
GlobalScope.launch {
val deferredUser = async { fetchUser(1) }
val deferredPosts = async { fetchPosts(1) }
val user = deferredUser.await()
val posts = deferredPosts.await()
println("Loaded ${posts.size} posts for ${user.name}")
}
}
// runBlocking: bridges blocking and suspending worlds
fun runBlockingExample() = runBlocking {
val user = fetchUser(1)
println("User loaded: ${user.name}")
}
// coroutineScope: structured concurrency
suspend fun loadMultipleUsers(ids: List<Int>): List<User> = coroutineScope {
ids.map { id ->
async { fetchUser(id) }
}.awaitAll()
}
// supervisorScope: independent child failures
suspend fun loadDataWithSupervisor(): List<User> = supervisorScope {
val user1 = async { fetchUser(1) }
val user2 = async {
delay(100)
throw Exception("Failed")
}
// user1 succeeds even if user2 fails
listOfNotNull(
try { user1.await() } catch (e: Exception) { null }
)
}
// withTimeout: time-limited coroutines
suspend fun fetchWithTimeout(id: Int): User? {
return try {
withTimeout(2000) {
fetchUser(id)
}
} catch (e: TimeoutCancellationException) {
null
}
}
// Structured concurrency with lifecycle
class ViewModel : CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
fun loadData() {
launch {
val user = fetchUser(1)
// Update UI
}
}
fun onCleared() {
job.cancel()
}
}
Structured concurrency with coroutineScope ensures child coroutines complete
before the scope exits, preventing leaks and ensuring proper cleanup.
Coroutine context defines the execution environment including dispatcher, job, exception handler, and coroutine name for debugging.
// Dispatchers for thread pools
suspend fun dispatcherExamples() {
// Main: UI thread (Android/JavaFX)
withContext(Dispatchers.Main) {
println("On main thread: ${Thread.currentThread().name}")
}
// IO: for blocking I/O operations
withContext(Dispatchers.IO) {
println("On IO thread: ${Thread.currentThread().name}")
}
// Default: CPU-intensive work
withContext(Dispatchers.Default) {
println("On default thread: ${Thread.currentThread().name}")
}
// Unconfined: starts in caller thread, resumes where suspended
withContext(Dispatchers.Unconfined) {
println("Unconfined: ${Thread.currentThread().name}")
}
}
// Coroutine context elements
fun contextExample() {
val scope = CoroutineScope(
Dispatchers.Main +
SupervisorJob() +
CoroutineName("MyCoroutine") +
CoroutineExceptionHandler { _, throwable ->
println("Caught: $throwable")
}
)
scope.launch {
println("Context: $coroutineContext")
}
}
// Inheriting context
fun inheritContextExample() {
CoroutineScope(Dispatchers.Main).launch {
println("Parent: ${Thread.currentThread().name}")
launch {
// Inherits Dispatchers.Main
println("Child: ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
// Overrides with IO dispatcher
println("Override: ${Thread.currentThread().name}")
}
}
}
// ThreadLocal context element
val threadLocalValue = ThreadLocal<String>()
suspend fun threadLocalExample() {
threadLocalValue.set("initial")
withContext(threadLocalValue.asContextElement("new value")) {
println("In context: ${threadLocalValue.get()}")
}
println("After context: ${threadLocalValue.get()}")
}
// Custom context element
data class UserId(val id: Int) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<UserId>
}
suspend fun customContextExample() {
withContext(UserId(42)) {
val userId = coroutineContext[UserId]
println("User ID: ${userId?.id}")
}
}
Dispatchers determine which thread pool executes the coroutine. Context elements are inherited by child coroutines and can be overridden.
Flow represents a cold stream of values that are computed on demand, providing reactive programming capabilities with backpressure and transformation operators.
// Basic Flow
fun numberFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
suspend fun collectFlow() {
numberFlow().collect { value ->
println("Received: $value")
}
}
// Flow builders
fun flowBuilders() {
// flowOf: emit fixed values
val fixedFlow = flowOf(1, 2, 3, 4, 5)
// asFlow: convert collections
val listFlow = listOf(1, 2, 3).asFlow()
// flow: custom emission logic
val customFlow = flow {
repeat(3) {
emit(it)
delay(100)
}
}
}
// Flow transformations
suspend fun flowTransformations() {
numberFlow()
.map { it * 2 }
.filter { it > 5 }
.take(3)
.collect { println(it) }
}
// Flow combining
suspend fun combineFlows() {
val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf("A", "B", "C")
// zip: pairs elements
flow1.zip(flow2) { num, letter ->
"$num$letter"
}.collect { println(it) }
// combine: latest from each
flow1.combine(flow2) { num, letter ->
"$num$letter"
}.collect { println(it) }
}
// Flow exception handling
suspend fun flowExceptionHandling() {
flow {
emit(1)
emit(2)
throw Exception("Error!")
}.catch { e ->
println("Caught: ${e.message}")
emit(-1)
}.collect { println(it) }
}
// StateFlow and SharedFlow
class DataRepository {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events
suspend fun loadUsers() {
val loaded = fetchUsers()
_users.value = loaded
}
suspend fun emitEvent(event: Event) {
_events.emit(event)
}
private suspend fun fetchUsers(): List<User> {
delay(100)
return listOf(User(1, "Alice"))
}
}
data class Event(val type: String)
// Flow on different dispatchers
suspend fun flowWithContext() {
flow {
emit(1)
emit(2)
}
.flowOn(Dispatchers.IO)
.collect { value ->
// Collected on caller's context
println("Value: $value")
}
}
// Channel Flow for hot streams
fun channelFlowExample() = channelFlow {
launch {
repeat(3) {
send(it)
delay(100)
}
}
launch {
repeat(3) {
send(it * 10)
delay(150)
}
}
}
Flow is cold—it doesn't execute until collected. StateFlow holds state, SharedFlow broadcasts events, and channelFlow enables concurrent emissions.
Channels provide communication primitives for sending and receiving values between coroutines, similar to BlockingQueue but suspending.
// Basic channel usage
suspend fun channelExample() {
val channel = Channel<Int>()
launch {
for (x in 1..5) {
channel.send(x)
}
channel.close()
}
for (y in channel) {
println("Received: $y")
}
}
// Buffered channels
fun bufferedChannelExample() {
val channel = Channel<Int>(capacity = 4)
GlobalScope.launch {
for (x in 1..10) {
println("Sending $x")
channel.send(x)
}
channel.close()
}
GlobalScope.launch {
delay(1000)
for (y in channel) {
println("Received: $y")
delay(200)
}
}
}
// Channel types
fun channelTypes() {
// Rendezvous: no buffer, sender suspends until receiver
val rendezvous = Channel<Int>()
// Buffered: specified capacity
val buffered = Channel<Int>(10)
// Unlimited: unlimited buffer
val unlimited = Channel<Int>(Channel.UNLIMITED)
// Conflated: keeps only latest value
val conflated = Channel<Int>(Channel.CONFLATED)
}
// Produce builder
fun produceNumbers(): ReceiveChannel<Int> = GlobalScope.produce {
for (x in 1..5) {
send(x * x)
delay(100)
}
}
suspend fun consumeNumbers() {
val channel = produceNumbers()
channel.consumeEach { println(it) }
}
// Multiple consumers
suspend fun multipleConsumers() {
val channel = Channel<Int>()
repeat(3) { id ->
launch {
for (value in channel) {
println("Consumer $id received: $value")
}
}
}
repeat(10) {
channel.send(it)
delay(100)
}
channel.close()
}
// Select expression for multiple channels
suspend fun selectExample() {
val channel1 = produce { send("A") }
val channel2 = produce { send("B") }
select<Unit> {
channel1.onReceive { value ->
println("From channel1: $value")
}
channel2.onReceive { value ->
println("From channel2: $value")
}
}
}
fun CoroutineScope.produce(block: suspend () -> Unit): ReceiveChannel<String> {
return produce {
block()
}
}
Channels enable fan-out (multiple consumers), fan-in (multiple producers), and pipeline patterns for concurrent data processing.
Coroutines support cooperative cancellation and structured exception handling to ensure proper resource cleanup and error propagation.
// Basic cancellation
suspend fun cancellationExample() {
val job = GlobalScope.launch {
repeat(1000) { i ->
println("Working: $i")
delay(500)
}
}
delay(2000)
println("Cancelling...")
job.cancel()
job.join()
println("Cancelled")
}
// Checking for cancellation
suspend fun checkCancellation() {
GlobalScope.launch {
var i = 0
while (isActive) {
println("Computing: ${i++}")
}
}
}
// Non-cancellable work
suspend fun nonCancellableCleanup() {
val job = GlobalScope.launch {
try {
repeat(1000) {
delay(500)
}
} finally {
withContext(NonCancellable) {
println("Cleanup in non-cancellable context")
delay(1000)
println("Cleanup complete")
}
}
}
delay(1000)
job.cancelAndJoin()
}
// Timeout handling
suspend fun timeoutExample() {
try {
withTimeout(1000) {
repeat(100) {
delay(100)
println("Working...")
}
}
} catch (e: TimeoutCancellationException) {
println("Timed out")
}
}
// Exception handling in coroutines
suspend fun exceptionHandlingExample() {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught: $exception")
}
GlobalScope.launch(handler) {
throw Exception("Coroutine failed")
}
delay(100)
}
// Structured exception handling
suspend fun structuredExceptions() = coroutineScope {
val job1 = launch {
delay(100)
throw Exception("Job 1 failed")
}
val job2 = launch {
delay(200)
println("Job 2 completed")
}
// job2 is cancelled when job1 fails
}
// SupervisorScope for independent failures
suspend fun supervisorExample() = supervisorScope {
val job1 = launch {
delay(100)
throw Exception("Job 1 failed")
}
val job2 = launch {
delay(200)
println("Job 2 completed")
}
// job2 continues even if job1 fails
}
// Error handling with try-catch
suspend fun errorHandling() {
coroutineScope {
launch {
try {
fetchUser(1)
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
}
}
Cancellation is cooperative—coroutines must check isActive or call suspending
functions. Exception handling respects structured concurrency boundaries.
Use structured concurrency with scopes to ensure coroutines are properly managed and cancelled when no longer needed
Choose appropriate dispatchers for the work type: IO for blocking operations, Default for CPU work, Main for UI updates
Handle cancellation cooperatively by checking isActive in loops and using suspending functions that support cancellation
Prefer Flow over callbacks for reactive streams to gain backpressure, operators, and structured lifecycle management
Use supervisorScope for independent operations to prevent one failure from cancelling unrelated coroutines
Avoid GlobalScope except for truly application-wide work to prevent leaks and maintain structured concurrency benefits
Apply withContext for dispatcher switching instead of launching new coroutines to maintain structure and reduce overhead
Handle exceptions explicitly with try-catch or CoroutineExceptionHandler to prevent silent failures
Use StateFlow for state and SharedFlow for events to provide observable streams with proper lifecycle awareness
Test coroutines with TestCoroutineDispatcher to control time and ensure deterministic test execution
Using GlobalScope for scoped work causes memory leaks when coroutines outlive their relevant context like activities or view models
Blocking inside coroutines with Thread.sleep or blocking I/O defeats the purpose and can exhaust thread pools
Not handling cancellation in long-running loops causes coroutines to continue executing after cancellation
Forgetting suspend modifier on functions that call other suspend functions causes compilation errors
Catching CancellationException and not rethrowing it prevents proper cancellation propagation in structured concurrency
Using delay(0) to yield is less clear than explicitly calling yield() for cooperative multitasking
Creating too many coroutines unnecessarily can degrade performance; batch or throttle operations when possible
Not using withContext for dispatcher switching and instead launching unnecessary child coroutines adds complexity
Assuming immediate execution after launch; coroutines may not start until dispatcher has capacity
Mixing callbacks and coroutines incorrectly without proper bridging creates race conditions and leaks
Use Kotlin coroutines when building Android applications for asynchronous operations like network calls, database queries, or any I/O-bound work that should not block the main thread.
Apply coroutines in server-side Kotlin applications with Ktor or Spring Boot for handling concurrent requests efficiently without thread-per-request overhead.
Employ Flow for reactive streams in MVVM architecture, replacing LiveData or RxJava for state management and event propagation with lifecycle awareness.
Leverage structured concurrency for coordinating multiple async operations, ensuring proper cancellation when navigating away from screens or closing connections.
Use channels for producer-consumer patterns, pipelines, or any scenario requiring explicit communication between concurrent coroutines.