In Android development, Kotlin Coroutines have become a powerful tool to handle asynchronous operations more efficiently than traditional callbacks or RxJava.
However, many developers encounter a common issue: coroutines are not cancelled when the lifecycle is destroyed.
This problem may lead to memory leaks, crashes, and unnecessary background work.
In this article, we will explore why this happens, common mistakes, and how to fix coroutine cancellation properly with lifecycle-aware components.

Why Coroutines Are Not Cancelled Automatically
By default, launching a coroutine using GlobalScope.launch
or other non-lifecycle aware scopes does not bind it to the lifecycle of an Activity or Fragment.
This means that even if the UI component is destroyed, the coroutine will continue running in the background.
If the coroutine tries to update a destroyed UI, it may cause crashes such as IllegalStateException
.
Common Mistake Example
Here’s a simple example of a coroutine running in an Activity
without lifecycle cancellation:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Wrong: Coroutine not bound to lifecycle GlobalScope.launch { delay(5000) Log.d("CoroutineTest", "This will run even if activity is destroyed") } } }
In this case, if the user navigates away from the activity before 5 seconds, the coroutine still continues running.
This may lead to memory leaks or UI crashes.
How to Fix Coroutine Cancellation
The correct approach is to use lifecycle-aware scopes such as lifecycleScope
or viewLifecycleOwner.lifecycleScope
in Fragments.
These automatically cancel coroutines when the lifecycle is destroyed.
Using lifecycleScope in Activity
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Correct: Coroutine tied to Activity lifecycle lifecycleScope.launch { delay(5000) Log.d("CoroutineTest", "This will be cancelled if activity is destroyed") } } }
Using viewLifecycleOwner.lifecycleScope in Fragment
class ExampleFragment : Fragment(R.layout.fragment_example) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Correct: Coroutine tied to Fragment view lifecycle viewLifecycleOwner.lifecycleScope.launch { delay(5000) Log.d("CoroutineTest", "Cancelled automatically when view is destroyed") } } }
Using Lifecycle.repeatOnLifecycle
If you want to collect Flow or execute tasks only when the UI is visible,repeatOnLifecycle
is the recommended pattern.
This ensures coroutines only run when the lifecycle is at a certain state (e.g., STARTED
).
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.dataFlow.collect { data -> Log.d("CoroutineFlow", "Received data: $data") } } }
Comparison: GlobalScope vs lifecycleScope
Scope | Lifecycle Awareness | Cancellation | Use Case |
---|---|---|---|
GlobalScope | No | Not automatically cancelled | Background jobs not tied to UI |
lifecycleScope | Yes | Cancelled when lifecycle destroyed | UI-related coroutines |
viewModelScope | Yes | Cancelled when ViewModel cleared | Long-running UI logic in ViewModel |
Best Practices
- Never use
GlobalScope
for coroutines tied to UI lifecycle. - Use
lifecycleScope
in Activities andviewLifecycleOwner.lifecycleScope
in Fragments. - Use
repeatOnLifecycle
when collecting Flows. - For ViewModel, always use
viewModelScope
instead of launching from Activity/Fragment.
Conclusion
When coroutines are not cancelled after the lifecycle is destroyed,
it can cause memory leaks and crashes in your Android app.
By using lifecycleScope
, viewLifecycleOwner.lifecycleScope
,
or viewModelScope
, you ensure proper cancellation and safe execution of asynchronous tasks.
Always prefer lifecycle-aware scopes to avoid issues in production apps.