Kotlin Coroutines in Android
Why coroutines?
Android draws the screen on the main thread. If you load a database or call an API there, the app freezes; after ~5 seconds Android shows "App isn't responding" (ANR). Coroutines let you write background work in normal top-to-bottom code — without callbacks.
Blocking main thread
- Thread.sleep / network on main
- UI frozen, no taps work
- ANR dialog
Coroutine
- suspend fun loadNotes()
- thread is free while waiting
- UI smooth, result applied later
suspend functions and launch
These examples are plain Kotlin and the outputs are real. In an app you would use viewModelScope instead of runBlocking.
import kotlinx.coroutines.*
suspend fun loadNotes(): List<String> {
delay(1000) // pretend database / network work
return listOf("Maths homework", "Physics notes")
}
fun main() = runBlocking {
println("1. Start (thread: main)")
val job = launch {
val notes = loadNotes() // suspends, does NOT block the thread
println("3. Loaded ${notes.size} notes")
}
println("2. UI is still free while loading…")
job.join()
println("4. Done")
}Parallel work with async
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun getProfile(): String { delay(1000); return "Riya" }
suspend fun getMarks(): Int { delay(1000); return 92 }
fun main() = runBlocking {
val sequential = measureTimeMillis {
val p = getProfile(); val m = getMarks()
println("Sequential: $p $m")
}
val parallel = measureTimeMillis {
val p = async { getProfile() }
val m = async { getMarks() }
println("Parallel: ${p.await()} ${m.await()}")
}
println("sequential ≈ ${sequential / 1000}s, parallel ≈ ${parallel / 1000}s")
}Dispatchers and withContext
| Dispatcher | Use for |
|---|---|
| Dispatchers.Main | Updating views (default in viewModelScope / lifecycleScope) |
| Dispatchers.IO | Files, plain JDBC/SQLite, blocking network libraries |
| Dispatchers.Default | CPU-heavy work: sorting big lists, parsing, image processing |
import kotlinx.coroutines.*
fun main() = runBlocking {
val result = withContext(Dispatchers.Default) { // heavy CPU work off the main thread
(1..1_000_000).sumOf { it.toLong() }
}
println("Sum = $result")
try {
withTimeout(500) { delay(2000); println("never printed") }
} catch (e: TimeoutCancellationException) {
println("Timed out after 500 ms")
}
val job = launch { repeat(10) { i -> println("tick $i"); delay(300) } }
delay(700)
job.cancel() // like leaving the screen
println("Cancelled: ${job.isCancelled}")
}Android scopes
// ViewModel – cancelled automatically when the ViewModel is cleared
class NoteViewModel(app: Application) : AndroidViewModel(app) {
fun save(note: Note) = viewModelScope.launch {
repo.save(note) // Room suspend function
}
}
// Fragment / Activity – cancelled when the lifecycle is destroyed
viewLifecycleOwner.lifecycleScope.launch {
val note = viewModel.getNote(id) // suspend
binding.etTitle.setText(note?.title)
}
// Collect a Flow only while the screen is visible
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.results.collect { adapter.submitList(it) }
}
}Timeouts and cancellation
When a scope is cancelled (the user leaves the screen), all its coroutines stop at the next suspension point — no leaks, no updates to dead views. withTimeout stops slow work.
Handling errors
viewModelScope.launch {
_tip.value = TipState.Loading
_tip.value = try {
val tips = TipsApi.service.getTips().tips // may throw IOException / HttpException
TipState.Success(tips.random())
} catch (e: Exception) {
TipState.Error(e.message ?: "Network error")
}
}Real example from Student Diary
// AddEditNoteFragment – copy the picked photo on a background thread
private fun copyImage(uri: Uri) {
viewLifecycleOwner.lifecycleScope.launch {
val path = withContext(Dispatchers.IO) { // file work → IO thread
ImageStore.copyToAppStorage(requireContext(), uri)
}
if (path == null) toast(R.string.image_error) else { imagePath = path; showImage() } // back on Main
}
}
// ReminderWorker – WorkManager's CoroutineWorker runs doWork() as a suspend function
override suspend fun doWork(): Result {
val note = AppDatabase.get(applicationContext).noteDao().getById(noteId) ?: return Result.success()
NotificationHelper.show(applicationContext, note.id, note.title, note.subject)
return Result.success()
}Common mistakes
- Using
GlobalScope.launch→ work keeps running after the screen is gone. - Calling
Thread.sleepinside a coroutine → blocks the thread; usedelay. - Catching
CancellationExceptionand ignoring it → the coroutine cannot be cancelled. Re-throw it (or catch specific exceptions). - Updating a view from
Dispatchers.IO→ crash; switch back to Main (end thewithContextblock first).
Summary
suspendfunctions pause without blocking; start them withlaunchorasync.- Main for UI, IO for files/blocking I/O, Default for CPU work; switch with
withContext. - Use
viewModelScopeandlifecycleScope; wrap risky calls in try/catch.
In a ViewModel, load "profile" and "marks" (two suspend functions with delay) in parallel with async, show a progress bar while loading, and show an error Snackbar if one of them throws.
