One Off the Slack

These articles summarize the discussion on a particular thread in the #compose channel of JetBrains’ Slack workspace for Kotlin developers. If you would like to read the original thread, you will need access to that workspace — sign up at https://slack.kotlinlang.org!

Are Large State Classes Performant?

Aaron Waller asked:

Does holding the screen state in large data classes negatively affecting the performance?

Aaron then points to a Stack Overflow question, where the asker worried about whether having a single view-state class with lots of properties might be worse than having several smaller ones. Basically, it boils down to whether recompositions will be more frequent or more expensive if the compositions react to changes in larger state objects.

Google’s Leland Richardson weighed in, with an answer of “its complicated”:

there’s sort of a tradeoff here, so unfortunately there’s no “do this, don’t do this” sort of answer.

On one side of the scale, where the size of the data class gets very very large, the answer is this may not be optimal if you’re just changing one property at a time. If the entire data class instance is being passed around through many composable scopes then this means they will all become invalid, since the data class instance is not equal to its prior value. on the other hand, if you’re just passing individual properties down to other scopes, then it may be fine, considering those values will have remained unchanged.

on the other side of the scale, you can imagine a very large number of mutable state objects, one for each tiny little piece of state in your app. On the one hand, this has nice properties in that when a small piece of that data is mutated, compose invalidates only in the scopes where each small little piece is used.

on the other hand, mutable state objects have some overhead, so you’ve added that overhead for every tiny piece of state

in general i am a fan of using immutable (all val) data classes to hold on to small-ish groups/collections of state that are semantically grouped together. so in other words, somewhere in the middle of that spectrum

finally, there may be some situations where state might be semantically coupled but it is beneficial to isolate one piecce of state into a mutable state object because it is being updated very rapidly and you want to minimize the scope of recomposition as much as possible. a value getting animated continuously is a good example of such a thing

IOW, focus less on the actual size of the state object and more on how much of the object’s content changes on a per-change basis, and how frequent those changes are.

Posted 2022-05-21, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1652555947214209


Is Mutating State From a ViewModel Constructor OK?

TL;DR: don’t do that.

theapache64 asked:

Why changing snapshot state from background thread during composition causing a crash?

@Composable
fun MyComposable(
    viewModel: MyViewModel = hiltViewModel()
) {
    Text(text = viewModel.myState)
}


class MyViewModel : ViewModel() {
    var myState by mutableStateOf("A")
        private set

    init {
        viewModelScope.launch(Dispatchers.IO) {
            myState = "B"
        }
    }
}

The above code (myState= “B”) will crash the app saying

Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied

PS: I read this thread but I don’t fully understand “why” the runtime had to throw that crash? Why it can’t handle the scenario gracefully ? Maybe update the UI with the new state (“B”)? And does it only happen during composition?

Then, theapache64 did something unfortunate: using @ notation to ping a bunch of people directly, including Google developers. Almost every Google employee contributing to Kotlinlang Slack is doing so voluntarily. As Google’s Adam Powell put it:

please don’t do this kind of mention storm. we’ll see threads here without people trying to light up our phones

Adam then went on to explain the problem with the code:

Since your ViewModel’s constructor has side effects you’ve leaked those side effects out of the composition snapshot and attempted to interact with that composition snapshot from outside before it’s been committed

ideally viewmodel construction should not have these kinds of side effects. This is why you’ll often see viewmodels with suspend funs invoked by LaunchedEffect and similar, or use of stateIn(..., SharingStarted.WhileSubscribed(), ...); the viewmodel itself is generally inert unless acted on from outside

when a viewmodel has side effects of construction then it needs to concern itself with cases like the above, or rolling back those side effects if the composition it’s created in fails to apply, or ensuring those side effects don’t have lingering observable impact

Filip Wiesner then wanted confirmation that this statement (from earlier in the thread) made sense:

The problem is that you are changing a state that is not yet managed by any snapshot and the history of these changes cannot be “saved” anywhere.

Adam responded:

close enough to be a useful mental model. Think of it kind of like leaking this of a non-final class during construction; non-null things can be observed to be null, writes will be overwritten when the real constructor actually runs, lots of bad times

Filip was still confused:

I think that I still don’t understand the relation between State and the Snapshot 😞 Even after reading a lot about it. I thought of State being “just a dumb value holder” until some Snapshot takes control of it, observing and managing it’s changes.\

But from the way you are describing it, it feels more like they know already about each other but the parent is not yet ready to manage it until some preparations are completed (first composition?).

Does the question/comparison make sense?

Adam tried to clarify further:

Composition only matters insofar as composition is performed in a mutable snapshot that gets committed when recomposition is complete, it’s the snapshot transaction that matters

When you create a snapshot state holder, you’ve created it in the current snapshot. If that’s the global snapshot then it’s effectively created immediately, but if you’re in a snapshot that isn’t committed then the snapshot state holder doesn’t really exist until the snapshot is committed

So if you send (leak) a reference to it somewhere else and try to manipulate it from outside of that snapshot, it doesn’t exist until the original snapshot is committed so you can’t

That’s what’s happening in the OP, the ViewModel is launching a coroutine that runs on a different dispatcher on a different thread, and on that thread the composition snapshot isn’t the current snapshot

The composition snapshot isn’t committed yet, so when the background thread tries to access a state object that doesn’t exist in that timeline, boom.

There is more to the thread, including pointing out how the stack trace for this problem is not super-informative. 🙄

Posted 2022-05-16, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1652084646064769


StateFlow or State?

Jorge Dominguez asked:

Checking the Jetnews sample app there’s this code snippet that allows state collection from a composable:

private val viewModelState = MutableStateFlow(HomeViewModelState(isLoading = true))

val uiState = viewModelState
    .map { it.toUiState() }
    .stateIn(
        viewModelScope,
        SharingStarted.Eagerly,
        viewModelState.value.toUiState()
    )

My question is, what’s the advantage of using stateIn() here? I would normally do something like:

private val _viewModelState = MutableStateFlow(HomeViewModelState(isLoading = true))
val viewModelState get() = _viewModelState

But looking into the stateIn() docs there’s a mention to increased performance for multiple observers since the upstream flow is instantiataed only once, but what if the flow is collected from a single composable? is there really an advantage there?

I was thinking that each recomposition can be considered as a new observer, in which case I can see how the use of stateIn() helps, but I’d like to fully understand the implications of its usage and how it’s better, so if anyone can shed some light I’d be grateful.

Google’s Adam Powell was unimpressed with that code:

it’s trying to satisfy conflicting requirements: the source of truth isn’t a UiState, it’s something that gets mapped to one, and if you don’t have a StateFlow then you don’t have an initial value available, you have to subscribe and wait for it to emit, and in the case of consuming from compose that means you get a frame of empty data before the first item is known.

the whole thing could instead be written as:

private var viewModelState by mutableStateOf(HomeViewModelState(isLoading = true))
val uiState: UiState
  get() = viewModelState.toUiState()

to leverage snapshots instead and skip all of the subscription complexity, since the upstream source of truth is a MutableStateFlow anyway - a hot data source that doesn’t care about subscription awareness in the first place.

Jorge wanted a bit more clarification, though, on part of his original question:

do you think the statement “…each recomposition can be considered as a new observer” is true?

And the answer to that is “no”, as Adam explained:

the call to .collectAsState is an observer for as long as it remains in composition with the same receiver flow instance

the same underlying observer (call to Flow.collect persists undisturbed across recompositions for as long as the call to .collectAsState is present in the composition for the same Flow instance

Posted 2022-05-09, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1651856934895499


More!

Older “One Off the Slack” articles can be found in the archives.