Kotlin Android Concurrency

Kotlin Coroutines in Android:
A Practical Guide

May 10, 2026 6 min read
Kotlin Coroutines in Android

Coroutines transformed how we handle asynchronous code in Android. No more callback hell, no more RxJava's steep learning curve. Here's how I use coroutines in production apps like QR Attendee.

Why Coroutines?

Before coroutines, Android async code looked like nested callbacks or complex RxJava chains. Coroutines let you write asynchronous code that reads like synchronous code:

MainViewModel.kt
fun loadAttendance() {
    viewModelScope.launch {
        val students = repository.getStudents()  // suspend
        val records = repository.getRecords()    // suspend
        _uiState.value = AttendanceState(students, records)
    }
}

Clean. Readable. No callbacks. The viewModelScope automatically cancels when the ViewModel is cleared.

Structured Concurrency

The most powerful concept in Kotlin coroutines is structured concurrency — every coroutine belongs to a scope, and when that scope is cancelled, all child coroutines are cancelled too.

Key Insight: In QR Attendee, when a user navigates away from the attendance screen, viewModelScope cancels all ongoing database queries automatically. Zero memory leaks.

Parallel Execution

Need to run multiple operations at once? Use async:

DashboardViewModel.kt
viewModelScope.launch {
    val stats = async { repo.getStats() }
    val recent = async { repo.getRecentActivity() }
    val alerts = async { repo.getAlerts() }

    // All three run in parallel, await all
    _state.value = Dashboard(
        stats.await(),
        recent.await(),
        alerts.await()
    )
}

Error Handling

Wrap your coroutine code with proper error handling:

viewModelScope.launch {
    try {
        val data = repository.syncFromCloud()
        _state.value = Success(data)
    } catch (e: IOException) {
        _state.value = Error("Network unavailable")
    }
}

Kotlin Flow for Reactive Streams

For data that changes over time (like a live attendance count), use Flow:

// In Repository
fun observeAttendance(): Flow<List<Record>> =
    dao.getAllRecords()  // Room returns Flow

// In ViewModel
val records = repository.observeAttendance()
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

Key Takeaways

  • Use viewModelScope for UI-related coroutines
  • Use async for parallel operations
  • Always handle exceptions in coroutines
  • Prefer Flow over LiveData for new projects
  • Use Dispatchers.IO for database/network work
MD Rajibul Islam

About MD Rajibul Islam

Android developer and founder of Raji's Lab. Built apps with 20K+ downloads. Passionate about clean architecture and Kotlin.