Kotlin Coroutines

Write asynchronous, non-blocking code with suspend functions, scopes, dispatchers, and async/await

suspend Scope Dispatchers async

Table of Contents

1Coroutines Basics

Coroutines are lightweight threads for asynchronous programming. They let you write sequential-looking code that runs non-blocking work — network calls, database queries, file I/O — without blocking the main thread. Coroutines are the recommended approach for async code in Kotlin and Android.

Why coroutines over threads?

FeatureThreadsCoroutines
CostHeavy (~1 MB stack each)Very lightweight — thousands possible
CreationExpensiveCheap — suspend/resume
BlockingBlocks OS threadSuspends without blocking thread
CancellationManual, error-proneStructured — auto propagates
Code styleCallbacks, CompletableFutureSequential suspend functions

Your first coroutine

runBlocking and launchimport kotlinx.coroutines.* fun main() = runBlocking { println("Main starts") launch { delay(1000L) println("Coroutine finished") } println("Main continues") } // Output order: // Main starts // Main continues // Coroutine finished (after ~1 second)

Key building blocks

suspend
Pausable functions
CoroutineScope
Lifecycle boundary
Dispatcher
Which thread pool
Gradle dependency// build.gradle.kts dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") // Android: also use lifecycle-viewmodel-ktx, etc. }
Structured concurrency

Coroutines are organized in scopes — child coroutines cannot outlive their parent. This prevents leaks and makes cancellation predictable.

2Suspend Functions

A suspend function can pause execution without blocking the thread, then resume later. Mark functions with the suspend keyword. They can only be called from other suspend functions or inside a coroutine.

