DEV Community

Cover image for Mastering Kotlin Coroutines in Android: A Practical Guide
Mohit Rajput
Mohit Rajput

Posted on

Mastering Kotlin Coroutines in Android: A Practical Guide

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 vs async
  • 🔄 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)
}
Enter fullscreen mode Exit fullscreen mode

🧵 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
}
Enter fullscreen mode Exit fullscreen mode

🚀 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()
}
Enter fullscreen mode Exit fullscreen mode

🔹 async: returns a Deferred

  • Used when you need a result
val deferred = async {
    fetchDataFromApi()
}
val result = deferred.await()
Enter fullscreen mode Exit fullscreen mode

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"
    }
}

Enter fullscreen mode Exit fullscreen mode

🔄 withContext: for switching threads

Switch coroutine execution to a different dispatcher.

withContext(Dispatchers.IO) {
    val data = fetchData()
    withContext(Dispatchers.Main) {
        updateUI(data)
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Use withContext for sequential tasks. Prefer it over async/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")
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Use CoroutineExceptionHandler for top-level coroutines

val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    Log.e("CoroutineError", "Caught $exception")
}

viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
    throw RuntimeException("Oops!")
}
Enter fullscreen mode Exit fullscreen mode

📱 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")
    }
}
Enter fullscreen mode Exit fullscreen mode

🧼 Best Practices

  • Always use viewModelScope or lifecycleScope, not GlobalScope
  • 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.

Postmark Image

"Please fix this..."

Focus on creating stellar experiences without email headaches. Postmark's reliable API and detailed analytics make your transactional emails as polished as your product.

Start free

Top comments (0)

Postmark Image

"Please fix this..."

Focus on creating stellar experiences without email headaches. Postmark's reliable API and detailed analytics make your transactional emails as polished as your product.

Start free

Join the Runner H "AI Agent Prompting" Challenge: $10,000 in Prizes for 20 Winners!

Runner H is the AI agent you can delegate all your boring and repetitive tasks to - an autonomous agent that can use any tools you give it and complete full tasks from a single prompt.

Check out the challenge

DEV is bringing live events to the community. Dismiss if you're not interested. ❤️