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
- π
launchvsasync - π
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
withContextfor sequential tasks. Prefer it overasync/awaitwhen 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
viewModelScopeorlifecycleScope, notGlobalScope - Use
Dispatchers.IOfor heavy I/O tasks (network, DB) - Use
withContextfor 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.
Top comments (0)