Defining suspend functionsimport kotlinx.coroutines.* suspend fun fetchUser(): String { delay(500) // simulates network delay — non-blocking return "Nikhil" } suspend fun fetchPosts(): List<String> { delay(300) return listOf("Post 1", "Post 2") } fun main() = runBlocking { val user = fetchUser() val posts = fetchPosts() println("$user has ${posts.size} posts") }

Sequential vs parallel suspend calls

Sequential — one after anotherimport kotlinx.coroutines.* suspend fun loadDataSequential() { val user = fetchUser() // waits ~500ms val posts = fetchPosts() // then waits ~300ms // Total: ~800ms } // Parallel loading uses async — see Async and Await section

suspend rules

RuleExplanation
suspend keyword requiredCompiler transforms function for coroutine machinery
Call from coroutine onlyRegular functions cannot call suspend directly
Can call other suspend functionsNatural chaining of async operations
Non-blockingThread is free while suspended
Bridging coroutine to regular codeimport kotlinx.coroutines.* fun startLoading() { // Launch coroutine from non-suspend context GlobalScope.launch { val data = fetchUser() println(data) } } // Android: viewModelScope.launch { } or lifecycleScope.launch { }
delay vs Thread.sleep: delay() is suspend — it releases the thread. Thread.sleep() blocks the thread — avoid in coroutines.

3Coroutine Scope

A CoroutineScope defines the lifecycle of coroutines launched within it. When the scope is cancelled, all its child coroutines are cancelled too — this is structured concurrency.

Creating a scopeimport kotlinx.coroutines.* class DataLoader { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) fun load() { scope.launch { val data = fetchUser() println(data) } } fun cleanup() { scope.cancel() // cancels all running coroutines in scope } }

launch vs async

BuilderReturnsUse when
launch { }Job (no result)Fire-and-forget, UI updates, logging
async { }Deferred<T>Need a computed result — use await()
runBlocking { }Blocks caller threadTests, main(), bridging only
launch exampleimport kotlinx.coroutines.* fun main() = runBlocking { val job = launch { repeat(5) { i -> println("Tick $i") delay(200L) } } delay(500L) job.cancel() job.join() println("Cancelled") }

Android scopes

ScopeLifecycle
viewModelScopeCleared when ViewModel is destroyed
lifecycleScopeActivity/Fragment lifecycle
rememberCoroutineScope()Jetpack Compose composable
GlobalScopeApp-wide — avoid in production
Android ViewModel pattern (concept)// class UserViewModel : ViewModel() { // fun loadUser() { // viewModelScope.launch { // val user = repository.getUser() // _uiState.value = UiState.Success(user) // } // } // }
SupervisorJob

Use SupervisorJob() when one child failure should not cancel siblings — common in ViewModels loading multiple independent resources.

4Dispatchers

Dispatchers determine which thread pool runs coroutine code. Switch dispatchers with withContext(dispatcher) to run work on the appropriate thread.

DispatcherThread poolUse for
Dispatchers.MainUI thread (Android main)Updating UI, Compose state
Dispatchers.IOShared pool (~64 threads)Network, disk I/O, database
Dispatchers.DefaultCPU cores countSorting, parsing, heavy computation
Dispatchers.UnconfinedStarts on caller, resumes anywhereTesting only — avoid in production
withContext — switch dispatcherimport kotlinx.coroutines.* suspend fun loadUserFromNetwork(): String { return withContext(Dispatchers.IO) { delay(500) // network call simulation "User data from API" } } suspend fun processData(raw: String): String { return withContext(Dispatchers.Default) { raw.uppercase().reversed() // CPU work } } fun main() = runBlocking { val raw = loadUserFromNetwork() val processed = processData(raw) println(processed) }

launch with dispatcher

Specify dispatcher in launchimport kotlinx.coroutines.* fun main() = runBlocking { launch(Dispatchers.IO) { val data = fetchFromDatabase() withContext(Dispatchers.Main) { updateUI(data) // switch to Main for UI } } } suspend fun fetchFromDatabase(): String { delay(200) return "DB result" } fun updateUI(data: String) { println("UI updated: $data") }

Dispatcher decision guide

Main

Touch UI, show Toast, update TextView or Compose State.

IO

Retrofit API calls, Room database, file read/write.

Default

JSON parsing, image processing, list sorting.

Rule of thumb: Do heavy work off Main; return to Main only for UI updates. Never block Main with Thread.sleep or synchronous network calls.

5Async and Await

async starts a coroutine that returns a Deferred<T> — a future result. Call await() to get the value (suspending until ready). Use async for parallel work when you need multiple results.

Basic async/awaitimport kotlinx.coroutines.* suspend fun fetchUser(): String { delay(500) return "Nikhil" } suspend fun fetchPosts(): List<String> { delay(300) return listOf("Post A", "Post B") } fun main() = runBlocking { val userDeferred = async { fetchUser() } val postsDeferred = async { fetchPosts() } val user = userDeferred.await() val posts = postsDeferred.await() println("$user — ${posts.size} posts") }

Parallel vs sequential timing

Parallel loading — ~500ms totalimport kotlinx.coroutines.* fun main() = runBlocking { val time = measureTimeMillis { val user = async { fetchUser() } // starts immediately val posts = async { fetchPosts() } // starts immediately println("${user.await()} — ${posts.await().size} posts") } println("Completed in ${time}ms") // ~500ms (max of both), not 800ms } suspend fun fetchUser(): String { delay(500); return "User" } suspend fun fetchPosts(): List<String> { delay(300); return listOf("P1") }

async with dispatchers

Parallel IO operationsimport kotlinx.coroutines.* fun main() = runBlocking { val profile = async(Dispatchers.IO) { fetchProfile() } val settings = async(Dispatchers.IO) { fetchSettings() } val result = ProfileResult( profile = profile.await(), settings = settings.await() ) println(result) } data class ProfileResult(val profile: String, val settings: String) suspend fun fetchProfile(): String { delay(400); return "Profile" } suspend fun fetchSettings(): String { delay(300); return "Settings" }

launch vs async comparison

Aspectlaunchasync
Return typeJobDeferred<T>
Get resultNo return value.await()
ExceptionPropagates immediatelyHeld until await()
Best forSide effectsParallel computation with results

Error handling with async

try-catch around awaitimport kotlinx.coroutines.* suspend fun riskyFetch(): String { delay(100) throw IOException("Network failed") } fun main() = runBlocking { val deferred = async(Dispatchers.IO) { riskyFetch() } try { val result = deferred.await() println(result) } catch (e: Exception) { println("Error: ${e.message}") } }
When NOT to use async

If operations are independent but you don't need parallel speedup, sequential suspend calls are simpler. Use async when wall-clock time matters — e.g. loading dashboard data from multiple APIs at once.

6Summary Cheatsheet

TopicKey Takeaway
Coroutines BasicsLightweight async; launch, runBlocking; structured concurrency
Suspend Functionssuspend fun; pause without blocking; call from coroutine
Coroutine ScopeCoroutineScope; cancel scope cancels children; viewModelScope on Android
DispatchersMain = UI; IO = network/disk; Default = CPU; withContext
Async and Awaitasync { }Deferred<T>; await() for parallel results
Librarykotlinx-coroutines-core
Next lessonAndroid Development