https://kotlinlang.org logo
#compose
Title
# compose
l

Lukas K-G

02/21/2021, 9:12 PM
Beginners question here: When do I use
ViewModel
and
LiveData
and when should I use
mutableStateOf
?
1
z

Zach Klippenstein (he/him) [MOD]

02/21/2021, 11:35 PM
They’re not mutually exclusive. You could use view model with mutable state of
l

Lukas K-G

02/22/2021, 7:59 AM
Thanks for your answers. To me it looks like
mutableStateOf
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. ❤️
❤️ 1
z

Zach Klippenstein (he/him) [MOD]

02/22/2021, 4:06 PM
MSO and
LiveData
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).
So to apply this to your original question, because the LiveData api is just simple callbacks, it is a lot easier to work with when you don’t have a framework running and managing all these snapshots. It is a simpler implementation and a lot easier to actually dig into the code to figure out what it’s doing. States and snapshots provide a more ergonomic api for “feature” code, but require some ceremony on the outside. Compose does provide some high level apis to make this easier. I forget the name at the moment but there’s a function that takes a lambda and returns a
StateFlow
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.
The use of
remember
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:
Copy code
val stateFlow = remember { MutableStateFlow(0) }
val state = stateFlow.collectAsState()
Or whatever the equivalent for
LiveData
would be, instead of using
mutableStateOf
.
And there is lots of code that creates MutableStates inside regular classes and exposes them as delegated properties. It’s a common pattern to start with some remembers, then once you have more than a couple, extract all your state values out into a separate class and just remember an instance of that class. That class can then create and use its MutableStates internally.
l

Lukas K-G

02/23/2021, 10:24 AM
Thank you a lot for this great overview! This helps immensely.