Lukas K-G
02/21/2021, 9:12 PMViewModel
and LiveData
and when should I use mutableStateOf
?Cicero
02/21/2021, 9:29 PMZach Klippenstein (he/him) [MOD]
02/21/2021, 11:35 PMLukas K-G
02/22/2021, 7:59 AMmutableStateOf
is more or less exclusively used in combination with remember{}
inside of composable functions. This is at least what I see from the sample projects.
But I did not find any sufficient explanation about why that is the case. Maybe this is my lack of understanding for LiveData
, for me it seems to be the same as the state but more complicated.
I will try to read into this a bit more.
OT: Shoutout to @Zach Klippenstein (he/him) [MOD] for answering every single one of my questions on this channel (and so many others I have read). You are the real MVP here. ❤️Zach Klippenstein (he/him) [MOD]
02/22/2021, 4:06 PMLiveData
essentially solve the same problem: they’re a holder for data that can change over time, and that allows code to observe when the data changes. They use very different mechanisms for this.
LiveData
is essentially a simple implementation of the standard observer pattern. You can add/remove listeners, and when the data changes all the currently registered listeners get invoked.
MutableState
allows observers too, but it does so without an explicit subscription api. MutableState
is a subtype of State
. State
is the read only interface, MS provides a write api as well. `State`s are backed by snapshots.
The way snapshots work are a little complicated, but basically that means that when you read a State
value you’re seeing the current snapshot of the value. Different callers (ie on different threads) might see different values of the same State
at the same time. Compose provides a low level api that basically takes a lambda and a callback, and notifies your callback whenever any of the code inside the lambda reads a State
value. This basically lets you construct a set of all the `State`s that need to be “observed” in that lambda. Let’s call this a snapshot reader, we’ll come back to it later.
When you write to a MutableState
, that write won’t be seen by other threads that are looking at their own snapshots - yet. All state writes occur in a mutable snapshot - either an explicit one using an api similar to what’s described above, or the “global snapshot” that is the default snapshot for all writes that happen outside an explicit snapshot. Either way, the snapshot must be asked to apply its changes in order for any other threads to see them. When a mutable snapshot is applied, all the other snapshot readers get notified about which `State`s have changed. The readers can then make a decision about whether or not they need to re-execute their lambda to read the new values.
The whole snapshots api, as fancy as it is, is implemented in regular code and doesn’t actually need the compiler plugin. You could use the snapshot api in your own framework without using any of what most people consider to be “Compose” (UI or not).
In Compose, IIUC, each “recompose scope” is a snapshot reader. A recompose scope is any composable function that returns Unit
(and thus can be re-invoked arbitrarily without the knowledge of its callers). The actual code that implements this is part of the magic that the compose compiler plugin does. The compose runtime keeps track of all State
reads that take place while executing a recompose scope, and when those states change schedules that scope for recomposition before the next frame is generated.
This is a super rough high level description that probably gets a few technical details wrong, but is hopefully enough to start to reason about how they work. The way I think of the difference between this snapshot system and traditional observer pattern is that it moves the mundane and error-prone job of managing subscriptions from the code that is implementing business logic to the framework (eg Compose). This makes for much cleaner business logic since there are no subscription callbacks cluttering it up, and you don’t have to worry about forgetting to dispose a subscription and leak memory (LiveData
tries to solve this by integrating tightly with Lifecycle, but it’s still possible to mess it up).Zach Klippenstein (he/him) [MOD]
02/22/2021, 4:11 PMStateFlow
of values returned by the lambda, where a new value will be generated and emitted every time a State
that your lambda read was changed. This could probably be useful for unit tests.Zach Klippenstein (he/him) [MOD]
02/22/2021, 4:16 PMremember
with MutableState
is a bit of a red herring - remember
is just needed to memoize a value across recompositions. You could also do something like:
val stateFlow = remember { MutableStateFlow(0) }
val state = stateFlow.collectAsState()
Or whatever the equivalent for LiveData
would be, instead of using mutableStateOf
.Zach Klippenstein (he/him) [MOD]
02/22/2021, 4:45 PMLukas K-G
02/23/2021, 10:24 AM