For further actions, you may consider blocking this person and/or reporting abuse
Read next
Emulating classes with functions in Kotlin for maximum performance 🚀
CharlieTap -
Building Real-Time Chat with Kotlin Multiplatform and Stream Chat SDK
Zaiter -
iOS App Testing: A Comprehensive Step-by-Step Guide
Ronika Kashyap -
How I set up Design System for my React Native Projects for 10x Faster Development
M. Saad Ullah -
Top comments (25)
I found a typo: mutableSnapshotOf -> mutableStateOf, but more importantly: I don't understand what do you mean by "all writes are considered non-conflicting" in standard equality policies. I guess you've shown the write conflict in the example above. I also don't get how custom merge should report a failure. By returning null? But what if the mutable state type is nullable and we want to succeed with null result.
BTW if you're hesitating about next parts of this awesome series how about an example of custom simplified text "UI"? Sth like github.com/JakeWharton/mosaic but simplified to fit the blog post(s). It would be great to see how this snapshot system (and some custom applier I guess) should be used in such cases and why. Especially the "why". I find your explanations of intentions behind the compose parts especially useful.
Thanks, fixed the typo!
I am not sure of the answer to this unfortunately. Since the default return value is
null
, and in that case conflicting writes do fail, the wording "all writes are non-conflicting" is definitely confusing. I'll dig into this a bit more and try to clarify.Equality policies docs have been clarified since then. Looks like "structural" & "referential" are non-conflicting when new value is equal to an old one.
neverEqualPolicy
is always conflicting.So, if we change two pieces of
State
one after another on the main thread, these changes are guaranteed to become visible to observers "atomically" (in the same recomposition), but to achieve the same result from background thread we need something likewithMutableSnapshot
, right?@zachklipp Could you please clarify a bit on this point? If snapshot system has these atomicity guarantees, we can replace
StateFlow<SomeDataClass>.update { it.copy(...) }
with several separateStates
, greatly simplifying ViewModel code without sacrificing behavior correctness.BTW, will upcoming parallel (multi-threaded) composition change anything in this regard?
Generally, yes. It can often be a lot more natural and simpler to express multiple parts of state as separate properties vs having to wrap them all up into one immutable thing to shove inside a
MutableStateFlow
.Note that there are some subtle differences since
update
handles contention by simply re-running the function, whereas the snapshot system can sometimes resolve conflicts without re-running anything by merging conflicting values in a type-aware way. That said, in mobile apps contention is actually probably quite rare.Multi-threaded composition will probably have lots of interesting implications, but thread safety is thread safety – it shouldn't require any significant changes to how snapshots work.
Thanks for the detailed reply and for the warning about contention. It may be rare, but that means better chances of not catching it in tests and then having a rare crash with
SnapshotApplyConflictException
in production. ;)So, to recap:
SnapshotMutationPolicy
).MutableStateFlow.update()
does), for example:Composable
s won't see any partial updates), because Compose wraps compositions in snapshots.Correct?
Super useful and insightful. Looking forward to more on the inner workings of this mechanism!
The part about advancing global snapshots had me a little confused. Are global snapshots "automatically" advanced only when also paired with Compose UI (where composables + UI frames are involved)? i.e. manipulating mutable states in View toolkit world would require using something like withMutableSnapshot {} ?
Yes. If you're not using the rest of the Compose UI Android runtime, then nothing is advancing the global snapshot for you. Using
withMutableSnapshot
is one approach, although it's easy to forget to do that and you'd need to use it even in event handlers which would probably feel pretty boilerplatey. It would probably be better to set up your own callback to advance the global snapshot on every frame, similar to how Compose UI Android works under the hood.true
Ahh yes perfect, this was the missing piece for me. Trying to experiment with using these APIs in Fragments/Android Views for some interop cases. Thank you!
This article just blew my mind, I had no idea all this stuff is happening under the hood! It's so well explained here, I've got so many ideas of where this snapshot API could be used in some of my open source work!
One question though, what Gradle dependency would I need to play around with this API? Or, do you have a sample repo with the Gradle setup and some of these examples?
It’s all in the compose runtime artifact. But you don’t need to enable the compose compiler to use the snapshot apis. That’s a good question, I’ll add this information to the post directly.
Done!
Amazing post, can't wait for a next part about deep dive into inner workings! It still looks magical, like calling static methods and everything is somehow hooked up under the hoods. Unless your examples were intentionally simplified? I'm wondering how the entry point to all of this looks like. Anyways thank you for taking time and writing this down!
The samples are a bit contrived, and as noted I omitted dispose calls for all but the first, but other than that this is all working code you can run. There’s no other entry point - this is the entry point!
One of the reasons it looks like magic is because it’s using thread locals to pass information implicitly down the call stack.
why compose don't take a snapshot when call
Side Effects
api and not apply it if the effect not run successfully, consider I have LaunchEffect:shouldn't we roll back state1 and state2 if this effect cancel before it modify state3?
Amazing post! This not only drastically elevated my understanding of Compose, but also got the gears spinning in my head about other ways that the Snapshot system can be used... brilliant.
Also, I have been stuck on one part of this in particular and @zachklipp it would be really great if you have a chance to help with this.
In the part where you are talking about
SnapshotStateObserver
you say the following:And you have the following code:
However, I have been staring at the source code for the
SnapshotStateObserver
for hours, and can not understand why this should be done. As far as I can see, theSnapshotStateObserver
will keep observing the same values as long as you don't touch it. So, if you changed it to just:This should be fine. I will note though, that I guess this won't account for new values that need to be read, like if there is a mutating list of
MutableState
objects.I have tested this, and confirmed my understanding that calling
observeReads
is not necessary. Even without that, the listener continued to work.I think I see where you might have made the mistake when you said:
That is, I am guessing maybe you interpreted this from reading
ObservedScopeMap.notifyInvalidatedScopes
which at the bottom callsinvalidated.clear()
every time. Did you perhaps misread this as meaning that the states that were invalidated are "cleared" as in "listeners removed"? I assumed that was the case too based on what you wrote here, but after digging deeper it seems likeinvalidated.clear()
just marks the states that were previously marked as invalid, as valid again. I don't think it removes any listeners.I see a lot of "clear" methods in the class, but most of them are not called from other methods in the class, so it seems like clearing registered states is not an automatic process as you suggest?
I also realize Compose is still in development, and maybe it was different at the time that you wrote this?
Also, what is the advantage of SnapshotStateListener, which seems relatively complex, over
snapshotFlow
which seems more robust and elegant in so many ways?A great article! I have another doubt that 'How compose kotlin compiler plugin sense/observe State changes?'.
Does Android Compose leverage 'Bytecode enhancement' such as cglib to intercept State.getValue?
Good read :)
Great Post! Enjoyed it. Thanks