Modern Android development is all about writing clean, efficient, and asynchronous code — and Kotlin Coroutines have become the go-to tool for that. If you're tired of callback hell and want to write non-blocking, readable code, coroutines are your best friend.
In this post, we'll cover:
- ✅ What are Coroutines?
- 🔁 Coroutine vs Thread
- 🧭 Coroutine Scope
- 🚀
launch
vsasync
- 🔄
withContext
- ⚠️ Exception handling
- 📱 Real-world Android examples
🌱 What is a Coroutine?
A coroutine is a lightweight thread that can be suspended and resumed. It allows you to perform long-running tasks like network calls or database operations without blocking the main thread.
Coroutine = Co + routine i.e. it's the cooperation among routines(functions).
Think of it as a function that can pause mid-way and resume later, keeping your UI responsive.
GlobalScope.launch {
val data = fetchDataFromNetwork()
updateUI(data)
}
🧵 Coroutine vs Thread
Feature | Coroutine | Thread |
---|---|---|
Lightweight | ✅ Yes | ❌ No (heavy OS object) |
Performance | 🚀 High (thousands at once) | 🐌 Limited (few hundred) |
Blocking | ❌ Non-blocking | ❗ Blocking |
Context Switching | ✨ Easy with withContext
|
⚠️ Complex |
Cancellation | ✅ Scoped and structured | ❌ Manual and error-prone |
Coroutines don’t create new threads — they efficiently use existing ones via dispatchers.
🧭 Coroutine Scope
A CoroutineScope defines the lifecycle of a coroutine. If the scope is canceled, so are all its coroutines.
Common scopes:
-
GlobalScope
: Application-wide (⚠️ Avoid in Android) -
lifecycleScope
: Tied to Activity/Fragment -
viewModelScope
: Tied to ViewModel lifecycle
viewModelScope.launch(Dispatchers.IO) {
val user = userRepository.getUser()
_userState.value = user
}
🚀 launch
vs async
Both start coroutines, but differ in intent:
🔹 launch
: fire-and-forget
- Doesn’t return a result
- Ideal for background tasks
launch {
saveDataToDb()
}
🔹 async
: returns a Deferred
- Used when you need a result
val deferred = async {
fetchDataFromApi()
}
val result = deferred.await()
You can call functions concurrently using async. Here is the example:
class UserViewModel : ViewModel() {
private val _userInfo = MutableLiveData<String>()
val userInfo: LiveData<String> get() = _userInfo
fun loadUserData() {
viewModelScope.launch {
val userDeferred = async { fetchUser() }
val settingsDeferred = async { fetchUserSettings() }
try {
val user = userDeferred.await()
val settings = settingsDeferred.await()
_userInfo.value = "User: $user, Settings: $settings"
} catch (e: Exception) {
_userInfo.value = "Error: ${e.message}"
}
}
}
// Simulated suspending functions
private suspend fun fetchUser(): String {
delay(1000) // Simulate network/API delay
return "Alice"
}
private suspend fun fetchUserSettings(): String {
delay(1200) // Simulate network/API delay
return "Dark Mode"
}
}
🔄 withContext
: for switching threads
Switch coroutine execution to a different dispatcher.
withContext(Dispatchers.IO) {
val data = fetchData()
withContext(Dispatchers.Main) {
updateUI(data)
}
}
✅ Use
withContext
for sequential tasks. Prefer it overasync/await
when there's no concurrency benefit.
⚠️ Exception Handling in Coroutines
✅ Use try-catch
inside coroutine blocks
viewModelScope.launch(Dispatchers.IO) {
try {
val result = repository.getData()
_dataLiveData.postValue(result)
} catch (e: Exception) {
_errorLiveData.postValue("Something went wrong")
}
}
✅ Use CoroutineExceptionHandler
for top-level coroutines
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
Log.e("CoroutineError", "Caught $exception")
}
viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
throw RuntimeException("Oops!")
}
📱 Real-world Example (Network + DB)
viewModelScope.launch(Dispatchers.Main) {
try {
val user = withContext(Dispatchers.IO) {
val networkUser = apiService.fetchUser()
userDao.insertUser(networkUser)
networkUser
}
_userLiveData.postValue(user)
} catch (e: Exception) {
_errorLiveData.postValue("Failed to load user")
}
}
🧼 Best Practices
- Always use
viewModelScope
orlifecycleScope
, notGlobalScope
- Use
Dispatchers.IO
for heavy I/O tasks (network, DB) - Use
withContext
for sequential switching - Catch exceptions explicitly
- Avoid blocking calls like
Thread.sleep()
inside coroutines
📚 Final Thoughts
Kotlin Coroutines are powerful, concise, and align beautifully with modern Android architecture. Once you embrace them, you’ll write faster, cleaner, and more maintainable asynchronous code.
✍️ Enjoyed this post? Drop a ❤️, share it with your Android dev circle, or follow me for more practical guides.
Got questions or want advanced coroutine topics like Flow
, SupervisorJob
? Let me know in the comments! I will cover that in the next blog.