Why changing snapshot state from background thread...
# compose
t
Why changing snapshot state from background thread during composition causing a crash? 🧵
Copy code
@Composable
fun MyComposable(
    viewModel: MyViewModel = hiltViewModel()
) {
    Text(text = viewModel.myState)
}


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

    init {
        viewModelScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
            myState = "B"
        }
    }
}
The above code (myState= “B”) will crash the app saying
Copy code
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?
cc @Abhinav Sharma @Manideep Polireddi
cc @Adam Powell
f
How I understand this: It's not about changing state from a non-Main thread. That should be safe and OK. The problem is that you are changing a state that was not yet applied to (managed by) any snapshot and the history of these changes cannot be "saved" anywhere. Not sure how true that is but that is how I understand it.
t
but the error only comes if and only if we’re doing the state change from a background thread. isn’t it ?
f
Yes because if you are doing it from a main thread, the snapshot is already applied. But that does not generally mean that reading or writing to a State from a non-main tread is bad.
a
please don't do this kind of mention storm. we'll see threads here without people trying to light up our phones
1
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 fun`s 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
f
While you are here Adam, can you confirm that the way I think about it makes 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.
I might have it from some of Zachs articles about State but I am not sure if I remember it correctly.
a
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
f
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. Like a parent and adopted child 😅 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?
a
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
f
Right, I worded that badly
Coming back to the original question. I know why there is a problem and how to fix it. But to be honest I still don't understand why is that "problem" really problematic. Why does it matter? Sorry if I can't express myself clearly. English can sometimes be hard 😅
a
Nah, you're fine. The topic area of thinking in parallel time and transactions is the stuff of time travel sci-fi that makes your head hurt for fun. 🙂 Part of the reason why the best answers in a practical sense usually involve some form of, "don't do that"
😄 2
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.
m
Is there a way we can figure out which code is causing the issue/crash? We are getting this crash and we are not able to find out which state read(or which composable) is causing this.
a
The stack trace is the first place to start
m
Copy code
AndroidRuntime: java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.runtime.snapshots.SnapshotKt.readError(Snapshot.kt:1646) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.runtime.snapshots.SnapshotKt.readable(Snapshot.kt:1641) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.runtime.snapshots.SnapshotKt.readable(Snapshot.kt:1632) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.runtime.SnapshotMutableStateImpl.getValue(SnapshotState.kt:130) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.animation.core.AnimationScope.isRunning(AnimationState.kt:337) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.animation.core.SuspendAnimationKt.animate(SuspendAnimation.kt:260) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.animation.core.SuspendAnimationKt$animate$4.invokeSuspend(Unknown Source:17) 05-06 203722.524 17381 17381 E AndroidRuntime:     at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) 05-06 203722.524 17381 17381 E AndroidRuntime:     at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41) 05-06 203722.524 17381 17381 E AndroidRuntime:     at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:68) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1278) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.view.Choreographer.doCallbacks(Choreographer.java:1019) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.view.Choreographer.doFrame(Choreographer.java:907) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1248) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.os.Handler.handleCallback(Handler.java:900) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:103) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.os.Looper.loop(Looper.java:219) 05-06 203722.524 17381 17381 E AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:8668) 05-06 203722.524 17381 17381 E AndroidRuntime:     at java.lang.reflect.Method.invoke(Native Method) 05-06 203722.524 17381 17381 E AndroidRuntime:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) 05-06 203722.524 17381 17381 E AndroidRuntime:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)
The only indication is animation is involved. But unfortunately we aren't getting anything from our source
f
So when we talk about "State holder", what we really mean is "reference to the State in a timeline"? Because if it really was "just" a state holder, there wouldn't be a problem reading it from other thread. Or is is it another "you can think about it in that way but not really". It feels like it is both a value holder and reference to a timeline at the same time. Like photon being both wave and particle
Or it is a value holder, but it is managed by a Snapshot which forbids others from writing to it before committing so it can enforce the isolation of a Snapshot 🤔 Meaning the magic comes solely from the Snapshot and not the State
Sorry Adam, everything you say makes total sense but the concepts are still not clicking together in my head. Maybe I just have to sleep on it for it to make sense 😅
a
think of
mutableStateOf
as a key into a map held by the snapshot system. Reading or writing the value with that key will fail if you try to do it before the key is in the map, which doesn't happen until the snapshot where the key was created is committed
f
Yes, I understand that. So it is more like a reference than an actual value holder.
a
or in arrow terms, a lens that can be applied to the snapshot you're currently in
@Manideep Polireddi I think we may be reaching the limits of how deep we can branch into a different root cause for the same exception here without similarly branching a new thread, but I would look for any cases of launching into a different coroutine scope from inside some sort of compose animation scope
👀 1
further questions on your stack trace above should get their own thread
👍 2
f
Nice! Thank you Adam for the indepth explanation and your patience 🙇
👍 2
2
I am sorry to revisit this already long thread but this is actually not true, right?
because if you are doing it from a main thread, the snapshot is already applied
If we use the
Main.immediate
dispatcher (
viewModelScope
) in the
init
block, the State could still be read before the snapshot is applied. If I understand it correctly, the
Main.immediate
dispatcher does not schedule the job to the end of the queue but executes it immediately if already on main thread.
a
If it's executed synchronously by an immediate dispatcher than the uncommitted snapshot is still current and accessing the newly created snapshot state is valid
m
f
Oh, right. Of course 🤦‍♂️ Sorry 😅 So the sentence is still false because the snapshot is not applied yet but the result is the same. Thank you
z
I just wrote a post about how things like
MutableState
work, if you’re looking for a longer explanation: https://dev.to/zachklipp/implementing-snapshot-aware-data-structures-3pi8
❤️ 1
237 Views