Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/AmolPardeshi99/android-performance-skills/llms.txt

Use this file to discover all available pages before exploring further.

A coroutine that never checks for cancellation continues running after its scope is cancelled, consuming CPU and potentially updating a destroyed UI. Tight loops over large data sets, image processing, and computation jobs all need explicit cancellation checkpoints.

Why cancellation checkpoints matter

Kotlin coroutines use cooperative cancellation: cancellation is not forced on a running coroutine. Instead, the coroutine must check its own cancellation status. When a scope is cancelled — for example, in ViewModel.onCleared() or Fragment.onDestroyView() — only coroutines that regularly yield or check their status will stop promptly. A coroutine in a tight CPU loop with no suspension points will keep running until its current work finishes, even if the user has already left the screen.

The problem: tight loops with no suspension point

// ❌ BAD: infinite loop; scope cancellation has no effect
suspend fun processAll(items: List<Item>) {
    for (item in items) {
        heavyTransform(item)  // no suspension point — cancellation is never checked
    }
}
When the ViewModel is cleared and its viewModelScope is cancelled, this coroutine continues iterating through every remaining item. On a large data set this wastes CPU cycles and may write to state that no longer has observers.

Fix 1: ensureActive() for a cancellation checkpoint

ensureActive() checks whether the current coroutine’s job is still active. If the scope has been cancelled, it throws CancellationException immediately, unwinding the coroutine cleanly.
// ✅ CORRECT: ensureActive() checks cancellation at each iteration; throws CancellationException
suspend fun processAll(items: List<Item>) {
    for (item in items) {
        ensureActive()          // cooperative cancellation checkpoint
        heavyTransform(item)
    }
}

Fix 2: yield() for very tight loops

For extremely tight inner loops where even ensureActive() adds measurable overhead, yield() provides both a cancellation checkpoint and a scheduling opportunity — it suspends briefly to let other coroutines run on the same dispatcher.
// ✅ For very tight loops: yield() provides a suspension + cancellation point
suspend fun crunch(data: LargeDataSet) = withContext(Dispatchers.Default) {
    data.forEachIndexed { index, value ->
        if (index % 500 == 0) yield()  // give scheduler and cancellation a chance
        compute(value)
    }
}
ensureActive() throws CancellationException if the scope is cancelled but does not suspend — it returns immediately otherwise. yield() also throws CancellationException on cancellation, but additionally suspends the coroutine and hands control back to the dispatcher, giving other coroutines a chance to run. Use ensureActive() when you only need a cancellation check; use yield() when you also want to avoid starving other coroutines on the same thread pool.

When cancellation checkpoints matter most

Cancellation responsiveness is critical in these situations:
  • ViewModel.onCleared()viewModelScope is cancelled. Any in-progress computation coroutines should stop immediately so they do not write to StateFlow or LiveData after the ViewModel is destroyed.
  • Fragment.onDestroyView()viewLifecycleOwner.lifecycleScope is cancelled. Coroutines updating views must stop before the view hierarchy is torn down to avoid NullPointerException on view references.
  • Cancelled async / launch jobs — When a parent job cancels a child job directly, the child must check cancellation to exit promptly rather than finishing its full workload unnecessarily.

Build docs developers (and LLMs) love