KaustubhPatange / kapture

A small library for Jetpack Compose to capture Composable content to Android Bitmap.
Apache License 2.0
13 stars 0 forks source link

Crash: Detected multithreaded access to SnapshotStateObserver #3

Closed nordfalk closed 4 weeks ago

nordfalk commented 4 weeks ago

Using Kapture, sometimes our app crashes with this exception:

java.lang.IllegalArgumentException: Detected multithreaded access to SnapshotStateObserver: previousThreadId=854), currentThread={id=2, name=main}. Note that observation on multiple threads in layout/draw is not supported. Make sure your measure/layout/draw for each Owner (AndroidComposeView) is executed on the same thread.
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.o(SnapshotStateObserver.kt:82)
    at androidx.compose.ui.node.OwnerSnapshotObserver.i(OwnerSnapshotObserver.kt:3)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.a(NodeCoordinator.kt:32)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:3)
    at androidx.compose.ui.platform.e4.record(RenderNodeApi29.android.kt:37)
    at androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList(RenderNodeLayer.android.kt:45)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:88)
    at android.view.View.draw(View.java:23348)

And additionally this exception get thrown:

android.util.AndroidRuntimeException: Animators may only be run on Looper threads
    at android.animation.ValueAnimator.start(ValueAnimator.java:1135)
    at android.animation.ValueAnimator.start(ValueAnimator.java:1189)
    at android.graphics.drawable.RippleAnimationSession.startAnimation(RippleAnimationSession.java:192)
    at android.graphics.drawable.RippleAnimationSession.enterSoftware(RippleAnimationSession.java:218)
    at android.graphics.drawable.RippleAnimationSession.enter(RippleAnimationSession.java:73)
    at android.graphics.drawable.RippleDrawable.drawPatterned(RippleDrawable.java:907)
    at android.graphics.drawable.RippleDrawable.draw(RippleDrawable.java:804)
    at android.view.View.drawBackground(View.java:23591)
    at android.view.View.draw(View.java:23328)
    at androidx.compose.material.ripple.AndroidRippleIndicationInstance.drawIndication(Ripple.android.kt:101)
    at androidx.compose.foundation.u.draw(Indication.kt:3)
...
    at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:52)
    at android.view.View.draw(View.java:23348)
    at androidx.core.view.ViewKt.a(View.kt:39)
    at com.kpstv.compose.kapture.ScreenshotController$captureToBitmap$2.invokeSuspend(Kapture.kt:49)
    at com.kpstv.compose.kapture.ScreenshotController$captureToBitmap$2.invoke(Kapture.kt:2)
    at com.kpstv.compose.kapture.ScreenshotController$captureToBitmap$2.invoke(Kapture.kt:1)
    at nc.b.b(Undispatched.kt:8)
    at kotlinx.coroutines.i.e(Builders.common.kt:54)
    at kotlinx.coroutines.g.g(Unknown Source:1)
    at com.kpstv.compose.kapture.ScreenshotController.b(Kapture.kt:65)
    at com.kpstv.compose.kapture.ScreenshotController.c(Kapture.kt:7)

I believe this is because you are asking to render in a coroutine instead of the main thread.

KaustubhPatange commented 4 weeks ago

Which dispatcher are you using make sure to use, Dispatchers.Main or Immediate.

nordfalk commented 4 weeks ago

Yes, you are right. I was invoking like

    val screenshotController = rememberScreenshotController()
    val coroutineScope = rememberCoroutineScope()
    ...
    Card(Modifier.attachController(screenshotController) {
      ... // content
    }

    Column(modifier = Modifier.clickable {
        coroutineScope.launch {
            withContext(Dispatchers.IO) {
                try {
                    val bitmap: Result<Bitmap> = screenshotController.captureToBitmap()