One Off the Slack: MutableState vs. MutableStateFlow

Frankelot asked what probably seemed like an innocuous question:

Hey, I’m seeing some overlap between MutableState<T> and MutableStateFlow<T>, when should I use each?

seems like I can use either one and just use collectAsState() 🤔 but I’m sure each has their own intender purposes

There was a fair amount of discussion between community members, with some advocating for MutableStateFlow (for consistency with non-Compose uses) and others plugging MutableState (since in the end it and the snapshot system is what powers Compose).

…and then Google’s Adam Powell showed up and wrote a blog post in the form of a series of Slack messages:

There are many reasons to use [Mutable]StateFlow over snapshot [Mutable]State but, “it’s compose-only” isn’t one of them any more than, “it’s kotlinx.coroutines-only” should be a reason not to use Flows. Snapshots are lower-level than the compose compiler plugin or any other compose-runtime machinery and can be used without any of it.

Declining to use snapshots as a tool on the grounds that it’s shipped in the compose-runtime artifact and compose-runtime includes other things is like declining to use suspendCancellableCoroutine because the same artifact includes Flow.

Now that said… 😄

Snapshots are about state and state alone, it’s a MVCC system that happens to be observable at the whole-snapshot level, and when a snapshot is committed you can see which individual elements of the snapshot changed. This is the basis for the snapshotFlow {} API - you can get a cold flow from any block of code or expression that reads snapshot state and it will bridge the two, no @Composable or compose compiler plugin required.

A great many usages of [Mutable]StateFlow out there would be better served by snapshots.

That said, snapshots aren’t a substitute for things like .stateIn(SharingStarted.WhileSubscribed)

they also aren’t a substitute for cases where timeliness of collectors being notified is important as opposed to an, “update soon, but eventually” model. For example, Compose’s own Recomposer exposes current state information using StateFlow, not snapshot state. It’s a better fit for a bunch of timing and isolation-related reasons.

As for testability, personally I find snapshots to be as easy or easier to test than flows, including in isolation and in host-side tests.

You’re in control over when the global snapshot transaction is committed (Snapshot.sendApplyNotifications()), you can perform your own isolated transactions of several changes all at once that you can then examine and assert on atomically, regardless of any other concurrency setup that might be active (the other APIs on Snapshot’s companion object itself)

when you’re working with viewmodel-like objects (I hedge the description to make a distinction of the concept, as opposed to subclasses of android’s ViewModel class) and consuming its state from compose using Flow.collectAsState, you’re not using Flow instead of snapshot state, you’re using Flow and snapshot state - collectAsState collects the flow into a snapshot state object for compose to react to. When you do this you lose the atomic transaction properties of snapshots.

With snapshots if you change several snapshot state objects in a transaction, observers will see those changes together, atomically. When you collect several different flows into snapshot state objects, there is no transaction tying emits from those different flows into the same atomic change. They’re all separate.

If you have a ViewModel-like object with a handful of MutableStateFlows tracking state that get observed by UI code, it would almost certainly be a better fit for snapshot state instead.

Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!