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.
Top comments (0)