diff --git a/core-compose/api/core-compose.api b/core-compose/api/core-compose.api index 594f3674..90273676 100644 --- a/core-compose/api/core-compose.api +++ b/core-compose/api/core-compose.api @@ -31,28 +31,31 @@ public final class com/squareup/workflow/ui/compose/CompositionRootKt { public static final fun withCompositionRoot (Lcom/squareup/workflow/ui/ViewRegistry;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow/ui/ViewRegistry; } +public final class com/squareup/workflow/ui/compose/RenderAsStateKt { + public static final fun renderAsState (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)Landroidx/compose/State; + public static final fun renderAsState (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)Landroidx/compose/State; + public static final fun renderAsState (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)Landroidx/compose/State; + public static final fun renderAsState (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)Landroidx/compose/State; + public static synthetic fun renderAsState$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)Landroidx/compose/State; + public static synthetic fun renderAsState$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)Landroidx/compose/State; + public static synthetic fun renderAsState$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)Landroidx/compose/State; + public static synthetic fun renderAsState$default (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)Landroidx/compose/State; +} + public final class com/squareup/workflow/ui/compose/ViewEnvironmentsKt { public static final fun WorkflowRendering (Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Landroidx/compose/Composer;)V public static synthetic fun WorkflowRendering$default (Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Landroidx/compose/Composer;ILjava/lang/Object;)V } public final class com/squareup/workflow/ui/compose/WorkflowContainerKt { - public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V - public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V - public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V - public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V - public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V - public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V - public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V - public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V + public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V + public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V + public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V - public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V - public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V - public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V - public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V - public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V - public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V + public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V + public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V + public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V } public final class com/squareup/workflow/ui/compose/internal/ComposeSupportKt { diff --git a/core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/RenderAsStateTest.kt b/core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/RenderAsStateTest.kt new file mode 100644 index 00000000..868c6fe7 --- /dev/null +++ b/core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/RenderAsStateTest.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2020 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry") + +package com.squareup.workflow.ui.compose + +import androidx.compose.FrameManager +import androidx.compose.Providers +import androidx.compose.mutableStateOf +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.ui.savedinstancestate.UiSavedStateRegistry +import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient +import androidx.ui.test.createComposeRule +import androidx.ui.test.runOnIdleCompose +import androidx.ui.test.waitForIdle +import com.google.common.truth.Truth.assertThat +import com.squareup.workflow.RenderContext +import com.squareup.workflow.Snapshot +import com.squareup.workflow.StatefulWorkflow +import com.squareup.workflow.Workflow +import com.squareup.workflow.action +import com.squareup.workflow.parse +import com.squareup.workflow.readUtf8WithLength +import com.squareup.workflow.stateless +import com.squareup.workflow.ui.compose.RenderAsStateTest.SnapshottingWorkflow.SnapshottedRendering +import com.squareup.workflow.writeUtf8WithLength +import okio.ByteString +import okio.ByteString.Companion.decodeBase64 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RenderAsStateTest { + + @Rule @JvmField val composeRule = createComposeRule() + + @Test fun passesPropsThrough() { + val workflow = Workflow.stateless { it } + lateinit var initialRendering: String + + composeRule.setContent { + initialRendering = workflow.renderAsState("foo").value + } + + runOnIdleCompose { + assertThat(initialRendering).isEqualTo("foo") + } + } + + @Test fun seesPropsAndRenderingUpdates() { + val workflow = Workflow.stateless { it } + val props = mutableStateOf("foo") + lateinit var rendering: String + + composeRule.setContent { + rendering = workflow.renderAsState(props.value).value + } + + waitForIdle() + assertThat(rendering).isEqualTo("foo") + FrameManager.framed { + props.value = "bar" + } + waitForIdle() + assertThat(rendering).isEqualTo("bar") + } + + @Test fun invokesOutputCallback() { + val workflow = Workflow.stateless Unit> { + { string -> actionSink.send(action { setOutput(string) }) } + } + val receivedOutputs = mutableListOf() + lateinit var rendering: (String) -> Unit + + composeRule.setContent { + rendering = workflow.renderAsState(onOutput = { receivedOutputs += it }).value + } + + waitForIdle() + assertThat(receivedOutputs).isEmpty() + rendering("one") + + waitForIdle() + assertThat(receivedOutputs).isEqualTo(listOf("one")) + rendering("two") + + waitForIdle() + assertThat(receivedOutputs).isEqualTo(listOf("one", "two")) + } + + @Test fun savesSnapshot() { + val workflow = SnapshottingWorkflow() + val savedStateRegistry = UiSavedStateRegistry(emptyMap()) { true } + lateinit var rendering: SnapshottedRendering + + composeRule.setContent { + Providers(UiSavedStateRegistryAmbient provides savedStateRegistry) { + rendering = renderAsStateImpl( + workflow, + props = Unit, + onOutput = {}, + diagnosticListener = null, + snapshotKey = SNAPSHOT_KEY + ).value + } + } + + waitForIdle() + assertThat(rendering.string).isEmpty() + rendering.updateString("foo") + + waitForIdle() + val savedValues = FrameManager.framed { + savedStateRegistry.performSave() + } + println("saved keys: ${savedValues.keys}") + // Relying on the int key across all runtimes is brittle, so use an explicit key. + val snapshot = ByteString.of(*(savedValues.getValue(SNAPSHOT_KEY) as ByteArray)) + println("snapshot: ${snapshot.base64()}") + assertThat(snapshot).isEqualTo(EXPECTED_SNAPSHOT) + } + + @Test fun restoresSnapshot() { + val workflow = SnapshottingWorkflow() + val restoreValues = mapOf(SNAPSHOT_KEY to EXPECTED_SNAPSHOT.toByteArray()) + val savedStateRegistry = UiSavedStateRegistry(restoreValues) { true } + lateinit var rendering: SnapshottedRendering + + composeRule.setContent { + Providers(UiSavedStateRegistryAmbient provides savedStateRegistry) { + rendering = renderAsStateImpl( + workflow, + props = Unit, + onOutput = {}, + diagnosticListener = null, + snapshotKey = "workflow-snapshot" + ).value + } + } + + waitForIdle() + assertThat(rendering.string).isEqualTo("foo") + } + + @Test fun restoresFromSnapshotWhenWorkflowChanged() { + val workflow1 = SnapshottingWorkflow() + val workflow2 = SnapshottingWorkflow() + val currentWorkflow = mutableStateOf(workflow1) + lateinit var rendering: SnapshottedRendering + + var compositionCount = 0 + var lastCompositionCount = 0 + fun assertWasRecomposed() { + assertThat(compositionCount).isGreaterThan(lastCompositionCount) + lastCompositionCount = compositionCount + } + + composeRule.setContent { + compositionCount++ + rendering = currentWorkflow.value.renderAsState().value + } + + // Initialize the first workflow. + waitForIdle() + assertThat(rendering.string).isEmpty() + assertWasRecomposed() + rendering.updateString("one") + waitForIdle() + assertWasRecomposed() + assertThat(rendering.string).isEqualTo("one") + + // Change the workflow instance being rendered. This should restart the runtime, but restore + // it from the snapshot. + FrameManager.framed { + currentWorkflow.value = workflow2 + } + + waitForIdle() + assertWasRecomposed() + assertThat(rendering.string).isEqualTo("one") + } + + private companion object { + const val SNAPSHOT_KEY = "workflow-snapshot" + + /** Golden value from [savesSnapshot]. */ + val EXPECTED_SNAPSHOT = "AAAABwAAAANmb28AAAAA".decodeBase64()!! + } + + // Seems to be a problem accessing Workflow.stateful. + private class SnapshottingWorkflow : + StatefulWorkflow() { + + data class SnapshottedRendering( + val string: String, + val updateString: (String) -> Unit + ) + + override fun initialState( + props: Unit, + snapshot: Snapshot? + ): String = snapshot?.bytes?.parse { it.readUtf8WithLength() } ?: "" + + override fun render( + props: Unit, + state: String, + context: RenderContext + ) = SnapshottedRendering( + string = state, + updateString = { newString -> context.actionSink.send(updateString(newString)) } + ) + + override fun snapshotState(state: String): Snapshot = + Snapshot.write { it.writeUtf8WithLength(state) } + + private fun updateString(newString: String) = action { + nextState = newString + } + } +} diff --git a/core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/WorkflowContainerTest.kt b/core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/WorkflowContainerTest.kt index 716b9375..8ae3f540 100644 --- a/core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/WorkflowContainerTest.kt +++ b/core-compose/src/androidTest/java/com/squareup/workflow/ui/compose/WorkflowContainerTest.kt @@ -17,33 +17,15 @@ package com.squareup.workflow.ui.compose -import androidx.compose.FrameManager -import androidx.compose.Providers -import androidx.compose.mutableStateOf -import androidx.compose.onActive import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.ui.foundation.Clickable import androidx.ui.foundation.Text -import androidx.ui.layout.Column -import androidx.ui.savedinstancestate.UiSavedStateRegistry -import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient +import androidx.ui.test.assertIsDisplayed import androidx.ui.test.createComposeRule -import androidx.ui.test.doClick import androidx.ui.test.findByText -import androidx.ui.test.waitForIdle -import com.google.common.truth.Truth.assertThat -import com.squareup.workflow.RenderContext -import com.squareup.workflow.Snapshot -import com.squareup.workflow.StatefulWorkflow import com.squareup.workflow.Workflow -import com.squareup.workflow.action -import com.squareup.workflow.parse -import com.squareup.workflow.readUtf8WithLength import com.squareup.workflow.stateless -import com.squareup.workflow.ui.compose.WorkflowContainerTest.SnapshottingWorkflow.SnapshottedRendering -import com.squareup.workflow.writeUtf8WithLength -import okio.ByteString -import okio.ByteString.Companion.decodeBase64 +import com.squareup.workflow.ui.ViewEnvironment +import com.squareup.workflow.ui.ViewRegistry import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -53,150 +35,27 @@ class WorkflowContainerTest { @Rule @JvmField val composeRule = createComposeRule() - @Test fun passesPropsThrough() { - val workflow = Workflow.stateless { it } + @Test fun rendersFromViewRegistry() { + val workflow = Workflow.stateless { "hello" } + val registry = ViewRegistry(composedViewFactory { rendering, _ -> Text(rendering) }) composeRule.setContent { - WorkflowContainer(workflow, "foo") { - assertThat(it).isEqualTo("foo") - } - } - } - - @Test fun seesPropsAndRenderingUpdates() { - val workflow = Workflow.stateless { it } - val props = mutableStateOf("foo") - - composeRule.setContent { - WorkflowContainer(workflow, props.value) { - Text(it) - } + WorkflowContainer(workflow, ViewEnvironment(registry)) } - findByText("foo").assertExists() - FrameManager.framed { - props.value = "bar" - } - findByText("bar").assertExists() + findByText("hello").assertIsDisplayed() } - @Test fun invokesOutputCallback() { - val workflow = Workflow.stateless Unit> { - { string -> actionSink.send(action { setOutput(string) }) } - } - - val receivedOutputs = mutableListOf() - composeRule.setContent { - WorkflowContainer(workflow, onOutput = { receivedOutputs += it }) { sendOutput -> - Column { - Clickable(onClick = { sendOutput("one") }) { - Text("send one") - } - Clickable(onClick = { sendOutput("two") }) { - Text("send two") - } - } - } + @Test fun automaticallyAddsComposeRenderingFactory() { + val workflow = Workflow.composed { _, _, _ -> + Text("it worked") } - - waitForIdle() - assertThat(receivedOutputs).isEmpty() - findByText("send one").doClick() - - waitForIdle() - assertThat(receivedOutputs).isEqualTo(listOf("one")) - findByText("send two").doClick() - - waitForIdle() - assertThat(receivedOutputs).isEqualTo(listOf("one", "two")) - } - - @Test fun savesSnapshot() { - val savedStateRegistry = UiSavedStateRegistry(emptyMap()) { true } + val registry = ViewRegistry() composeRule.setContent { - Providers(UiSavedStateRegistryAmbient provides savedStateRegistry) { - WorkflowContainerImpl( - SnapshottingWorkflow, - props = Unit, - onOutput = {}, - snapshotKey = SNAPSHOT_KEY - ) { (string, updateString) -> - onActive { - assertThat(string).isEmpty() - updateString("foo") - } - } - } + WorkflowContainer(workflow, ViewEnvironment(registry)) } - waitForIdle() - val savedValues = FrameManager.framed { - savedStateRegistry.performSave() - } - println("saved keys: ${savedValues.keys}") - // Relying on the int key across all runtimes might be flaky, might need to pass explicit key. - val snapshot = ByteString.of(*(savedValues.getValue(SNAPSHOT_KEY) as ByteArray)) - println("snapshot: ${snapshot.base64()}") - assertThat(snapshot).isEqualTo(EXPECTED_SNAPSHOT) - } - - @Test fun restoresSnapshot() { - val restoreValues = mapOf(SNAPSHOT_KEY to EXPECTED_SNAPSHOT.toByteArray()) - val savedStateRegistry = UiSavedStateRegistry(restoreValues) { true } - - composeRule.setContent { - Providers(UiSavedStateRegistryAmbient provides savedStateRegistry) { - WorkflowContainerImpl( - SnapshottingWorkflow, - props = Unit, - onOutput = {}, - snapshotKey = "workflow-snapshot" - ) { (string) -> - onActive { - assertThat(string).isEqualTo("foo") - } - Text(string) - } - } - } - - findByText("foo").assertExists() - } - - private companion object { - const val SNAPSHOT_KEY = "workflow-snapshot" - val EXPECTED_SNAPSHOT = "AAAABwAAAANmb28AAAAA".decodeBase64()!! - } - - // Seems to be a problem accessing Workflow.stateful. - private object SnapshottingWorkflow : - StatefulWorkflow() { - - data class SnapshottedRendering( - val string: String, - val updateString: (String) -> Unit - ) - - override fun initialState( - props: Unit, - snapshot: Snapshot? - ): String = snapshot?.bytes?.parse { it.readUtf8WithLength() } ?: "" - - override fun render( - props: Unit, - state: String, - context: RenderContext - ) = SnapshottedRendering( - string = state, - updateString = { newString -> context.actionSink.send(updateString(newString)) } - ) - - override fun snapshotState(state: String): Snapshot = - Snapshot.write { it.writeUtf8WithLength(state) } - - private fun updateString(newString: String) = action { - nextState = newString - } + findByText("it worked").assertIsDisplayed() } } diff --git a/core-compose/src/main/java/com/squareup/workflow/ui/compose/RenderAsState.kt b/core-compose/src/main/java/com/squareup/workflow/ui/compose/RenderAsState.kt new file mode 100644 index 00000000..3307a3aa --- /dev/null +++ b/core-compose/src/main/java/com/squareup/workflow/ui/compose/RenderAsState.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2020 Square Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("NOTHING_TO_INLINE") + +package com.squareup.workflow.ui.compose + +import androidx.annotation.VisibleForTesting +import androidx.compose.Composable +import androidx.compose.CompositionLifecycleObserver +import androidx.compose.FrameManager +import androidx.compose.MutableState +import androidx.compose.State +import androidx.compose.mutableStateOf +import androidx.compose.remember +import androidx.ui.core.CoroutineContextAmbient +import androidx.ui.core.Ref +import androidx.ui.savedinstancestate.Saver +import androidx.ui.savedinstancestate.SaverScope +import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient +import androidx.ui.savedinstancestate.savedInstanceState +import com.squareup.workflow.Snapshot +import com.squareup.workflow.Workflow +import com.squareup.workflow.diagnostic.WorkflowDiagnosticListener +import com.squareup.workflow.launchWorkflowIn +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import okio.ByteString +import kotlin.coroutines.CoroutineContext + +/** + * Runs this [Workflow] as long as this composable is part of the composition, and returns a + * [State] object that will be updated whenever the runtime emits a new [RenderingT]. + * + * The workflow runtime will be started when this function is first added to the composition, and + * cancelled when it is removed. The first rendering will be available immediately as soon as this + * function returns, as [State.value]. Composables that read this value will automatically recompose + * whenever the runtime emits a new rendering. + * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * + * @receiver The [Workflow] to run. If the value of the receiver changes to a different [Workflow] + * while this function is in the composition, the runtime will be restarted with the new workflow. + * @param props The [PropsT] for the root [Workflow]. Changes to this value across different + * compositions will cause the root workflow to re-render with the new props. + * @param onOutput A function that will be executed whenever the root [Workflow] emits an output. + * @param diagnosticListener An optional [WorkflowDiagnosticListener] to start the runtime with. If + * this value changes while this function is in the composition, the runtime will be restarted. + */ +@Composable +fun Workflow.renderAsState( + props: PropsT, + onOutput: (OutputT) -> Unit, + diagnosticListener: WorkflowDiagnosticListener? = null +): State = renderAsStateImpl(this, props, onOutput, diagnosticListener) + +/** + * Runs this [Workflow] as long as this composable is part of the composition, and returns a + * [State] object that will be updated whenever the runtime emits a new [RenderingT]. + * + * The workflow runtime will be started when this function is first added to the composition, and + * cancelled when it is removed. The first rendering will be available immediately as soon as this + * function returns, as [State.value]. Composables that read this value will automatically recompose + * whenever the runtime emits a new rendering. + * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * + * @receiver The [Workflow] to run. If the value of the receiver changes to a different [Workflow] + * while this function is in the composition, the runtime will be restarted with the new workflow. + * @param onOutput A function that will be executed whenever the root [Workflow] emits an output. + * @param diagnosticListener An optional [WorkflowDiagnosticListener] to start the runtime with. If + * this value changes while this function is in the composition, the runtime will be restarted. + */ +@Composable +inline fun Workflow.renderAsState( + noinline onOutput: (OutputT) -> Unit, + diagnosticListener: WorkflowDiagnosticListener? = null +): State = renderAsState(Unit, onOutput, diagnosticListener) + +/** + * Runs this [Workflow] as long as this composable is part of the composition, and returns a + * [State] object that will be updated whenever the runtime emits a new [RenderingT]. + * + * The workflow runtime will be started when this function is first added to the composition, and + * cancelled when it is removed. The first rendering will be available immediately as soon as this + * function returns, as [State.value]. Composables that read this value will automatically recompose + * whenever the runtime emits a new rendering. + * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * + * @receiver The [Workflow] to run. If the value of the receiver changes to a different [Workflow] + * while this function is in the composition, the runtime will be restarted with the new workflow. + * @param props The [PropsT] for the root [Workflow]. Changes to this value across different + * compositions will cause the root workflow to re-render with the new props. + * @param diagnosticListener An optional [WorkflowDiagnosticListener] to start the runtime with. If + * this value changes while this function is in the composition, the runtime will be restarted. + */ +@Composable +inline fun Workflow.renderAsState( + props: PropsT, + diagnosticListener: WorkflowDiagnosticListener? = null +): State = renderAsState(props, {}, diagnosticListener) + +/** + * Runs this [Workflow] as long as this composable is part of the composition, and returns a + * [State] object that will be updated whenever the runtime emits a new [RenderingT]. + * + * The workflow runtime will be started when this function is first added to the composition, and + * cancelled when it is removed. The first rendering will be available immediately as soon as this + * function returns, as [State.value]. Composables that read this value will automatically recompose + * whenever the runtime emits a new rendering. + * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * + * @receiver The [Workflow] to run. If the value of the receiver changes to a different [Workflow] + * while this function is in the composition, the runtime will be restarted with the new workflow. + * @param diagnosticListener An optional [WorkflowDiagnosticListener] to start the runtime with. If + * this value changes while this function is in the composition, the runtime will be restarted. + */ +@Composable +inline fun Workflow.renderAsState( + diagnosticListener: WorkflowDiagnosticListener? = null +): State = renderAsState(Unit, {}, diagnosticListener) + +/** + * @param snapshotKey Allows tests to pass in a custom key to use to save/restore the snapshot from + * the [UiSavedStateRegistryAmbient]. If null, will use the default key based on source location. + */ +@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) +@Composable internal fun renderAsStateImpl( + workflow: Workflow, + props: PropsT, + onOutput: (OutputT) -> Unit, + diagnosticListener: WorkflowDiagnosticListener?, + snapshotKey: String? = null +): State { + @Suppress("DEPRECATION") + val coroutineContext = CoroutineContextAmbient.current + Dispatchers.Main.immediate + val snapshotState = savedInstanceState(key = snapshotKey, saver = SnapshotSaver) { null } + + val outputRef = remember { Ref<(OutputT) -> Unit>() } + outputRef.value = onOutput + + // We can't use onActive/on(Pre)Commit because they won't run their callback until after this + // function returns, and we need to run this immediately so we get the rendering synchronously. + val state = remember(coroutineContext, workflow, diagnosticListener) { + WorkflowState(coroutineContext, workflow, props, outputRef, snapshotState, diagnosticListener) + } + state.setProps(props) + + return state.rendering +} + +@Suppress("EXPERIMENTAL_API_USAGE") +private class WorkflowState( + coroutineContext: CoroutineContext, + workflow: Workflow, + initialProps: PropsT, + private val outputRef: Ref<(OutputT) -> Unit>, + private val snapshotState: MutableState, + private val diagnosticListener: WorkflowDiagnosticListener? +) : CompositionLifecycleObserver { + + private val workflowScope = CoroutineScope(coroutineContext) + private val renderingState = mutableStateOf(null) + + // This can be a StateFlow once coroutines is upgraded to 1.3.6. + private val propsChannel = Channel(capacity = Channel.CONFLATED) + .apply { offer(initialProps) } + val propsFlow = propsChannel.consumeAsFlow() + .distinctUntilChanged() + + // The value is guaranteed to be set before returning, so this cast is fine. + @Suppress("UNCHECKED_CAST") + val rendering: State + get() = renderingState as State + + init { + launchWorkflowIn(workflowScope, workflow, propsFlow, snapshotState.value) { session -> + session.diagnosticListener = diagnosticListener + + session.outputs.onEach { outputRef.value!!.invoke(it) } + .launchIn(this) + + session.renderingsAndSnapshots + .onEach { (rendering, snapshot) -> + FrameManager.framed { + renderingState.value = rendering + snapshotState.value = snapshot + } + } + .launchIn(this) + } + } + + fun setProps(props: PropsT) { + propsChannel.offer(props) + } + + override fun onEnter() {} + + override fun onLeave() { + workflowScope.cancel() + } +} + +private object SnapshotSaver : Saver { + override fun SaverScope.save(value: Snapshot?): ByteArray { + return value?.bytes?.toByteArray() ?: ByteArray(0) + } + + override fun restore(value: ByteArray): Snapshot? { + return value.takeUnless { it.isEmpty() } + ?.let { bytes -> Snapshot.of(ByteString.of(*bytes)) } + } +} + +private class OutputCallback(var onOutput: (OutputT) -> Unit) diff --git a/core-compose/src/main/java/com/squareup/workflow/ui/compose/WorkflowContainer.kt b/core-compose/src/main/java/com/squareup/workflow/ui/compose/WorkflowContainer.kt index 643af54f..908864a6 100644 --- a/core-compose/src/main/java/com/squareup/workflow/ui/compose/WorkflowContainer.kt +++ b/core-compose/src/main/java/com/squareup/workflow/ui/compose/WorkflowContainer.kt @@ -14,46 +14,22 @@ * limitations under the License. */ @file:Suppress( - "EXPERIMENTAL_API_USAGE", "FunctionNaming", - "NOTHING_TO_INLINE", - "RemoveEmptyParenthesesFromAnnotationEntry" + "NOTHING_TO_INLINE" ) package com.squareup.workflow.ui.compose -import androidx.annotation.VisibleForTesting -import androidx.annotation.VisibleForTesting.PRIVATE import androidx.compose.Composable -import androidx.compose.Direct -import androidx.compose.Pivotal -import androidx.compose.State -import androidx.compose.onDispose import androidx.compose.remember -import androidx.compose.state -import androidx.ui.core.CoroutineContextAmbient import androidx.ui.core.Modifier -import androidx.ui.foundation.Box -import androidx.ui.savedinstancestate.Saver -import androidx.ui.savedinstancestate.SaverScope -import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient -import androidx.ui.savedinstancestate.savedInstanceState import com.squareup.workflow.Snapshot import com.squareup.workflow.Workflow import com.squareup.workflow.diagnostic.WorkflowDiagnosticListener -import com.squareup.workflow.launchWorkflowIn import com.squareup.workflow.ui.ViewEnvironment -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import okio.ByteString -import kotlin.coroutines.CoroutineContext +import com.squareup.workflow.ui.ViewFactory +import com.squareup.workflow.ui.ViewRegistry +import com.squareup.workflow.ui.plus /** * Render a [Workflow]'s renderings. @@ -62,121 +38,33 @@ import kotlin.coroutines.CoroutineContext * any time [workflow], [diagnosticListener], or the `CoroutineContext` * changes. The runtime will be cancelled when this function stops composing. * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * * @param workflow The [Workflow] to render. * @param props The props to render the root workflow with. If this value changes between calls, * the workflow runtime will re-render with the new props. * @param onOutput A function that will be invoked any time the root workflow emits an output. + * @param viewEnvironment The [ViewEnvironment] used to display renderings. + * @param modifier The [Modifier] to apply to the root [ViewFactory]. * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. - * @param content A [Composable] function that gets executed every time the root workflow spits - * out a new rendering. */ -@Direct -@Composable fun WorkflowContainer( +@Composable fun WorkflowContainer( workflow: Workflow, props: PropsT, onOutput: (OutputT) -> Unit, - modifier: Modifier = Modifier, - diagnosticListener: WorkflowDiagnosticListener? = null, - content: @Composable() (rendering: RenderingT) -> Unit -) { - WorkflowContainerImpl(workflow, props, onOutput, modifier, diagnosticListener, content = content) -} - -/** - * Render a [Workflow]'s renderings. - * - * When this function is first composed it will start a new runtime. This runtime will be restarted - * any time [workflow], [diagnosticListener], or the `CoroutineContext` - * changes. The runtime will be cancelled when this function stops composing. - * - * @param workflow The [Workflow] to render. - * @param onOutput A function that will be invoked any time the root workflow emits an output. - * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. - * @param content A [Composable] function that gets executed every time the root workflow spits - * out a new rendering. - */ -@Composable inline fun WorkflowContainer( - workflow: Workflow, - noinline onOutput: (OutputT) -> Unit, - modifier: Modifier = Modifier, - diagnosticListener: WorkflowDiagnosticListener? = null, - noinline content: @Composable() (rendering: RenderingT) -> Unit -) { - WorkflowContainer(workflow, Unit, onOutput, modifier, diagnosticListener, content) -} - -/** - * Render a [Workflow]'s renderings. - * - * When this function is first composed it will start a new runtime. This runtime will be restarted - * any time [workflow], [diagnosticListener], or the `CoroutineContext` - * changes. The runtime will be cancelled when this function stops composing. - * - * @param workflow The [Workflow] to render. - * @param props The props to render the root workflow with. If this value changes between calls, - * the workflow runtime will re-render with the new props. - * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. - * @param content A [Composable] function that gets executed every time the root workflow spits - * out a new rendering. - */ -@Composable inline fun WorkflowContainer( - workflow: Workflow, - props: PropsT, - modifier: Modifier = Modifier, - diagnosticListener: WorkflowDiagnosticListener? = null, - noinline content: @Composable() (rendering: RenderingT) -> Unit -) { - WorkflowContainer(workflow, props, {}, modifier, diagnosticListener, content) -} - -/** - * Render a [Workflow]'s renderings. - * - * When this function is first composed it will start a new runtime. This runtime will be restarted - * any time [workflow], [diagnosticListener], or the `CoroutineContext` - * changes. The runtime will be cancelled when this function stops composing. - * - * @param workflow The [Workflow] to render. - * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. - * @param content A [Composable] function that gets executed every time the root workflow spits - * out a new rendering. - */ -@Composable inline fun WorkflowContainer( - workflow: Workflow, - modifier: Modifier = Modifier, - diagnosticListener: WorkflowDiagnosticListener? = null, - noinline content: @Composable() (rendering: RenderingT) -> Unit -) { - WorkflowContainer(workflow, Unit, {}, modifier, diagnosticListener, content) -} - -/** - * Render a [Workflow]'s renderings. - * - * When this function is first composed it will start a new runtime. This runtime will be restarted - * any time [workflow], [diagnosticListener], or the `CoroutineContext` - * changes. The runtime will be cancelled when this function stops composing. - * - * @param workflow The [Workflow] to render. - * @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by - * the workflow. - * @param props The props to render the root workflow with. If this value changes between calls, - * the workflow runtime will re-render with the new props. - * @param onOutput A function that will be invoked any time the root workflow emits an output. - * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. - */ -@Direct -@Composable fun WorkflowContainer( - workflow: Workflow, viewEnvironment: ViewEnvironment, - props: PropsT, - onOutput: (OutputT) -> Unit, modifier: Modifier = Modifier, diagnosticListener: WorkflowDiagnosticListener? = null ) { - WorkflowContainer(workflow, props, onOutput, modifier, diagnosticListener) { rendering -> - rendering.render(viewEnvironment) + // Ensure ComposeRendering is in the ViewRegistry. + val realEnvironment = remember(viewEnvironment) { + viewEnvironment.withFactory(ComposeRendering.Factory) } + + val rendering = workflow.renderAsState(props, onOutput, diagnosticListener) + WorkflowRendering(rendering.value, realEnvironment, modifier) } /** @@ -186,20 +74,24 @@ import kotlin.coroutines.CoroutineContext * any time [workflow], [diagnosticListener], or the `CoroutineContext` * changes. The runtime will be cancelled when this function stops composing. * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * * @param workflow The [Workflow] to render. - * @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by - * the workflow. * @param onOutput A function that will be invoked any time the root workflow emits an output. + * @param viewEnvironment The [ViewEnvironment] used to display renderings. + * @param modifier The [Modifier] to apply to the root [ViewFactory]. * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. */ -@Composable inline fun WorkflowContainer( - workflow: Workflow, - viewEnvironment: ViewEnvironment, +@Composable inline fun WorkflowContainer( + workflow: Workflow, noinline onOutput: (OutputT) -> Unit, + viewEnvironment: ViewEnvironment, modifier: Modifier = Modifier, diagnosticListener: WorkflowDiagnosticListener? = null ) { - WorkflowContainer(workflow, viewEnvironment, Unit, onOutput, modifier, diagnosticListener) + WorkflowContainer(workflow, Unit, onOutput, viewEnvironment, modifier, diagnosticListener) } /** @@ -209,21 +101,25 @@ import kotlin.coroutines.CoroutineContext * any time [workflow], [diagnosticListener], or the `CoroutineContext` * changes. The runtime will be cancelled when this function stops composing. * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * * @param workflow The [Workflow] to render. - * @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by - * the workflow. * @param props The props to render the root workflow with. If this value changes between calls, * the workflow runtime will re-render with the new props. + * @param viewEnvironment The [ViewEnvironment] used to display renderings. + * @param modifier The [Modifier] to apply to the root [ViewFactory]. * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. */ -@Composable inline fun WorkflowContainer( - workflow: Workflow, - viewEnvironment: ViewEnvironment, +@Composable inline fun WorkflowContainer( + workflow: Workflow, props: PropsT, + viewEnvironment: ViewEnvironment, modifier: Modifier = Modifier, diagnosticListener: WorkflowDiagnosticListener? = null ) { - WorkflowContainer(workflow, viewEnvironment, props, {}, modifier, diagnosticListener) + WorkflowContainer(workflow, props, {}, viewEnvironment, modifier, diagnosticListener) } /** @@ -233,111 +129,28 @@ import kotlin.coroutines.CoroutineContext * any time [workflow], [diagnosticListener], or the `CoroutineContext` * changes. The runtime will be cancelled when this function stops composing. * + * [Snapshot]s from the runtime will automatically be saved to the current + * [UiSavedStateRegistry][androidx.ui.savedinstancestate.UiSavedStateRegistry]. When the runtime is + * started, if a snapshot exists in the registry, it will be used to restore the workflows. + * * @param workflow The [Workflow] to render. - * @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by - * the workflow. + * @param viewEnvironment The [ViewEnvironment] used to display renderings. + * @param modifier The [Modifier] to apply to the root [ViewFactory]. * @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime. */ -@Composable inline fun WorkflowContainer( - workflow: Workflow, +@Composable inline fun WorkflowContainer( + workflow: Workflow, viewEnvironment: ViewEnvironment, modifier: Modifier = Modifier, diagnosticListener: WorkflowDiagnosticListener? = null ) { - WorkflowContainer(workflow, viewEnvironment, Unit, {}, modifier, diagnosticListener) -} - -/** - * Internal version of [WorkflowContainer] that accepts extra parameters for testing. - */ -@VisibleForTesting(otherwise = PRIVATE) -@Composable internal fun WorkflowContainerImpl( - workflow: Workflow, - props: PropsT, - onOutput: (OutputT) -> Unit, - modifier: Modifier = Modifier, - diagnosticListener: WorkflowDiagnosticListener? = null, - snapshotKey: String? = null, - content: @Composable() (rendering: RenderingT) -> Unit -) { - @Suppress("DEPRECATION") - val rendering = renderAsState( - workflow, props, onOutput, CoroutineContextAmbient.current, diagnosticListener, snapshotKey - ) - - Box(modifier = modifier) { - content(rendering.value) - } -} - -/** - * @param snapshotKey Allows tests to pass in a custom key to use to save/restore the snapshot from - * the [UiSavedStateRegistryAmbient]. If null, will use the default key based on source location. - */ -@Composable private fun renderAsState( - @Pivotal workflow: Workflow, - props: PropsT, - onOutput: (OutputT) -> Unit, - @Pivotal coroutineContext: CoroutineContext, - @Pivotal diagnosticListener: WorkflowDiagnosticListener?, - snapshotKey: String? -): State { - // This can be a StateFlow once coroutines is upgraded to 1.3.6. - val propsChannel = remember { Channel(capacity = CONFLATED) } - propsChannel.offer(props) - - // Need a mutable holder for onOutput so the outputs subscriber created in the onActive block - // will always be able to see the latest value. - val outputCallback = remember { OutputCallback(onOutput) } - outputCallback.onOutput = onOutput - - val renderingState = state { null } - val snapshotState = savedInstanceState(key = snapshotKey, saver = SnapshotSaver) { null } - - // We can't use onActive/on(Pre)Commit because they won't run their callback until after this - // function returns, and we need to run this immediately so we get the rendering synchronously. - val workflowScope = remember { - val coroutineScope = CoroutineScope(coroutineContext + Dispatchers.Main.immediate) - val propsFlow = propsChannel.consumeAsFlow() - .distinctUntilChanged() - - launchWorkflowIn(coroutineScope, workflow, propsFlow, snapshotState.value) { session -> - session.diagnosticListener = diagnosticListener - - // Don't call onOutput directly, since out captured reference won't be changed if the - // if a different argument is passed to observeWorkflow. - session.outputs.onEach { outputCallback.onOutput(it) } - .launchIn(this) - - session.renderingsAndSnapshots - .onEach { (rendering, snapshot) -> - renderingState.value = rendering - snapshotState.value = snapshot - } - .launchIn(this) - } - - return@remember coroutineScope - } - - onDispose { - workflowScope.cancel() - } - - // The value is guaranteed to be set before returning, so this cast is fine. - @Suppress("UNCHECKED_CAST") - return renderingState as State + WorkflowContainer(workflow, Unit, {}, viewEnvironment, modifier, diagnosticListener) } -private object SnapshotSaver : Saver { - override fun SaverScope.save(value: Snapshot?): ByteArray { - return value?.bytes?.toByteArray() ?: ByteArray(0) - } - - override fun restore(value: ByteArray): Snapshot? { - return value.takeUnless { it.isEmpty() } - ?.let { bytes -> Snapshot.of(ByteString.of(*bytes)) } +private fun ViewEnvironment.withFactory(viewFactory: ViewFactory<*>): ViewEnvironment { + return this[ViewRegistry].let { registry -> + if (viewFactory.type !in registry.keys) { + this + (ViewRegistry to registry + viewFactory) + } else this } } - -private class OutputCallback(var onOutput: (OutputT) -> Unit) diff --git a/samples/hello-compose/src/main/java/com/squareup/sample/hellocompose/App.kt b/samples/hello-compose/src/main/java/com/squareup/sample/hellocompose/App.kt index f778326e..da73f861 100644 --- a/samples/hello-compose/src/main/java/com/squareup/sample/hellocompose/App.kt +++ b/samples/hello-compose/src/main/java/com/squareup/sample/hellocompose/App.kt @@ -27,27 +27,21 @@ import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener import com.squareup.workflow.ui.ViewEnvironment import com.squareup.workflow.ui.ViewRegistry import com.squareup.workflow.ui.compose.WorkflowContainer -import com.squareup.workflow.ui.compose.WorkflowRendering private val viewRegistry = ViewRegistry(HelloBinding) private val viewEnvironment = ViewEnvironment(viewRegistry) @Composable fun App() { - WorkflowContainer( - workflow = HelloWorkflow, - diagnosticListener = SimpleLoggingDiagnosticListener() - ) { rendering -> - MaterialTheme { - WorkflowRendering( - rendering, - viewEnvironment, - modifier = Modifier.drawBorder( - shape = RoundedCornerShape(10.dp), - size = 10.dp, - color = Color.Magenta - ) - ) - } + MaterialTheme { + WorkflowContainer( + HelloWorkflow, viewEnvironment, + modifier = Modifier.drawBorder( + shape = RoundedCornerShape(10.dp), + size = 10.dp, + color = Color.Magenta + ), + diagnosticListener = SimpleLoggingDiagnosticListener() + ) } }