This isn’t another “how to expose state from a ViewModel” article. This is for those interview moments when the questions start simple — you explain how LiveData works, then Flow — and then comes the follow-up: Can Flow actually behave like LiveData?

It’s a reasonable question. The code might look fine. But the behavior? That depends — unless you know exactly how stateIn, lifecycle, and sharing strategies interact.

This article focuses on subtle but important differences in how ViewModel state works — what survives configuration changes, what survives process death, and what doesn’t.

Question 1: Can you make a Flow behave like LiveData?

At first glance, it seems like a reasonable idea. Both Flow and LiveData deliver observable data from a ViewModel to the UI.

So can you make a Flow behave just like LiveData — only collecting when there’s an active observer, and stopping when there isn’t?

Sort of. But not by default — and not without caveats.

Why this is a trick question

By default, Flow is cold and lazy. But once you turn it into a StateFlow using stateIn(), it becomes hot — and unless you configure it carefully, it may start collecting immediately and never stop.

LiveData starts and stops automatically based on active observers. StateFlow, on the other hand, keeps collecting as long as its scope is active — even if no one is reading its .value.

If you’re not careful, this can lead to background work continuing long after the UI is gone.

What actually happens

Let’s say you define this in a ViewModel:

val state = flow {
    emit(loadData())
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = InitialValue
)

This flow starts immediately — as soon as the ViewModel is created — even if no composable is on screen.

Now compare that to:

val state = flow {
    emit(loadData())
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = InitialValue
)

Here, the upstream flow starts only when someone collects, and stops after the subscriber disappears (with a short timeout). This behaves much closer to LiveData.

Why this question exists

It tests whether you:

  • Know that stateIn creates a hot flow
  • Understand that default StateFlow behavior differs from LiveData
  • Can control when your flow starts and stops — especially for expensive upstream sources (network, DB)

What to remember

  • LiveData starts/stops automatically — StateFlow doesn’t, unless you use SharingStarted.WhileSubscribed()
  • stateIn(…, Eagerly) → always running
  • stateIn(…, Lazily) → starts once, never stops
  • stateIn(…, WhileSubscribed()) → starts/stops based on active collectors

Bonus insight: What if you use collectWithLifecycle()?

collectWithLifecycle() ensures your collector follows the UI lifecycle — for example, stopping when the composable leaves the screen.

But it does not control the upstream flow.

If you’re collecting from a StateFlow created with SharingStarted.Eagerly, the flow has already started. collectWithLifecycle() simply controls whether the UI receives updates — not whether the flow is running.

To truly mimic LiveData behavior:

Use stateIn(…, WhileSubscribed()) in the ViewModel Use collectWithLifecycle() in the Composable

That way, both producer and consumer are lifecycle-aware.

Question 2: Where rememberSaveable and SavedStateHandle actually differ

At first glance, both rememberSaveable and SavedStateHandle seem to solve the same problem: preserving state across configuration changes and process death.

They both work — but not in the same way, and not in the same place.

That difference matters more than it seems.

Why this is a trick question

It’s easy to treat rememberSaveable as a simpler alternative to SavedStateHandle. Just pass a value into rememberSaveable, and it survives rotation. Why add extra code in the ViewModel?

But the two APIs operate in completely different layers:

  • rememberSaveable stores state in the UI layer — in a Bundle tied to the composable
  • SavedStateHandle stores state inside the ViewModel — available even before any UI is drawn

When the screen is destroyed and recreated — both can restore state. But after process death, only SavedStateHandle is guaranteed to survive independently.

What actually happens

Let’s say you store the selected tab index using rememberSaveable:

var tabIndex by rememberSaveable { mutableStateOf(0) }

It works across rotations — but only after the composable has been recomposed.

Now compare that to managing state inside the ViewModel:

class MyViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    val tabIndex: StateFlow<Int> = savedStateHandle.getStateFlow("tab_index", 0)

    fun setTab(index: Int) {
        savedStateHandle["tab_index"] = index
    }
}

And in Compose:

@Composable
fun TabScreen(viewModel: MyViewModel = viewModel()) {
    val tabIndex by viewModel.tabIndex.collectAsState()
    
    // UI that uses tabIndex
    TabRow(selectedTabIndex = tabIndex) {
        // ...
    }
}

This approach survives process death and is available immediately — even before the screen is drawn. It also keeps business logic in the ViewModel, where it belongs.

Why this question exists

It checks whether you:

  • Understand where state belongs — UI vs ViewModel
  • Know which solution is lifecycle-safe and process-safe
  • Realize that rememberSaveable can’t always recover your state when you need it

What to remember

  • rememberSaveable works inside the composition — it’s for UI-level state
  • SavedStateHandle works in the ViewModel — it survives process death and doesn’t depend on recomposition
  • For UI-only things (scroll, text), use rememberSaveable
  • For logic, identity, and navigation state — use SavedStateHandle

Original Link