How to handle Android lifecycle with ViewModel and StateFlow
Problem
When I started working with Android MVVM architecture, the lifecycle confused me. I had a ViewModel with a StateFlow, and I tried to collect it in my Activity:
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user)
lifecycleScope.launch { viewModel.state.collect { state -> // Update UI } } }}This collected my StateFlow even when the app was in the background. My UI kept updating when the screen wasn’t visible, wasting resources and sometimes crashing when views were detached.
The Android lifecycle has many states: onCreate, onStart, onResume, onPause, onStop, onDestroy. I didn’t know when to start or stop collecting my flows.
What happened?
I read on Reddit that “ViewModels exist to kinda make that simpler, the SDK does some lifecycle trickery to let them ignore a bit of that.” This sounded promising. The idea is that ViewModels survive configuration changes (like screen rotations), so they hold onto data while the Activity gets recreated.
But that’s only half the story. The ViewModel manages the data lifecycle, but the UI collection still needs to respect the Activity lifecycle.
I drew a diagram to understand the flow:
Activity States: CREATED → STARTED → RESUMED → (running) → PAUSED → STOPPED → DESTROYED ^ | UI should only update hereWhen the Activity is RESUMED, the UI is fully visible. When it’s STARTED, it’s visible but might be partially obscured. When it falls below STARTED (PAUSED, STOPPED), the UI is not visible.
The problem with my initial approach was that lifecycleScope.launch { viewModel.state.collect { } } keeps collecting in all states. This means:
- Wasted resources updating invisible UI
- Potential crashes when views are detached
- Battery drain from unnecessary processing
- Memory leaks if the coroutine outlives the Activity
How to solve it?
I first tried to manually manage the lifecycle:
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels() private var collectionJob: Job? = null
override fun onStart() { super.onStart() collectionJob = lifecycleScope.launch { viewModel.state.collect { state -> updateUI(state) } } }
override fun onStop() { super.onStop() collectionJob?.cancel() }
private fun updateUI(state: UserViewModel.State) { // Update views }}This worked, but it’s fragile. I need to remember to cancel the job in onStop, and if I have multiple flows to collect, the code gets messy quickly.
Then I found the flowWithLifecycle extension:
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user)
lifecycleScope.launch { viewModel.state .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .collect { state -> updateUI(state) } } }}This is much better. The flow only emits when the lifecycle is at least STARTED.
For Jetpack Compose, it’s even simpler:
@Composablefun UserScreen(viewModel: UserViewModel = hiltViewModel()) { // Automatically starts/stops collection based on lifecycle val state by viewModel.state.collectAsStateWithLifecycle()
when (state) { is UserViewModel.State.Loading -> CircularProgressIndicator() is UserViewModel.State.Success -> { val user = (state as UserViewModel.State.Success).user Text("Hello, ${user.name}") } is UserViewModel.State.Error -> { Text("Error: ${(state as UserViewModel.State.Error).message}") } }}The collectAsStateWithLifecycle function handles everything for me in Compose.
I also learned about repeatOnLifecycle for more control:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user)
lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // This block runs when lifecycle is at least STARTED // and is cancelled when lifecycle falls below STARTED launch { viewModel.state.collect { renderState(it) } } launch { viewModel.events.collect { handleEvent(it) } } } }}Use repeatOnLifecycle when you need to collect multiple flows together.
The reason
I think the key reason for this complexity is that Android has a complex lifecycle system, and reactive flows like StateFlow don’t automatically respect it.
The ViewModel simplifies part of this by surviving configuration changes. This is what the Reddit post meant by “lifecycle trickery.” The SDK manages the ViewModel lifecycle separately from the Activity lifecycle.
But the collection side still needs lifecycle awareness. Without it:
- Resource waste: You update UI that’s not visible
- Crashes: Views might be detached when the collector emits
- Battery drain: Unnecessary processing in the background
- Memory leaks: Coroutines that never cancel
The lifecycle-aware methods work by observing the lifecycle and automatically starting/stopping the flow collection:
When lifecycle >= STARTED: → Start collecting → Emit values to UI
When lifecycle < STARTED: → Stop collecting → Cancel the flow upstreamThis ensures the flow only emits when the UI can handle it.
Common mistakes to avoid
I made some mistakes along the way that you should avoid:
Don’t use deprecated launchWhenX methods:
// DEPRECATED: These only suspend but don't cancel upstreamlifecycleScope.launchWhenStarted { viewModel.state.collect { updateUI(it) }}Don’t collect in onResume/onPause manually:
override fun onResume() { super.onResume() job = lifecycleScope.launch { viewModel.state.collect { updateUI(it) } }}
override fun onPause() { super.onPause() job?.cancel() // Error-prone manual management}Don’t forget about StateFlow conflation:
// StateFlow drops intermediate valuesviewModel.state .onEach { println("Processing: $it") } // Might skip values .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .collect { state -> renderState(state) }StateFlow is conflated, meaning it only keeps the latest value. If you need every value, use a regular Flow instead.
Summary
In this post, I showed how to properly collect StateFlow from a ViewModel while respecting Android lifecycle. The key point is using lifecycle-aware collection methods.
For Jetpack Compose, use collectAsStateWithLifecycle():
val state by viewModel.state.collectAsStateWithLifecycle()For Activities/Fragments, use flowWithLifecycle():
viewModel.state .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .collect { state -> updateUI(state) }For multiple flows, use repeatOnLifecycle():
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { flow1.collect { } } launch { flow2.collect { } }}The ViewModel handles data persistence across configuration changes, but you still need lifecycle-aware collection to avoid updating invisible UI.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 StateFlow and SharedFlow documentation
- 👨💻 Collect flows with lifecycle awareness
- 👨💻 repeatOnLifecycle API
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments