Lifecycle-Aware Coroutines
Android components like Activities and Fragments go through lifecycle states (CREATED, STARTED, RESUMED, and back down through PAUSED, STOPPED, DESTROYED), and collecting a Flow without respecting that lifecycle can cause real problems: a coroutine collecting a StateFlow while the Activity is STOPPED (e.g. the screen is off or another app is in the foreground) still does work and can even crash if it tries to update UI that isn't in a safe state. Lifecycle-aware coroutine APIs, provided by the androidx.lifecycle libraries, exist to tie coroutine execution directly to specific lifecycle states so that collection automatically pauses and resumes as the component moves through its lifecycle.
Cricket analogy: A ground announcer who keeps reading out live commentary even after play is suspended for bad light is doing pointless, risky work, similar to a coroutine collecting a StateFlow while the Activity is STOPPED and the screen isn't visible.
lifecycleScope is a CoroutineScope tied to a LifecycleOwner (an Activity or Fragment) that is automatically cancelled when the Lifecycle is destroyed — analogous to viewModelScope but bound to the UI component instead of the ViewModel. However, lifecycleScope.launch by itself does not pause collection when the component is merely stopped (only cancels on destroy), which is where repeatOnLifecycle comes in.
Cricket analogy: lifecycleScope is like a player's individual contract that's automatically terminated the moment they retire from the team (the Activity is destroyed), but merely being benched for a match (STOPPED) doesn't cancel it on its own — that's where repeatOnLifecycle comes in.
repeatOnLifecycle
repeatOnLifecycle(Lifecycle.State.STARTED) launches a new coroutine every time the Lifecycle reaches (or re-reaches) the STARTED state, and automatically cancels that coroutine when the Lifecycle falls below STARTED (i.e. becomes STOPPED). This means a Flow collected inside repeatOnLifecycle(STARTED) is actively collecting exactly while the UI is visible to the user, and stops collecting (freeing resources, pausing recomposition-triggering updates) the instant the screen is no longer visible — then automatically resumes collecting when the screen becomes visible again, without any manual bookkeeping.
Cricket analogy: repeatOnLifecycle(STARTED) is like fielders taking their positions the moment play resumes after a rain delay, and stepping off the field the instant bad light suspends play again — active exactly while the match is visible, automatically re-engaging when play restarts.
class ProfileFragment : Fragment() {
private val viewModel: ProfileViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
renderProfile(state)
}
}
}
}
}
// Compose equivalent: collectAsStateWithLifecycle wraps this pattern internally
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
minActiveState = Lifecycle.State.STARTED
)
ProfileContent(uiState)
}collectAsStateWithLifecycle in Compose
In Compose, collectAsStateWithLifecycle is the lifecycle-aware equivalent for collecting a Flow directly into Compose State, and it wraps exactly the repeatOnLifecycle pattern internally — it automatically starts and stops collection based on the current Lifecycle.State (defaulting to STARTED) of the nearest LifecycleOwner, without requiring any manual repeatOnLifecycle boilerplate in the composable itself. This is why collectAsStateWithLifecycle is preferred over the plain collectAsState in production Compose code: collectAsState keeps collecting regardless of whether the app is in the foreground, continuing to do work (and potentially trigger recomposition) even while the app is backgrounded.
Cricket analogy: collectAsStateWithLifecycle is like a stadium's video board that automatically stops refreshing when the ground is closed to the public and resumes on match day, unlike a plain feed that keeps refreshing pointlessly even when no one's watching, wasting power.
Think of repeatOnLifecycle(STARTED) as an usher at a theater who only lets the show (Flow collection) run while there's an audience in the room (the screen is visible). The moment the room empties (STOPPED), the usher pauses the show; when the audience returns (STARTED again), the usher restarts it from the current state rather than leaving it running to an empty room the whole time.
Using plain collectAsState() (without the lifecycle-aware variant) in a production Compose screen is a subtle but real problem: collection continues even when the app is backgrounded, wasting battery/CPU and, in rare cases, attempting a StateFlow update from a background-only context. Always default to collectAsStateWithLifecycle() in app code; reserve plain collectAsState() for tests or non-Android common code where there is no Android Lifecycle to hook into.
- Lifecycle-aware coroutines tie collection start/stop to specific Android Lifecycle states, avoiding wasted background work.
- lifecycleScope is cancelled on Lifecycle destruction, analogous to viewModelScope but bound to the UI component.
- repeatOnLifecycle(STARTED) restarts its block every time the Lifecycle re-enters STARTED and cancels it on STOPPED.
- collectAsStateWithLifecycle wraps the same pattern for Compose, defaulting to STARTED as the minimum active state.
- Plain collectAsState keeps collecting even when the app is backgrounded, unlike the lifecycle-aware variant.
- Use viewLifecycleOwner (not the Fragment itself) when collecting Flows inside Fragment view code to match the view's shorter lifecycle.
Practice what you learned
1. What does repeatOnLifecycle(Lifecycle.State.STARTED) do when the Lifecycle drops to STOPPED?
2. What is the Compose-idiomatic equivalent of wrapping Flow.collect in repeatOnLifecycle(STARTED)?
3. Why is plain collectAsState() discouraged for production screens compared to collectAsStateWithLifecycle()?
4. In a Fragment, which LifecycleOwner should you typically use when collecting a Flow tied to the Fragment's view?
5. What is the main risk of collecting a StateFlow inside a coroutine that ignores lifecycle state entirely (e.g. plain lifecycleScope.launch without repeatOnLifecycle)?
Was this page helpful?
You May Also Like
Kotlin Flow Fundamentals
Flow is Kotlin's asynchronous stream type for emitting multiple sequential values over time, built on coroutines, and central to reactive state management in Android apps.
stateIn and shareIn
stateIn and shareIn convert cold Flows into hot, shareable StateFlow and SharedFlow instances, with SharingStarted policies controlling exactly when the underlying producer runs.
App Lifecycle and Process Death
How Android activities and processes move through creation, backgrounding, and destruction, and why saving state correctly is essential to survive system-initiated process death.
Coroutines Basics in Android
Kotlin coroutines provide a lightweight, structured way to write asynchronous, non-blocking code on Android using suspend functions, coroutine builders, dispatchers, and scopes.