Table of Contents
Understanding Lifecycle-Aware Tasks
Lifecycle-aware tasks are background operations that respect the lifecycle of Android components such as activities and fragments. Instead of manually starting and stopping work in onStart, onStop, or onDestroy, you let the system automatically manage when your tasks should run or be cancelled.
Lifecycle-aware code helps you avoid crashes, memory leaks, and wasted work when the user leaves a screen or rotates the device.
Lifecycle-aware tasks must automatically stop or cancel when the related lifecycle owner is destroyed. Never keep long running work tied to an activity or fragment without respecting its lifecycle.
Why Lifecycle Awareness Matters
Every screen in your app is temporary. The user can navigate away, rotate the device, or the system can destroy the activity to reclaim memory. If you start a background task tied to that screen and do not cancel it, you can end up with:
- Work still running when the user no longer cares about the result.
- Attempts to update views after the activity is destroyed, which causes exceptions.
- Memory leaks because long running tasks hold references to activities and views.
Lifecycle-aware tasks solve this by observing lifecycle events and automatically cancelling or cleaning up when appropriate. For beginners, this mostly means using lifecycle-aware utilities instead of manually wiring everything into lifecycle callbacks.
LifecycleOwner and Lifecycle
Lifecycle-aware tasks are built on two key ideas: LifecycleOwner and Lifecycle.
Most Android UI components that you work with, such as ComponentActivity and Fragment, implement LifecycleOwner. This means they expose a lifecycle property of type Lifecycle. The Lifecycle object tracks states such as INITIALIZED, CREATED, STARTED, RESUMED, and DESTROYED.
Lifecycle-aware tools, such as coroutine scopes or LiveData observers, can subscribe to these lifecycle changes. They start work when the lifecycle reaches a certain state and stop or cancel it when the lifecycle is destroyed.
For example, a lifecycle-aware task might be allowed to run only while the activity is at least in the STARTED state. As soon as the activity goes to STOPPED or DESTROYED, the underlying framework cancels the associated work.
Using Lifecycle-Aware Coroutines
Kotlin coroutines are a common way to run background tasks in Android. When you integrate them with lifecycle-aware scopes, you get automatic cancellation at the right time.
Android provides two main lifecycle-aware coroutine scopes you will typically use from UI code: lifecycleScope and viewModelScope. This chapter focuses on the part that is directly tied to LifecycleOwner, which is lifecycleScope. viewModelScope is connected to architecture components and MVVM, so it belongs in another chapter.
lifecycleScope is an extension property available in activities and fragments that are part of AndroidX. It is bound to the lifecycle of the activity or fragment. When that lifecycle reaches the DESTROYED state, all coroutines launched in lifecycleScope are automatically cancelled.
In an activity, you can use it like this:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// setContentView(...)
lifecycleScope.launch {
// This work is automatically cancelled when the activity is destroyed
delay(2000)
// Safe to update UI here as long as the activity still exists
// textView.text = "Finished task"
}
}
}
In a fragment, viewLifecycleOwner.lifecycleScope is often used so that work is bound to the fragment view lifecycle instead of the fragment instance itself. The fragment view lifecycle is destroyed when the fragment's view is destroyed, for example when you navigate away. That prevents attempts to update views that no longer exist.
A fragment example looks like this:
import androidx.fragment.app.Fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.fragment_example, container, false)
viewLifecycleOwnerLiveData.observe(viewLifecycleOwner) { owner ->
owner?.lifecycleScope?.launch {
delay(1000)
// Update views safely here using 'view'
}
}
return view
}
}This pattern ensures that the coroutine is cancelled as soon as the view is destroyed, which helps avoid illegal access to dead views.
Always launch UI related coroutines in a lifecycle-aware scope, such as lifecycleScope in activities or viewLifecycleOwner.lifecycleScope in fragments, so they automatically cancel when the associated UI is destroyed.
Selecting the Right Lifecycle State
Lifecycle-aware tasks can be limited to run only while the lifecycle is in a specific state. For example, you may want some work to continue as long as the activity is at least STARTED, but to temporarily pause it when the activity is STOPPED.
Android provides helper functions like repeatOnLifecycle to express this type of behavior. The idea is that you describe the minimum active state, and the system automatically starts and stops the block of work as the lifecycle moves across that state.
The usage pattern looks like this:
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// This block runs when the lifecycle is at least STARTED
// and automatically stops when it moves below STARTED
}
}
Inside the block, you can collect flows, observe data, or perform periodic work. When the activity or fragment is no longer started, all the work inside is automatically cancelled or paused, and it resumes when the lifecycle returns to the STARTED state.
This pattern is especially useful when you are collecting a continuous stream of data, for example updates from a repository or user input events, but you want to avoid doing this work while the UI is not visible.
Attaching Observers in a Lifecycle-Aware Way
Many Android APIs use observers to listen for changes. Lifecycle-aware observers attach to a LifecycleOwner and automatically stop receiving events when the lifecycle reaches DESTROYED.
A common example is LiveData, which you will see in more detail when you work with architecture components. For lifecycle-aware tasks, the important idea is that you should always prefer observer APIs that accept a LifecycleOwner parameter. That way, you do not need to manually remove observers in onStop or onDestroy.
The pattern looks like this:
liveDataObject.observe(this) { value ->
// 'this' is a LifecycleOwner, such as an activity
// This observer is automatically removed when the lifecycle is DESTROYED
}Lifecycle-aware observers protect you from receiving callbacks when the UI is gone, which reduces bugs and memory leaks.
Always pass a LifecycleOwner when subscribing to long-lived data or events. Avoid manual observer management unless you have a specific reason to do so.
Common Pitfalls Without Lifecycle Awareness
Understanding what can go wrong without lifecycle-aware tasks helps you appreciate why these tools exist.
One common mistake is starting a background thread or coroutine in onCreate and never cancelling it. If the thread keeps a reference to an activity or a view, that activity might never be garbage collected. The user could navigate away, but the thread would still be running and holding memory.
Another mistake is launching work that attempts to update UI elements after the activity is destroyed. For example, a delayed operation might try to change text on a TextView that no longer exists, which can cause IllegalStateException or similar runtime crashes.
A third problem happens when you manually track lifecycle in flags, such as setting a boolean in onPause or onResume and checking it in your background task. This becomes difficult to maintain and easy to get wrong. Lifecycle-aware APIs avoid this by reading the lifecycle state directly and tying task lifetime to that state.
Lifecycle-aware tools such as lifecycleScope, repeatOnLifecycle, and observer methods with LifecycleOwner parameters are designed to prevent these classes of bugs.
Choosing Between Lifecycle-Aware Tools
For basic screens, you can remember a few simple rules that cover most use cases.
If you need to run a coroutine or a small background task that is directly related to an activity or fragment, use lifecycleScope or viewLifecycleOwner.lifecycleScope. This is a natural choice for tasks that are cancelled when the user leaves that specific screen.
If you need to observe a stream of data while the UI is visible, and pause when it is hidden, use repeatOnLifecycle with an appropriate state, often Lifecycle.State.STARTED. This keeps your code clear and automatically restarts your data collection when the user returns.
If you subscribe to continuous data using observable types, prefer APIs that accept a LifecycleOwner so observers are added and removed automatically with lifecycle changes.
If you need work that is independent of any one screen and should continue even after the user leaves the app, lifecycle-aware scopes in activities and fragments are not the right tool. In that case, you will typically use other background mechanisms that are not tied directly to UI lifecycle, which are described in other chapters.
Summary of Lifecycle-Aware Behaviour
Lifecycle-aware tasks integrate with LifecycleOwner to know when to start, pause, or cancel work. This prevents wasted work, memory leaks, and crashes that come from trying to update UI after it has been destroyed.
You achieve lifecycle awareness by using:
lifecycleScope in activities and fragments for coroutines bound to the UI lifecycle.
viewLifecycleOwner.lifecycleScope for fragment view related work that should end when the view is destroyed.
repeatOnLifecycle when you want to run and pause tasks based on lifecycle states such as STARTED.
Observer APIs that accept a LifecycleOwner so subscriptions are removed automatically when the lifecycle ends.
By consistently using these tools, your background tasks stay in sync with the lifecycle of your screens, which makes your apps safer, more efficient, and easier to maintain.