State change completion cannot be called multiple times
Closed this issue · 17 comments
I've noticed my app crashed couple of times with this error message
FATAL EXCEPTION: main
java.lang.IllegalStateException: State change completion cannot be called multiple times!
at com.zhuinden.simplestack.NavigationCore$1.stateChangeComplete(NavigationCore.java:643)
at com.zhuinden.simplestackcomposeintegration.core.ComposeStateChanger$BackstackState$RenderScreen$2.invokeSuspend(ComposeIntegrationCore.kt:278)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.internal.ScopeCoroutine.afterResume(Scopes.kt:32)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:113)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7660)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
I have no reproduce steps, it seemingly happened out of nowhere (I was writing code when suddenly app crashed on the phone with no interaction).
Do you have any more information on why or how is this exception occurring?
This is with simple-stack-compose 0.3.0 and compose 1.0.0-beta05
Hey, first and foremost I sincerely hate the new Github notifications, because I was checking them and yet still didn't see it.
However, that is rather scary to hear. The only coroutine-related thing in the code is the LaunchedEffect
:
LaunchedEffect(key1 = completionCallback, block = {
if (isAnimating) {
lerping.animateTo(1.0f, animationConfiguration.animationSpec) {
animationProgress = this.value
}
isAnimating = false
lerping.snapTo(0f)
}
initialNewKey = topNewKey
previousKeys.fastForEach { previousKey ->
if (!newKeys.contains(previousKey)) {
saveableStateHolder.removeState(previousKey.saveableStateProviderKey)
}
}
completionCallback!!.stateChangeComplete()
})
The key
is the completionCallback
specifically, which means that unless that changes, the completionCallback
should be the same. However, if it changes, then the completionCallback
is different.
I do not see how you could possibly end up with multiple executions of the same LaunchedEffect
. I might have to re-implement this logic with DisposableEffect
and rememberCoroutineScope
.... 🤨
I'll try to investigate, but the answer here is not obvious at all. Theoretically it is impossible to get a new completionCallback while a state change is currently being executed, as any new state changes only execute once stateChangeComplete
is called.
2021-06-04 16:53:49.611 26002-26002/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.zhuinden.simplestackcomposedogexample, PID: 26002
java.lang.IllegalStateException: Offset is unspecified
at androidx.compose.ui.geometry.Offset.getX-impl(Offset.kt:67)
at androidx.compose.ui.unit.IntOffsetKt.plus-Nv-tHpc(IntOffset.kt:156)
at androidx.compose.ui.node.LayoutNodeWrapper.toParentPosition-MK-Hz9U(LayoutNodeWrapper.kt:442)
at androidx.compose.ui.node.LayoutNodeWrapper.localPositionOf-R5De75A(LayoutNodeWrapper.kt:351)
at androidx.compose.ui.input.pointer.Node.buildCache(HitPathTracker.kt:343)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:258)
at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:151)
at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:90)
at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-gBdvCQM(PointerInputEventProcessor.kt:77)
at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:767)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:465)
at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1849)
at android.app.Activity.dispatchTouchEvent(Activity.java:3993)
at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:423)
at android.view.View.dispatchPointerEvent(View.java:13689)
at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5482)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5285)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4841)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4807)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4947)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4815)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5004)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4841)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4807)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4815)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7505)
at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7474)
at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7435)
at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7630)
at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:188)
at android.view.InputEventReceiver.nativeConsumeBatchedInputEvents(Native Method)
at android.view.InputEventReceiver.consumeBatchedInputEvents(InputEventReceiver.java:178)
2021-06-04 16:53:49.612 26002-26002/? E/AndroidRuntime: at android.view.ViewRootImpl.doConsumeBatchedInput(ViewRootImpl.java:7581)
at android.view.ViewRootImpl$ConsumeBatchedInputRunnable.run(ViewRootImpl.java:7654)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:966)
at android.view.Choreographer.doCallbacks(Choreographer.java:790)
at android.view.Choreographer.doFrame(Choreographer.java:718)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:951)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
I broke Compose sooner than I broke simple-stack so far 🤔 (1.0.0-beta07, I think i'll try to update to 1.0.0-Beta08 before testing things)
I have not found a way to reproduce this, but I'll keep my eyes open.
I will report if I manage to reproduce it once again and I will try to be more observant on what exactly was I doing when it happens
Just fyi, I hit the same problem. Same stack trace. App had quiesced for some time after startup, no interaction with the app, then fatal exception with "java.lang.IllegalStateException: State change completion cannot be called multiple times!". Has happened a few times and I have not seen a way to reproduce. Never happens during interaction with the app. Compose beta09 and
const val CORE = "com.github.Zhuinden:simple-stack:2.6.2"
const val EXT = "com.github.Zhuinden:simple-stack-extensions:2.2.2"
const val COMPOSE = "com.github.Zhuinden:simple-stack-compose-integration:0.4.2"
@pandasys @matejdro I released 0.4.3 (Compose 1.0.0-beta09) just now with my original patch ideas therefore I think it should work. Please verify if you see anything strange
Sorry it took long, it's just really tricky to make changes when I can't verify their actual correctness, but I can't let it have random crashes either 🤷 my samples still continue to work, that's something for sure
Just had the same issue occur:
java.lang.IllegalStateException: State change completion cannot be called multiple times!
at com.zhuinden.simplestack.NavigationCore$1.stateChangeComplete(NavigationCore.java:643)
at com.zhuinden.simplestackcomposeintegration.core.ComposeStateChanger$BackstackState$RenderScreen$2$job$1.invokeSuspend(ComposeIntegrationCore.kt:287)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.internal.ScopeCoroutine.afterResume(Scopes.kt:33)
at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:102)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8512)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
const val CORE = "com.github.Zhuinden:simple-stack:2.6.2"
const val EXT = "com.github.Zhuinden:simple-stack-extensions:2.2.2"
const val COMPOSE = "com.github.Zhuinden:simple-stack-compose-integration:0.4.3"
const val VERSION = "1.0.0-beta09"
const val UI = "androidx.compose.ui:ui:$VERSION"
const val MATERIAL = "androidx.compose.material:material:$VERSION"
const val TOOLING = "androidx.compose.ui:ui-tooling:$VERSION"
I've reopened this but I'm kind of at a loss. I literally added a boolean check to ensure that even if it were called a second time, it'd just "nope out". If the coroutine is capable of being inside that if check twice at the same time, then what does it mean?
Does it actually just call it twice? How would that even happen? 🤔
Theoretically I'm not doing anything shady. But I do think there might be something about effects that I don't know. 😕 Am I supposed to add a Mutex here or something? But why are there two coroutines to begin with? Am I missing a remember over the job??
This actually makes me wonder if it is only possible if RenderScreen()
method is called twice with the same callback, but that should also be impossible
I actually still couldn't repro this.
Does anyone have any ideas how you manage to end up with 2 RenderScreen(
calls against the same completion callback?
Is the trick that the RenderScreen()
call is in another composable call with another argument that is changing? 🤔 It does NOT seem to happen to me with the dog example
Could you publish a version with logging on every crucial call? (println will do). Then, when we reproduce it again, we can provide these logs.
Well technically publishing it to Jitpack means I won't be able to remove the "instrumented" version later, it is almost easier to comment out the dependency in Gradle and just copy over the ComposeIntegationCore.kt
because it is 1 file
Oh wow I did not even see that there is only one file. Can you post a gist of this file with logs?
@matejdro definitely! It is now at https://gist.github.com/Zhuinden/d5c1325d7cec659d6b40522985f23143
Technically the services
has this one function in it and thats it:
@Composable
inline fun <reified T> rememberService(serviceTag: String = T::class.java.name): T {
val backstack = LocalBackstack.current
return remember { backstack.lookupService(serviceTag) }
}
I have a feeling it might have to do with needing a few val
s inside the effect, or at least that is my current guess 🤔
Thanks. I applied this version and will report if I get any crashes.
In the meantime, 0.5.0 swallows this. I think the issue should be resolved by me adding 2 extra val
s before the coroutine happens. If anyone experiences anything odd, please notify here.
Theoretically you won't see this. If you see any side-effect though, feel free to notify again 👀