One Off the Slack

These articles summarize the discussion on a particular thread in the #compose channel of JetBrains’ Slack workspace for Kotlin developers. If you would like to read the original thread, you will need access to that workspace — sign up at https://slack.kotlinlang.org!

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.

Posted 2021-06-12, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1623313858334200


Effects of Effects

len asked:

Is there any difference between LaunchedEffect and DisposableEffect when you don’t actually need any cleanup? I’ve seen in the official samples some DisposableEffects with an empty onDispose {} , and that makes me think LaunchedEffect should be a better option, but I’m in doubt 🤔

Google’s Adam Powell responded:

If you need to publish changes from composition to external objects you can use SideEffect

There are some trivial differences in overhead and one not so trivial difference in behavior between DisposableEffect and LaunchedEffect; LaunchedEffect has slightly more overhead from the coroutines machinery but if you’re noticing a difference from that then you’re using this pattern way too much. LaunchedEffect also won’t start running until the next frame due to coroutine dispatcher behavior, but DisposableEffect will run its setup code after composition apply but before measure/layout/draw happens for the current frame. Sometimes this can affect correctness or jank one way or the other but only for fairly rare cases.

If you only ever use effects to publish results of composition and not to try to affect the current composition, you’ll never notice a correctness issue from this

If an effect can synchronously alter inputs of both composition and layout/drawing then you can see a shear where for that frame, layout/draw will see new data but the composition will still reflect old data

len seemed grateful for the clarification:

Oh, I see! I knew LaunchedEffect was delayed by a frame, but I didn’t know DisposableEffect isn’t delayed…

My initial thinking was that they’re the same but one allows to dispose of something if the key changes or it leaves the composition

Posted 2021-06-06, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1622556885065600


Remember Your Flows

Michal Mlimczak had a problem with some code:

    //causes infinite recomposition
    val playerUiStateWrong by player.observeState()
        .map { PlayerUIState(it, sound) }
        .collectAsState(PlayerUIState(null, null))
        
    //works fine
    val playerState by player.observeState().collectAsState(PlayerState.Stopped)
    val playerUiState = PlayerUIState(playerState, sound)

I thought they would both do the same thing, but the first one, whenever I use playerUiStateWrong as a parameter in a modifier, causes infinite recompositions, whereas the second one works fine.

Google’s Adam Powell went back to a previous Slack thread to point out the problem:

Assembling an operator chain in a composable function will yield a new Flow instance each time it’s recomposed and collectAsState can’t tell that it’s semantically equal

In playerUiStateWrong, map() returns a new Flow. On each recomposition, that Flow will be different than the previous Flow, so the old collectAsState() will be cancelled and a new one invoked.

He went on to say:

If you remember the assembled flow based on its inputs you’ll see the same results as with your second snippet

Michal wondered if this would be fixed at some point… and Adam replied that this is not a bug, but a feature:

No, it’s by design. If collectAsState did not cancel an old subscription and start a fresh one when the flow being collected changes then its behavior would be incorrect when a new Flow is used, and Flows do not implement equality for collectAsState to tell the difference between a new and very different flow vs. a new but semantically equivalent one.

Michal then wondered if a Lint check could catch this sort of thing, since this might trip up newcomers to Compose. Adam was unconvinced:

Possibly, but it’s easy to defeat a lint check or create false positives for something like this. Consider the example from the linked thread a day or so ago; nothing says the ViewModel can’t or wouldn’t cache returned flows based on id, use shareIn internally, etc. It would catch only very basic cases

The motive for memoizing assembled flows is several layers of semantic meaning away from anything compose understands or arguably should understand

I’m not sure it would be an overall win for understanding the mental model of composition if things like flows/collectAsState did some sort of magic thing here that would be somehow different from the basic constructs anyone else would write themselves (

Posted 2021-05-29, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1622294790253600


More!

Older “One Off the Slack” articles can be found in the archives.