Android Kotlin Coroutines – Complete Guide with Real Examples
Coroutines are one of the most important tools in modern Android development. They help you write asynchronous code that is clean, readable, and safe.
In this guide, you’ll learn:
- What coroutines are
- Why they matter in Android
- How to use them in real-world scenarios
What Are Coroutines?
Coroutines are lightweight threads that allow you to run tasks asynchronously without blocking the main (UI) thread.
Instead of writing complex callbacks, coroutines let you write code that looks sequential but runs asynchronously.
Why Coroutines Matter in Android
Android has a single UI thread.
If you block it:
- UI freezes ❌
- App becomes unresponsive ❌
Coroutines help by moving heavy work off the main thread while keeping code simple.
Basic Example
lifecycleScope.launch {
delay(1000)
textView.text = "Hello after delay"
}
What happens:
- Coroutine runs on main thread
delaydoes NOT block UI- UI remains smooth
Dispatchers (Thread Control)
Dispatchers decide where your coroutine runs:
Dispatchers.Main→ UI workDispatchers.IO→ network/databaseDispatchers.Default→ CPU-heavy work
Switching Threads
A very common Android pattern:
lifecycleScope.launch {
val data = withContext(Dispatchers.IO) {
fetchFromApi()
}
textView.text = data
}
Flow:
- Run API call in background
- Return to main thread
- Update UI safely
Coroutine Scopes in Android
Scopes control lifecycle.
Common ones:
lifecycleScope→ Activity/FragmentviewModelScope→ ViewModel
viewModelScope.launch {
loadData()
}
If ViewModel is cleared → coroutine is cancelled automatically.
Real-World Use Cases
1. Network API Calls (Retrofit)
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) {
apiService.getUsers()
}
_users.value = result
} catch (e: Exception) {
_error.value = "Failed to load data"
}
}
Why this works well:
- Network runs on background thread
- UI updates safely
- Errors handled cleanly
2. Database Operations (Room)
viewModelScope.launch {
val users = withContext(Dispatchers.IO) {
userDao.getAllUsers()
}
_users.value = users
}
Room + coroutines = clean async DB access.
3. Parallel API Calls
viewModelScope.launch {
val result = coroutineScope {
val user = async { api.getUser() }
val posts = async { api.getPosts() }
Pair(user.await(), posts.await())
}
_data.value = result
}
Both API calls run at the same time → faster performance.
4. Handling Loading States
viewModelScope.launch {
_loading.value = true
val data = withContext(Dispatchers.IO) {
repository.getData()
}
_loading.value = false
_data.value = data
}
Used in almost every production app.
5. Timeout Handling
viewModelScope.launch {
val result = withTimeoutOrNull(3000) {
api.getData()
}
if (result == null) {
_error.value = "Request timed out"
}
}
Prevents infinite loading.
6. Retry Logic
suspend fun fetchWithRetry(): String {
repeat(3) {
try {
return api.getData()
} catch (e: Exception) {
delay(1000)
}
}
throw Exception("Failed after retries")
}
7. Flow for UI State
val userFlow = flow {
emit(api.getUser())
}
viewModelScope.launch {
userFlow.collect {
_user.value = it
}
}
Used for:
- Live updates
- Reactive UI
8. StateFlow in ViewModel
private val _state = MutableStateFlow<String>("")
val state = _state
viewModelScope.launch {
val data = api.getData()
_state.value = data
}
Great for Jetpack Compose.
9. Handling User Input (Search Example)
searchFlow
.debounce(300)
.distinctUntilChanged()
.flatMapLatest {
flow { emit(api.search(it)) }
}
Prevents:
- Too many API calls
- Laggy UI
10. Cancellation (User Leaves Screen)
viewModelScope.launch {
repeat(1000) {
delay(500)
}
}
When user leaves:
→ coroutine automatically cancels
Common Mistakes
Blocking the thread
Thread.sleep(1000)
Using GlobalScope
GlobalScope.launch { }
Too many context switches
withContext(IO) { }
withContext(Main) { }
withContext(IO) { }
Best Practices
- Use
viewModelScopefor UI logic - Use
Dispatchers.IOfor network/DB - Handle exceptions properly
- Avoid GlobalScope
- Use Flow for streams
Final Mental Model
Think of coroutines like this:
- Scope = lifecycle manager
- Dispatcher = where work happens
- Coroutine = the task
Once you understand these three:
→ coroutines become easy to reason about
Final Thoughts
Coroutines are not just a feature. They are the standard way to handle async work in Android today.