Skip to content

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:

UserActivity.kt - Bad approach
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:

Android Lifecycle Flow
Activity States: CREATED → STARTED → RESUMED → (running) → PAUSED → STOPPED → DESTROYED
^
|
UI should only update here

When 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:

  1. Wasted resources updating invisible UI
  2. Potential crashes when views are detached
  3. Battery drain from unnecessary processing
  4. Memory leaks if the coroutine outlives the Activity

How to solve it?

I first tried to manually manage the lifecycle:

UserActivity.kt - Manual lifecycle management
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:

UserActivity.kt - Using flowWithLifecycle
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:

UserScreen.kt
@Composable
fun 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:

UserActivity.kt - Using repeatOnLifecycle
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:

  1. Resource waste: You update UI that’s not visible
  2. Crashes: Views might be detached when the collector emits
  3. Battery drain: Unnecessary processing in the background
  4. Memory leaks: Coroutines that never cancel

The lifecycle-aware methods work by observing the lifecycle and automatically starting/stopping the flow collection:

Lifecycle-aware collection flow
When lifecycle >= STARTED:
→ Start collecting
→ Emit values to UI
When lifecycle < STARTED:
→ Stop collecting
→ Cancel the flow upstream

This 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:

Don't do this
// DEPRECATED: These only suspend but don't cancel upstream
lifecycleScope.launchWhenStarted {
viewModel.state.collect { updateUI(it) }
}

Don’t collect in onResume/onPause manually:

Don't do this
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:

Understand conflation
// StateFlow drops intermediate values
viewModel.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():

Compose UI observation
val state by viewModel.state.collectAsStateWithLifecycle()

For Activities/Fragments, use flowWithLifecycle():

Activity/Fragment observation
viewModel.state
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { state -> updateUI(state) }

For multiple flows, use repeatOnLifecycle():

Multiple flows observation
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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments