Kotlin Coroutines simplify asynchronous programming on Android. They let you write non-blocking code that looks synchronous — no callback hell, no raw threads, and automatic cancellation when a screen is destroyed.
Main Thread (UI)
│
├── viewModelScope.launch { ... } ← starts coroutine
│ │
│ ├── suspend fetchFromApi() ← pauses, doesn't block thread
│ ├── withContext(IO) { ... } ← switches to background
│ └── update UI state ← back on Main
│
└── UI stays responsive
Old Approach
Coroutines
AsyncTask (deprecated)
viewModelScope.launch
Callback chains
Sequential suspend calls
Manual thread management
Dispatchers.IO / Main
Manual cancellation
Auto-cancel with scope lifecycle
1Coroutine Basics
A coroutine is a lightweight thread managed by Kotlin. You launch coroutines inside a CoroutineScope, and Android provides lifecycle-aware scopes so work is cancelled automatically.
Kotlin — Basic launch// GlobalScope — avoid in production (not lifecycle-aware)
GlobalScope.launch {
delay(1000L)
println("Coroutine finished")
}
// lifecycleScope — tied to Activity/Fragment lifecycle
lifecycleScope.launch {
delay(1000L)
binding.tvResult.text = "Loaded!"
}
// viewModelScope — tied to ViewModel (survives rotation)
viewModelScope.launch {
val data = repository.fetchData()
_uiState.value = UiState.Success(data)
}
CoroutineScope and Job
Kotlin — Scope and cancellationclass DataLoader {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun startLoading() {
scope.launch {
try {
val result = loadData()
showResult(result)
} catch (e: CancellationException) {
// coroutine was cancelled — rethrow
throw e
} catch (e: Exception) {
showError(e.message)
}
}
}
fun cancel() {
scope.cancel() // cancels all coroutines in this scope
}
}
launch vs async
Builder
Returns
Use When
launch
Job
Fire-and-forget, update UI, no return value needed
async
Deferred<T>
Parallel work, need a result via await()
2suspend Functions
A suspend function can pause execution without blocking the thread. It must be called from a coroutine or another suspend function. Retrofit, Room, and most modern Android libraries expose suspend APIs.
Define and call suspend functions
Kotlin — suspend functionsuspend fun fetchUser(id: Int): User {
delay(500) // simulates network delay — non-blocking
return User(id, "Nikhil", "nikhil@example.com")
}
// Call from coroutine
viewModelScope.launch {
val user = fetchUser(1)
_user.value = user
}
Retrofit suspend API
Kotlin — ApiService.ktinterface ApiService {
@GET("posts")
suspend fun getPosts(): List<Post>
@GET("posts/{id}")
suspend fun getPost(@Path("id") id: Int): Post
}
class PostRepository(private val api: ApiService) {
suspend fun loadPosts(): Result<List<Post>> {
return try {
Result.success(api.getPosts())
} catch (e: Exception) {
Result.failure(e)
}
}
}
Room suspend queries
Kotlin — TaskDao.kt@Dao
interface TaskDao {
@Query("SELECT * FROM tasks")
suspend fun getAll(): List<Task>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(task: Task)
@Delete
suspend fun delete(task: Task)
}
Sequential suspend calls
Kotlin — Chain suspend functionssuspend fun loadUserProfile(userId: Int): UserProfile {
val user = api.getUser(userId) // waits, doesn't block thread
val posts = api.getPostsByUser(userId) // runs after user returns
val avatar = storage.downloadAvatar(user.avatarUrl)
return UserProfile(user, posts, avatar)
}
viewModelScope.launch {
_loading.value = true
try {
_profile.value = loadUserProfile(42)
} catch (e: Exception) {
_error.value = e.message
} finally {
_loading.value = false
}
}
3Dispatchers
Dispatchers determine which thread pool runs your coroutine. Use the right dispatcher to keep the UI thread free and run heavy work on background threads.
Main dispatcher types
Dispatcher
Thread
Use For
Dispatchers.Main
Main/UI thread
Updating Views, LiveData, Compose state
Dispatchers.IO
Shared pool (~64 threads)
Network, disk I/O, database reads/writes
Dispatchers.Default
CPU pool (cores count)
Sorting, parsing, image processing
Dispatchers.Unconfined
Caller thread
Rare — testing only, avoid in production
withContext — switch dispatchers
Kotlin — withContext patternclass UserViewModel : ViewModel() {
fun loadUser(userId: Int) {
viewModelScope.launch { // starts on Main (viewModelScope default)
_loading.value = true
val user = withContext(Dispatchers.IO) {
// runs on background thread
repository.fetchUser(userId)
}
// back on Main — safe to update LiveData
_user.value = user
_loading.value = false
}
}
}
launch with explicit dispatcher
Kotlin — Launch on IO// Start coroutine directly on IO
viewModelScope.launch(Dispatchers.IO) {
val data = heavyComputation()
withContext(Dispatchers.Main) {
binding.tvResult.text = data
}
}
suspend fun heavyComputation(): String {
return withContext(Dispatchers.Default) {
// CPU-intensive work
(1..1_000_000).map { it * 2 }.last().toString()
}
}
Dispatcher flow diagram
viewModelScope.launch → Dispatchers.Main
│
├── withContext(IO) → Network / Room / File
│ └── return result
│
├── withContext(Default) → JSON parse / sort list
│ └── return result
│
└── _liveData.value = ... → Update UI on Main
4Async Tasks
Use async to run multiple operations in parallel and combine results with await(). This replaces nested callbacks and is much cleaner than the deprecated AsyncTask.
async and await
Kotlin — Parallel async callsviewModelScope.launch {
_loading.value = true
val usersDeferred = async(Dispatchers.IO) { api.getUsers() }
val postsDeferred = async(Dispatchers.IO) { api.getPosts() }
// Both requests run in parallel
val users = usersDeferred.await()
val posts = postsDeferred.await()
_dashboard.value = Dashboard(users, posts)
_loading.value = false
}
async vs launch comparison
Kotlin — Sequential vs parallel// Sequential — total time = time1 + time2
viewModelScope.launch {
val users = api.getUsers() // waits
val posts = api.getPosts() // waits after users
}
// Parallel — total time ≈ max(time1, time2)
viewModelScope.launch {
coroutineScope {
val users = async { api.getUsers() }
val posts = async { api.getPosts() }
_data.value = CombinedData(users.await(), posts.await())
}
}
coroutineScope and supervisorScope
Kotlin — Structured concurrency// coroutineScope — if one child fails, all siblings are cancelled
suspend fun loadAllData(): Pair<List<User>, List<Post>> = coroutineScope {
val users = async { api.getUsers() }
val posts = async { api.getPosts() }
Pair(users.await(), posts.await())
}
// supervisorScope — one child failure doesn't cancel siblings
suspend fun loadDashboardSafely() = supervisorScope {
val users = async { runCatching { api.getUsers() }.getOrDefault(emptyList()) }
val posts = async { runCatching { api.getPosts() }.getOrDefault(emptyList()) }
Dashboard(users.await(), posts.await())
}
Replacing AsyncTask
AsyncTask (deprecated)
Coroutines equivalent
onPreExecute()
Code before withContext(IO)
doInBackground()
withContext(Dispatchers.IO) { ... }
onPostExecute()
Code after withContext on Main
cancel()
job.cancel() or scope cancellation
5Background Processing
Coroutines handle in-app background work tied to a lifecycle. For work that must survive app closure — sync, uploads, periodic tasks — combine coroutines with WorkManager or run them inside a Service.