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>
andMutableStateFlow<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 andcompose-runtime
includes other things is like declining to usesuspendCancellableCoroutine
because the same artifact includesFlow
.
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 usingStateFlow
, 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 onSnapshot
’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 usingFlow.collectAsState
, you’re not usingFlow
instead of snapshot state, you’re usingFlow
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!