I'm starting to forget some compose fundamentals l...
# compose
c
I'm starting to forget some compose fundamentals lol. if i do this
Copy code
fun updateStuff(x: String, y: String, z: String ){
email = x
phone = y
address = z
}
email, phone and address is snapshot state is that 3 recompositions? or just 1 recomposition
For example, if I just had a state class that was setup as a single mutableStateOf, and I adjusted all of those fields at one time and set it to person, would that be 1 recomposition or technically do all of the composables recompose, so that would be 3 recompositions anyway?
f
If it's on main thread, it's one recomposition
c
how does it know to do it in one shot. lol. or is it that it'll batch as many as it can in a single frame before dispatching?
f
there would have to be another thread involved. When you are executing a normal function, code generally cannot just stop executing in middle. That's what coroutines are for
c
interesting. okay. i guess i was thinking in the extreme case (like lets say I had 1000 fields being set) would all of that just be executed in one change. i think i gotta pull out android studio composition count and see what the counts look like for one state class with multiple mutaableStateOf vs one mutableStateOf(your state class).
could have sown @Chuck Jazdzewski [G] said to prefer one state class with multiple mutableStateOf's lol
f
It does not matter how many changes are made in a function. If you are on the main thread and changing thousands of values, it will just cause jank. Compose cannot simply interrupt you in the middle and display another frame with only half of the data.
💯 1
👆 1
c
interesting TIL. thanks @Filip Wiesner
👍 1
p
Even from multiple threads you might be lucky and have the updates all in one composition frame. If they happen to be in the same 16ms time frame
r
it was the same with Views. If you called 1,000 setters one after the ohter, only one relay out/redraw would be scheduled
c
gotcha. basically a dev on my team said "hey instead of having 3 separate mutableStateOf and having 3 recompositions, I'm creating a single state class (that has 3 fields) and the state class is mutableStateOf so we save on composition count" and that threw me in a loop. i was like. wait. thats not how this works... right? lol
p
Your UI will dictate. For a form is better one class state. If your UI has 3 input text is simpler 3 different states. You can have also a class that contains MutableState forming a MutableState compound cluster. This is efficient in collections where the item cell UI updates very often
j
@Colton Idle Even though Compose will amortize/batch “update” calls isn’t that more of a implementation details and not something you should assume? I would argue updating multiple fields at once via your Ui state classes
.copy(…)
function is the more technically correct approach (assumes your Ui state object is a
data class
)? It guarantees UI state changes are atomic and it would be slightly more performant instead of 3 separate calls
MutableState.setValue(…)
with the individual changing values, which will not be amortized (only the “scheduled UI draw”).
m
There is also general perf consideration. Updating mutable state isn't free. There is a droidcon talk from Andrei Shikov that explains the cost of mutable state, derived state etc
c
No formulation of
updateStuff
will cause 3 recompositions. When a change is made in the global snapshot it triggers an eventual "advance" of the global snapshot that will send the set of all changes since the last time it advanced to all listeners. The
Recomposer
, upon receiving the first such notification since the last frame, will schedule a new frame (assuming any of the changes were observed by a composition). Until the frame starts, all changes will accumulate in the
Recomposer
until the frame starts. When the frame starts the
Recomposer
request the snapshot system to send, synchronously, any pending changes. Then a snapshot is taken and all external changes are ignored while a composition is running (using snapshot isolation). Any changes that occur during the composition will be treated as a change that occurred between the current frame and the next one (i.e. it will cause the
Recomposer
to loop and request another frame). This naturally batches changes that occur in the global snapshot and they are processed on the cadence of the monotonic frame clock of the
Recomposer
which is driven by the `Choreographer`in Android. By creating a snapshot the values of all mutable state are consistent with a single point in time. If you have a background thread and want all the changes to appear atomically to the composition, make the changes in a mutable snapshot and apply the changes when complete. The changes are isolated from the global snapshot until the snapshot is applied and then they appear as one atomic change. In other words, if
updateStuff()
is called in snapshot in a background thread you will never see a state where the update is partially complete. Either you will see all the changes of
updateStuff()
or none of them. Creating a single mutable state object that contains all the values, or mutable state for each field, does not affect the atomic, isolated and consistent properties of the object. All mutable state objects have these properties and are atomic, isolated and consistent based on the current snapshot they are viewed from. Also, as noted above, changing any or all the mutable state objects between frame will be process by the next frame all at once. As noted above, this is not free. Writes to the global snapshot are observed by the
GlobalSnapshotManager
whose job it is to schedule the change notifications. Reads are observed by the composer and the modifiers (during layout and draw) which tracks the reads in hash tables to be able to map the recompose scope or modifier node that was affected by a change. Also, to provide isolation, a "state record" is created (or reused) to store the isolated state of a snapshot for any mutable state object that is written to.
👍 1
j
@Chuck Jazdzewski [G] If, for example, I have 3 mutable state objects instead of encapsulating them in one state object that is exposed as a mutable state, and I update them in
updateAll()
one after another, is it at all possible for a snapshot to be taken in-between the individual calls and the UI reflects a briefly inconsistent state? More specifically, if
updateAll()
is called from a non-UI thread?
c
Yes. The global snapshot trades consistency (seeing intermediate results which may be inconsistent) for availability (being able to write at any time without conflict). A mutable snapshot trades availability (applying the snapshot may fail when there are conflicts) for consistency (no intermediate results). State merging can help mitigate availability but conflict-free data types are not common and are not easy to use. If you have clear write ownership (one thread/coroutine can write, any thread/coroutine can read) conflicts can also be avoided. To avoid seeing intermediate results you have a few choices. 1) create a snapshot in the background thread and apply it as described above, 2) bundle the values in a single immutable object and and use one mutable state object to contain the current state. 3) synchronize by some other means (e.g. guard reads and write with a synchronized block). Option (2) avoids much of the overhead of snapshots by only paying for snapshot overhead once, for the entire object, instead of once for every field, but usually at the cost of additional allocations. However, option (2) doesn't detect data races that option (1) does. Option (3) prevents data races, instead of detecting them. Also, with (2), often the the reader may have to inspect the new value closely to avoid over-invalidation. For example, in composition we may over compose for an immutable object because we see it changes somewhere high in the call graph and have to execute all the code that passes it as a parameter even if the fields they read are unchanged (e.g. changing `email`but not
phone
or
address
may still invalidate the component displaying
address
but not
email
). This is partially mitigated by slots and lambda capture (we invalidate the lambda invocation, not all the code between the lambda creation and the invocation). For example, capturing the immutable object the content lambda of a
Button
will invalidate just the
content
lambda, not the
Button
itself, If each field, on the other hand, where a
mutableStateOf
only the reader of
email
would be invalidated. In general, my recommendation is, if values always change together, make them one value in a single
mutableStateOf
. If values change independently, use a
mutableStateOf
for each independent value. This is not a hard rule, more of a guideline which should be validated by tracing and adjusted accordingly, as it can very greatly depending on the structure of the data and how it is read.
👏🏿 1
👏 1