Avoid These 5 Dangerous Kotlin Coroutine Pitfalls in Android
This article highlights five common Kotlin coroutine pitfalls for Android developers—directly calling suspend functions in Views, misusing GlobalScope, sequential network calls, improper exception handling, and ignoring cancellation—while providing correct implementations and best‑practice guidelines to avoid them.
Introduction
For Android developers, Kotlin coroutines have become an indispensable part of daily workflow, simplifying asynchronous programming and concurrency. However, this convenience can be deceptive, leading many developers into subtle and dangerous traps that appear to be best practices.
1. Calling suspend functions directly from a View
Invoking a suspend function directly in a View can cause serious lifecycle issues and main‑thread blocking, especially in an MVP architecture. Developers often define suspend functions in a ViewModel and call them from the View to fetch data and update UI state.
<code>class MyViewModel(private val repository: MyRepository) : ViewModel() {
suspend fun fetchUserData() {
val user = repository.getUser() // ❌ problematic
}
}</code>The problems include:
Blocking the main thread : If the suspend function performs a time‑consuming task without switching to a background dispatcher (e.g., not using
Dispatchers.IO), it blocks the UI thread, causing jank or ANR errors.
Lifecycle issues : While ViewModel survives configuration changes, a coroutine without the proper scope (e.g., not using
viewModelScope) may continue after the UI is destroyed, leading to memory leaks.
Result loss : If the ViewModel is cleared (e.g., Activity finishes), the coroutine is cancelled and its result never reaches the UI.
The correct approach is:
<code>class MyViewModel(private val repository: MyRepository) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun fetchUserData() {
viewModelScope.launch {
try {
val result = repository.getUser() // ✅ Safe coroutine call
_user.postValue(result) // ✅ Updates UI on the Main thread
} catch (e: Exception) {
// Handle error properly
}
}
}
}</code>2. Misusing GlobalScope
Using
GlobalScopefor coroutines leads to memory leaks, difficult management of background tasks, and crashes because the coroutine lives beyond the app’s lifecycle.
<code>GlobalScope.launch {
delay(400)
Log.d("Global scope", "Task Successful")
}</code>Issues with
GlobalScope:
Cannot be cancelled : Once started, the coroutine runs independently of the app’s lifecycle, wasting resources if the user leaves the app.
Ignores lifecycle awareness : Unlike
viewModelScope, it does not cancel when the ViewModel is cleared, risking crashes when updating a destroyed UI.
Lacks custom dispatcher and exception handling : It defaults to
Dispatchers.Default, which may be unsuitable for network I/O, and uncaught exceptions can crash the whole app.
Always use
viewModelScope.launchinstead:
<code>viewModelScope.launch {
delay(400)
Log.d("viewmodel", "Task Successful")
}</code>3. Not executing calls in parallel
A common pitfall is performing network calls sequentially when they could run concurrently, leading to unnecessary delays and poor user experience.
<code>suspend fun mistakeGetCarNames(ids: List<Int>): List<String> {
val names = mutableListOf<String>()
for (id in ids) {
names.add(getCarNameById(id)) // ❌ runs one after another
}
return names
}</code>Problems:
Each request is independent, so sequential execution wastes time.
Network latency adds up linearly (e.g., five 1‑second calls become 5 seconds).
Long wait times frustrate users.
Correct parallel implementation:
<code>suspend fun getCarNames(ids: List<Int>): List<String> {
return coroutineScope {
ids.map { id ->
async { getCarNameById(id) } // ✅ Runs all requests in parallel!
}.awaitAll() // ✅ Waits for all coroutines to complete and returns results
}
}</code>4. Not catching exceptions correctly
Catching all exceptions, including
CancellationException, without re‑throwing can hide cancellation signals, causing memory leaks and unintended behavior.
<code>suspend fun mistakeRiskyTask() {
try {
delay(3_000L) // Simulating long‑running operation
val error = 10 / 0 // ❌ throws ArithmeticException
} catch (e: Exception) {
println("iyke: code failed") // ❌ catches CancellationException too
}
}</code>When a coroutine is cancelled (e.g., Activity destroyed), the catch block may swallow the
CancellationException, preventing the parent coroutine from knowing about the cancellation.
Proper handling:
<code>suspend fun riskyTask() {
try {
delay(3_000L)
val error = 10 / 0
} catch (e: Exception) {
if (e is CancellationException) throw e // ✅ Rethrow CancellationException!
println("Emmanuel iyke: code not working") // ✅ Handle other exceptions properly
}
}</code>5. Long‑running tasks without checking coroutine cancellation
Running a long loop without checking
isActiveor handling
CancellationExceptionmeans the coroutine may continue even after it has been cancelled, wasting CPU and memory.
<code>suspend fun mistakeDoSomething() {
val job = CoroutineScope(Dispatchers.Default).launch {
var random = Random.nextInt(100_000)
while (random != 50_000) { // ❌ No cancellation check
println("Random: $random")
random = Random.nextInt(100_000)
}
}
println("Random: our job cancelled")
delay(500L)
job.cancel() // ❌ This won't immediately stop the loop
}</code>Issues:
The loop keeps running after cancellation, wasting resources.
Potential memory leaks and UI jank.
Unexpected behavior if the coroutine continues to update UI.
Correct approach using
isActive:
<code>suspend fun doSomethingWithIsActive() {
val job = CoroutineScope(Dispatchers.Default).launch {
var random = Random.nextInt(100_000)
while (isActive && random != 50_000) { // ✅ Manually check if coroutine is active
println("Random: $random")
random = Random.nextInt(100_000)
}
}
println("Random: our job cancelled")
delay(500L)
job.cancel() // ✅ Coroutine will now stop properly
}</code>By following these guidelines—using the appropriate coroutine scope, handling lifecycle, leveraging parallelism, catching exceptions properly, and checking cancellation—you can write robust, efficient Kotlin coroutine code for Android applications.
AndroidPub
Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.