square / workflow-kotlin

A Swift and Kotlin library for making composable state machines, and UIs driven by those state machines.
https://square.github.io/workflow
Apache License 2.0
1.04k stars 102 forks source link

Showing Overlay causes crash when using AnimatedContent to animate screens #987

Open blakelee opened 1 year ago

blakelee commented 1 year ago

I have been using workflow in a personal project and I'm trying to use screen animations. The screen animations work fine for screen but if I try and use it on an overlay by using BodyAndOverlaysScreen it will crash the app.

Here is a test workflow that will cause it to crash. I simplified the workflow significantly to show the issue.

In my own app I can navigate between screen normally with the AnimatedContent but when I open an Overlay it will crash. Another thing I tried is to return my BodyAndOverlaysScreen without the AnimatedContent when I see that the overlays are not empty. That also caused a crash.

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.compose.ComposeScreen
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.container.AlertOverlay
import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen

object TestWorkflow : StatefulWorkflow<Unit, Boolean, Unit, Screen>() {
  override fun initialState(props: Unit, snapshot: Snapshot?): Boolean = false

  @OptIn(ExperimentalAnimationApi::class)
  override fun render(renderProps: Unit, renderState: Boolean, context: RenderContext): Screen {
    val dialog = AlertOverlay(title = "Test") {}.takeIf { renderState }

    val screen = BodyAndOverlaysScreen(
      body = ComposeScreen {
        Button(
          onClick = context.eventHandler { state = true },
          modifier = Modifier.wrapContentSize(),
          content = { Text("Open Dialog") }
        )
      },
      overlays = listOfNotNull(dialog)
    )

    return ComposeScreen { viewEnvironment ->
      AnimatedContent(targetState = screen) { targetScreen ->
        WorkflowRendering(rendering = targetScreen, viewEnvironment = viewEnvironment)
      }
    }
  }

  override fun snapshotState(state: Boolean): Snapshot? = null
}

Here is the logcat associated with it

    java.lang.IllegalStateException: Expected a ViewTreeLifecycleOwner on com.squareup.workflow1.ui.container.BodyAndOverlaysContainer{560c713 V.E...... ......I. 0,0-0,0 #7f0801cb app:id/workflow_body_and_modals_container}
        at com.squareup.workflow1.ui.container.LayeredDialogSessions$Companion$forView$2.invoke(LayeredDialogSessions.kt:300)
        at com.squareup.workflow1.ui.container.LayeredDialogSessions$Companion$forView$2.invoke(LayeredDialogSessions.kt:303)
        at com.squareup.workflow1.ui.container.LayeredDialogSessions.update(LayeredDialogSessions.kt:173)
        at com.squareup.workflow1.ui.container.BodyAndOverlaysContainer.update(BodyAndOverlaysContainer.kt:63)
        at com.squareup.workflow1.ui.container.BodyAndOverlaysContainer$Companion$1$1$1.showRendering(BodyAndOverlaysContainer.kt:159)
        at com.squareup.workflow1.ui.container.BodyAndOverlaysContainer$Companion$1$1$1.showRendering(BodyAndOverlaysContainer.kt:158)
        at com.squareup.workflow1.ui.RealScreenViewHolder.runner$lambda$0(RealScreenViewHolder.kt:19)
        at com.squareup.workflow1.ui.RealScreenViewHolder.$r8$lambda$yL9n1S2Um7r5e-awSSvIzkyIEu4(Unknown Source:0)
        at com.squareup.workflow1.ui.RealScreenViewHolder$$ExternalSyntheticLambda0.showRendering(Unknown Source:4)
        at com.squareup.workflow1.ui.ScreenViewHolderKt.show(ScreenViewHolder.kt:84)
        at com.squareup.workflow1.ui.ScreenViewFactoryKt$startShowing$1$4.invoke(ScreenViewFactory.kt:336)
        at com.squareup.workflow1.ui.ScreenViewFactoryKt$startShowing$1$4.invoke(ScreenViewFactory.kt:335)
        at com.squareup.workflow1.ui.ScreenViewFactoryKt.startShowing$lambda$3$lambda$0(ScreenViewFactory.kt:298)
        at com.squareup.workflow1.ui.ScreenViewFactoryKt.$r8$lambda$f7ANZNINIUUEMwpxjsGnHJmWNsE(Unknown Source:0)
        at com.squareup.workflow1.ui.ScreenViewFactoryKt$$ExternalSyntheticLambda0.startView(Unknown Source:0)
        at com.squareup.workflow1.ui.ScreenViewFactoryKt.startShowing(ScreenViewFactory.kt:335)
        at com.squareup.workflow1.ui.ScreenViewFactoryKt.startShowing$default(ScreenViewFactory.kt:285)
        at com.squareup.workflow1.ui.compose.WorkflowRenderingKt$asComposeViewFactory$1$Content$1.invoke(WorkflowRendering.kt:189)
        at com.squareup.workflow1.ui.compose.WorkflowRenderingKt$asComposeViewFactory$1$Content$1.invoke(WorkflowRendering.kt:184)
lucamtudor commented 1 year ago

If you need a bandaid solution to get you moving forward, wrap your root rendering in a legacy view system WorkflowLayout.

return ComposeScreen { viewEnvironment ->
    AndroidView(
        factory = { context ->
            WorkflowLayout(context)
        },
        update = { workflowLayout ->
            workflowLayout.show(rendering, viewEnvironment)
        },
    )
}
blakelee commented 1 year ago

The legacy layout worked. Thank you