Is it possible to mutate two (or more) MutableStat...
# compose
j
Is it possible to mutate two (or more) MutableState variables in a single “transaction” ?
1
Or is this just nonsense because all composable callbacks happen inside a single “message” on the UI thread hence that act as “transaction”?
z
Yes, use
Snapshot.withMutableSnapshot
🙏 1
r
The explicit snapshot API aside, isn't what Marco said true (a callback is on the UI thread and hence an implicit transaction on MutableState variables)?
z
I can't actually remember off the top of my head if code running on the UI thread outside of an actual composition is in a non-global snapshot. But that only matters if you're mutating state from background threads concurrently.
r
My assumption has been, and I thought prior conversations on this channel have confirmed it, that a single callback can mutate multiple mutable state variables atomically. In other words, I assumed that the UI is not updated (recomposed) until the callback finishes.
c
From the little I understand about the Compose internals, I think when a state variable updates, it doesn't immediately update the UI, but rather schedules an update which is recomposed/applied for the next frame. So changes to multiple variables should all be applied to the next frame "atomically", assuming they were all made on the UI thread in the time between 2 frames. However, Compose is asynchronous by nature, and the expectation is that eventually recomposition will be multithreaded, so this may not always be the case and shouldn't be relied upon. It would be better to do some explicit action to force an atomic update (storing multiple variables in a single State object, or manually using the Snapshot API, for example).
r
Thanks for that explanation. If that is true then I would have to look closer at what I'm doing to guarantee atomicity in the long term. (I wish there was clear guidance on this in the docs). BTW, this was the thread I was thinking of (the most relevant part of the conversation occurs about 2/3 of the way down: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1618663666023500 ) In there Adam said "if you're running in a callback on the main thread this can't happen until your callback returns, so yes, changes made in that callback will be atomic with regard to that snapshot commit.." Maybe I misinterpreted that but that's where I'm coming from.
j
That's exactly what I meant, it's just that Adam is way better than me at saying it :) My assumption was that whenever the framework is giving us a chance to run code on the UI thread it will always be inside a “message” (queued on the UI thread’s Looper). So the framework (compose in this case) will have to wait until our message has finished running before resuming its own work. Of course this assumption works only if mutation of the state is done from the UI thread.
c
Just like coroutines, there's really nothing magical about Compose (beyond its magical compiler). Anything that is running at runtime is bound by the same rules as any other normal function:. On a single thread, functions are not interrupted, so if you do some long, expensive operation in a callback, it will ultimately block the thread until the callback function returns. In Compose, this would mean taking longer than 1 frame to recompose/apply a Snapshot, and you'll drop frames as a result. The things that would interrupt a thread are the "magic" of Compose and/or Coroutines: calling other such functions puts an interruption point in there, but the normal code between suspend/composable function calls is just normal code. The Compose documentation also has this bit which kinda confirms that, but also warns that compose functions run in parallel (although I don't think that is actually fully enabled just yet), and so you can't always be sure of what exactly is the "main thread" https://developer.android.com/jetpack/compose/mental-model#parallel
From my understanding, updating 2 normal `var`s would be an atomic update because they're just normal code, but updating 2
mutableStateOf
is moving into the Compose/Snapshot realm where things are much muddier and potentially multithreaded. However, that muddiness kinda goes away because you can trust that Compose is just going to do the right thing for you in that world, and you generally shouldn't have to think much harder about that
j
AFAIU If you update 2
var xyz by remember { mutableStateOf(123) }
from within a callback on the UI thread it will be atomic.
r
That's really what I want to confirm, and that that will always be true even if recomposition were to become multi-threaded.
j
From Adam’s words in that thread you linked I’m pretty sure it will still be the case. But you can mention Adam to double check ;)
a
We may end up needing to take the recomposition snapshot on the Recomposer's applier thread (i.e. the main thread) to keep some consistency expectations like the ones you're making here even if the actual recomposition is happening in parallel. Implementation details aside though, it's our goal to not make you have to think about this too hard if your event handling code is happening on the main thread. That is, after all, why the global transaction mechanism exists.
👍 1
If you're off the main thread and you're not implicitly in a snapshot because something else took one on your behalf, we assume you know what you're doing and can use withMutableSnapshot and such to maintain the guarantees you need. 🙂
🙏 3
👌 2