Zhuinden/simple-stack-compose-integration

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 vals 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 vals 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 👀