One Off the Slack: 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?

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. 🙄

Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!