diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index a9460eddd6..2a2782ad95 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -222,7 +222,7 @@ public final class com/squareup/workflow/WorkflowActionKt { public static final fun action (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/WorkflowAction; public static final fun action (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/WorkflowAction; public static synthetic fun action$default (Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow/WorkflowAction; - public static final fun applyTo (Lcom/squareup/workflow/WorkflowAction;Ljava/lang/Object;)Lkotlin/Pair; + public static final fun applyTo (Lcom/squareup/workflow/WorkflowAction;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lkotlin/Pair; } public final class com/squareup/workflow/WorkflowIdentifier { diff --git a/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt b/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt index c64eb5df9d..b355f64784 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt @@ -51,7 +51,7 @@ import com.squareup.workflow.WorkflowAction.Updater * * See [renderChild]. */ -interface RenderContext { +interface RenderContext { /** * Accepts a single [WorkflowAction], invokes that action by calling [WorkflowAction.apply] @@ -94,7 +94,7 @@ interface RenderContext { * @param key An optional string key that is used to distinguish between workflows of the same * type. */ - fun renderChild( + fun renderChild( child: Workflow, props: ChildPropsT, key: String = "", @@ -144,7 +144,7 @@ interface RenderContext { * Convenience alias of [RenderContext.renderChild] for workflows that don't take props. */ /* ktlint-disable parameter-list-wrapping */ -fun +fun RenderContext.renderChild( child: Workflow, key: String = "", @@ -157,7 +157,7 @@ fun * output. */ /* ktlint-disable parameter-list-wrapping */ -fun +fun RenderContext.renderChild( child: Workflow, props: PropsT, @@ -170,7 +170,7 @@ fun * output. */ /* ktlint-disable parameter-list-wrapping */ -fun +fun RenderContext.renderChild( child: Workflow, key: String = "" @@ -185,7 +185,7 @@ fun * * @param key An optional string key that is used to distinguish between identical [Worker]s. */ -fun RenderContext.runningWorker( +fun RenderContext.runningWorker( worker: Worker, key: String = "" ) { @@ -199,7 +199,7 @@ fun RenderContext.runningWorker( * Alternative to [RenderContext.actionSink] that allows externally defined * event types to be mapped to anonymous [WorkflowAction]s. */ -fun RenderContext.makeEventSink( +fun RenderContext.makeEventSink( update: Updater.(EventT) -> Unit ): Sink = actionSink.contraMap { event -> action({ "eventSink($event)" }) { update(event) } @@ -216,7 +216,7 @@ fun RenderContext.makeEventSink "Use runningWorker", ReplaceWith("runningWorker(worker, key, handler)", "com.squareup.workflow.runningWorker") ) -fun RenderContext.onWorkerOutput( +fun RenderContext.onWorkerOutput( worker: Worker, key: String = "", handler: (T) -> WorkflowAction diff --git a/workflow-core/src/main/java/com/squareup/workflow/Sink.kt b/workflow-core/src/main/java/com/squareup/workflow/Sink.kt index 3d0889db8c..de889e777a 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/Sink.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/Sink.kt @@ -71,7 +71,7 @@ fun Sink.contraMap(transform: (T2) -> T1): Sink { * ``` */ @ExperimentalWorkflowApi -suspend fun Flow.collectToSink( +suspend fun Flow.collectToSink( actionSink: Sink>, handler: (T) -> WorkflowAction ) { @@ -93,7 +93,7 @@ suspend fun Flow.collectToSink( * This method is intended to be used from [RenderContext.runningSideEffect]. */ @ExperimentalWorkflowApi -suspend fun Sink>.sendAndAwaitApplication( +suspend fun Sink>.sendAndAwaitApplication( action: WorkflowAction ) { suspendCancellableCoroutine { continuation -> diff --git a/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt b/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt index be094fc69d..af856fd60d 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/StatefulWorkflow.kt @@ -79,7 +79,7 @@ import com.squareup.workflow.WorkflowAction.Updater abstract class StatefulWorkflow< in PropsT, StateT, - out OutputT : Any, + out OutputT, out RenderingT > : Workflow { @@ -164,7 +164,7 @@ abstract class StatefulWorkflow< /** * Returns a stateful [Workflow] implemented via the given functions. */ -inline fun Workflow.Companion.stateful( +inline fun Workflow.Companion.stateful( crossinline initialState: (PropsT, Snapshot?) -> StateT, crossinline render: RenderContext.(props: PropsT, state: StateT) -> RenderingT, crossinline snapshot: (StateT) -> Snapshot, @@ -198,7 +198,7 @@ inline fun Workflow.Companion.statef /** * Returns a stateful [Workflow], with no props, implemented via the given functions. */ -inline fun Workflow.Companion.stateful( +inline fun Workflow.Companion.stateful( crossinline initialState: (Snapshot?) -> StateT, crossinline render: RenderContext.(state: StateT) -> RenderingT, crossinline snapshot: (StateT) -> Snapshot @@ -213,7 +213,7 @@ inline fun Workflow.Companion.stateful( * * This overload does not support snapshotting, but there are other overloads that do. */ -inline fun Workflow.Companion.stateful( +inline fun Workflow.Companion.stateful( crossinline initialState: (PropsT) -> StateT, crossinline render: RenderContext.(props: PropsT, state: StateT) -> RenderingT, crossinline onPropsChanged: ( @@ -233,7 +233,7 @@ inline fun Workflow.Companion.statef * * This overload does not support snapshots, but there are others that do. */ -inline fun Workflow.Companion.stateful( +inline fun Workflow.Companion.stateful( initialState: StateT, crossinline render: RenderContext.(state: StateT) -> RenderingT ): StatefulWorkflow = stateful( @@ -249,7 +249,7 @@ inline fun Workflow.Companion.stateful( * @param name A string describing the update for debugging, included in [toString]. * @param update Function that defines the workflow update. */ -fun +fun StatefulWorkflow.action( name: String = "", update: Updater.() -> Unit @@ -264,7 +264,7 @@ fun * in [toString]. * @param update Function that defines the workflow update. */ -fun +fun StatefulWorkflow.action( name: () -> String, update: Updater.() -> Unit @@ -280,7 +280,7 @@ fun imports = arrayOf("com.squareup.workflow.action") ) ) -fun +fun StatefulWorkflow.workflowAction( name: String = "", block: Mutator.() -> OutputT? @@ -294,7 +294,7 @@ fun imports = arrayOf("com.squareup.workflow.action") ) ) -fun +fun StatefulWorkflow.workflowAction( name: () -> String, block: Mutator.() -> OutputT? diff --git a/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt b/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt index 748973a87f..2a5d0bd1b5 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/StatelessWorkflow.kt @@ -34,7 +34,7 @@ import com.squareup.workflow.WorkflowAction.Updater * * @see StatefulWorkflow */ -abstract class StatelessWorkflow : +abstract class StatelessWorkflow : Workflow { @Suppress("UNCHECKED_CAST") @@ -82,7 +82,7 @@ abstract class StatelessWorkflow : * [props][PropsT] received from its parent, and it may render child workflows that do have * their own internal state. */ -inline fun Workflow.Companion.stateless( +inline fun Workflow.Companion.stateless( crossinline render: RenderContext.(props: PropsT) -> RenderingT ): Workflow = object : StatelessWorkflow() { @@ -108,7 +108,7 @@ fun Workflow.Companion.rendering( * @param name A string describing the update for debugging, included in [toString]. * @param update Function that defines the workflow update. */ -fun +fun StatelessWorkflow.action( name: String = "", update: Updater.() -> Unit @@ -123,7 +123,7 @@ fun * [toString]. * @param update Function that defines the workflow update. */ -fun +fun StatelessWorkflow.action( name: () -> String, update: Updater.() -> Unit diff --git a/workflow-core/src/main/java/com/squareup/workflow/Worker.kt b/workflow-core/src/main/java/com/squareup/workflow/Worker.kt index d70d40141a..33334504c2 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/Worker.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/Worker.kt @@ -247,7 +247,7 @@ interface Worker { * The returned [Worker] will equate to any other workers created with any of the [Worker] * builder functions that have the same output type. */ - inline fun fromNullable( + inline fun fromNullable( // This could be crossinline, but there's a coroutines bug that will cause the coroutine // to immediately resume on suspension inside block when it is crossinline. // See https://youtrack.jetbrains.com/issue/KT-31197. diff --git a/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt b/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt index 96d1e8c7b8..8d4832e8d0 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/Workflow.kt @@ -104,7 +104,7 @@ package com.squareup.workflow * @see StatefulWorkflow * @see StatelessWorkflow */ -interface Workflow { +interface Workflow { /** * Provides a [StatefulWorkflow] view of this workflow. Necessary because [StatefulWorkflow] is @@ -125,7 +125,7 @@ interface Workflow { */ /* ktlint-disable parameter-list-wrapping */ @OptIn(ExperimentalWorkflowApi::class) -fun +fun Workflow.mapRendering( transform: (FromRenderingT) -> ToRenderingT ): Workflow = diff --git a/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt b/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt index b8945b5fe7..9aca2c2134 100644 --- a/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt +++ b/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt @@ -21,7 +21,7 @@ import com.squareup.workflow.WorkflowAction.Updater /** * An atomic operation that updates the state of a [Workflow], and also optionally emits an output. */ -interface WorkflowAction { +interface WorkflowAction { @Deprecated("Use Updater") class Mutator(var state: S) @@ -31,8 +31,9 @@ interface WorkflowAction { * * @param nextState the state that the workflow should move to. Default is the current state. */ - class Updater(var nextState: S) { - internal var output: @UnsafeVariance O? = null + class Updater(var nextState: S) { + private var output: @UnsafeVariance O? = null + private var isOutputSet = false /** * Sets the value the workflow will emit as output when this action is applied. @@ -40,7 +41,12 @@ interface WorkflowAction { */ fun setOutput(output: O) { this.output = output + isOutputSet = true } + + @Suppress("UNCHECKED_CAST") + internal fun mapOutput(mapper: (@UnsafeVariance O) -> T): T? = + if (isOutputSet) mapper(output as O) else null } /** @@ -69,7 +75,7 @@ interface WorkflowAction { * Use this to, for example, ignore the output of a child workflow or worker. */ @Suppress("UNCHECKED_CAST") - fun noAction(): WorkflowAction = + fun noAction(): WorkflowAction = NO_ACTION as WorkflowAction /** @@ -83,7 +89,7 @@ interface WorkflowAction { imports = arrayOf("com.squareup.workflow.action") ) ) - fun enterState( + fun enterState( newState: StateT, emittingOutput: OutputT? = null ): WorkflowAction = @@ -103,7 +109,7 @@ interface WorkflowAction { imports = arrayOf("com.squareup.workflow.action") ) ) - fun enterState( + fun enterState( name: String, newState: StateT, emittingOutput: OutputT? = null @@ -123,7 +129,7 @@ interface WorkflowAction { imports = arrayOf("com.squareup.workflow.action") ) ) - fun modifyState( + fun modifyState( name: () -> String, emittingOutput: OutputT? = null, modify: (StateT) -> StateT @@ -143,7 +149,7 @@ interface WorkflowAction { imports = arrayOf("com.squareup.workflow.action") ) ) - fun emitOutput(output: OutputT): WorkflowAction = + fun emitOutput(output: OutputT): WorkflowAction = action({ "emitOutput($output)" }) { setOutput(output) } /** @@ -156,7 +162,7 @@ interface WorkflowAction { imports = arrayOf("com.squareup.workflow.action") ) ) - fun emitOutput( + fun emitOutput( name: String, output: OutputT ): WorkflowAction = @@ -179,7 +185,7 @@ interface WorkflowAction { * @see StatelessWorkflow.action * @see StatefulWorkflow.action */ -inline fun action( +inline fun action( name: String = "", crossinline apply: Updater.() -> Unit ) = action({ name }, apply) @@ -197,7 +203,7 @@ inline fun action( * @see StatelessWorkflow.action * @see StatefulWorkflow.action */ -inline fun action( +inline fun action( crossinline name: () -> String, crossinline apply: Updater.() -> Unit ): WorkflowAction = object : WorkflowAction { @@ -205,10 +211,19 @@ inline fun action( override fun toString(): String = "WorkflowAction(${name()})@${hashCode()}" } -fun WorkflowAction.applyTo( - state: StateT -): Pair { +/** + * Applies this [WorkflowAction] to [state]. If the action sets an output, the output will be + * transformed by [mapOutput], and then both the new state and the transformed output will be + * returned. + * + * If the action sets the output multiple times, only the last one will be used. + */ +fun WorkflowAction.applyTo( + state: StateT, + mapOutput: (OutputT) -> T +): Pair { val updater = Updater(state) updater.apply() - return Pair(updater.nextState, updater.output) + val output = updater.mapOutput(mapOutput) + return Pair(updater.nextState, output) } diff --git a/workflow-core/src/test/java/com/squareup/workflow/SinkTest.kt b/workflow-core/src/test/java/com/squareup/workflow/SinkTest.kt index 6d5ef1a2a6..55c04ea2fb 100644 --- a/workflow-core/src/test/java/com/squareup/workflow/SinkTest.kt +++ b/workflow-core/src/test/java/com/squareup/workflow/SinkTest.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.test.runBlockingTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.test.fail @@ -51,9 +52,11 @@ class SinkTest { assertEquals(1, sink.actions.size) sink.actions.removeFirst() .let { action -> - val (newState, output) = action.applyTo("state") + val (newState, output) = action.applyTo("state") { + assertEquals("output: 1", it) + } assertEquals("state 1", newState) - assertEquals("output: 1", output) + assertNotNull(output) } assertTrue(sink.actions.isEmpty()) @@ -62,9 +65,11 @@ class SinkTest { assertEquals(1, sink.actions.size) sink.actions.removeFirst() .let { action -> - val (newState, output) = action.applyTo("state") + val (newState, output) = action.applyTo("state") { + assertEquals("output: 2", it) + } assertEquals("state 2", newState) - assertEquals("output: 2", output) + assertNotNull(output) } collector.cancel() @@ -84,10 +89,12 @@ class SinkTest { advanceUntilIdle() val enqueuedAction = sink.actions.removeFirst() - val result = enqueuedAction.applyTo("state") + val (newState, output) = enqueuedAction.applyTo("state") { + assertEquals("output", it) + } assertEquals(1, applications) - assertEquals("state applied", result.first) - assertEquals("output", result.second) + assertEquals("state applied", newState) + assertNotNull(output) } } @@ -107,7 +114,7 @@ class SinkTest { val enqueuedAction = sink.actions.removeFirst() pauseDispatcher() - enqueuedAction.applyTo("state") + enqueuedAction.applyTo("state") {} assertFalse(resumed) resumeDispatcher() @@ -131,11 +138,11 @@ class SinkTest { val enqueuedAction = sink.actions.removeFirst() sendJob.cancel() advanceUntilIdle() - val result = enqueuedAction.applyTo("ignored") + val (newState, output) = enqueuedAction.applyTo("ignored") {} assertFalse(applied) - assertEquals("ignored", result.first) - assertNull(result.second) + assertEquals("ignored", newState) + assertNull(output) } } diff --git a/workflow-core/src/test/java/com/squareup/workflow/WorkflowActionTest.kt b/workflow-core/src/test/java/com/squareup/workflow/WorkflowActionTest.kt new file mode 100644 index 0000000000..10e7deac44 --- /dev/null +++ b/workflow-core/src/test/java/com/squareup/workflow/WorkflowActionTest.kt @@ -0,0 +1,93 @@ +/* + * 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. + */ +package com.squareup.workflow + +import com.squareup.workflow.WorkflowAction.Updater +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class WorkflowActionTest { + + @Test fun `applyTo works when no output is set`() { + val action = object : WorkflowAction { + override fun Updater.apply() { + nextState = "nextState: $nextState" + } + } + val (nextState, output) = action.applyTo("state", ::OutputHolder) + assertEquals("nextState: state", nextState) + assertNull(output) + } + + @Test fun `applyTo works when null output is set`() { + val action = object : WorkflowAction { + override fun Updater.apply() { + nextState = "nextState: $nextState" + setOutput(null) + } + } + val (nextState, output) = action.applyTo("state", ::OutputHolder) + assertEquals("nextState: state", nextState) + assertNotNull(output) + assertNull(output.value) + } + + @Test fun `applyTo works when non-null output is set`() { + val action = object : WorkflowAction { + override fun Updater.apply() { + nextState = "nextState: $nextState" + setOutput("output") + } + } + val (nextState, output) = action.applyTo("state", ::OutputHolder) + assertEquals("nextState: state", nextState) + assertNotNull(output) + assertEquals("output", output.value) + } + + @Test fun `applyTo doens't invoke mapOutput when output is not set`() { + val action = object : WorkflowAction { + override fun Updater.apply() { + nextState = "nextState: $nextState" + } + } + var outputCalls = 0 + val (nextState, output) = action.applyTo("state") { outputCalls++ } + assertEquals("nextState: state", nextState) + assertNull(output) + assertEquals(0, outputCalls) + } + + @Test fun `applyTo only invokes mapOutput once when output is set multiple times`() { + val action = object : WorkflowAction { + override fun Updater.apply() { + setOutput("first output") + nextState = "nextState: $nextState" + setOutput(null) + setOutput("third output") + } + } + val outputs = mutableListOf() + val (nextState, output) = action.applyTo("state") { outputs += it } + assertEquals("nextState: state", nextState) + assertNotNull(output) + assertEquals(listOf("third output"), outputs) + } + + private data class OutputHolder(val value: O) +} diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt b/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt index 209000aa19..c3e8c88793 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/RenderWorkflow.kt @@ -117,7 +117,7 @@ import kotlin.coroutines.EmptyCoroutineContext * rendering. */ @OptIn(ExperimentalCoroutinesApi::class, ExperimentalWorkflowApi::class) -fun renderWorkflowIn( +fun renderWorkflowIn( workflow: Workflow, scope: CoroutineScope, props: StateFlow, @@ -163,19 +163,9 @@ fun renderWorkflowIn( // After receiving an output, the next render pass must be done before emitting that output, // so that the workflow states appear consistent to observers of the outputs and renderings. renderingsAndSnapshots.value = runner.nextRendering() - output?.let { onOutput(it) } + output.withValue { onOutput(it) } } } return renderingsAndSnapshots } - -/** - * If this is already a [CancellationException], returns it as-is, otherwise wraps it in one with - * the given message. - */ -private inline fun Throwable?.toCancellationException(message: () -> String) = when (this) { - null -> null - is CancellationException -> this - else -> CancellationException(message(), this) -} diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/SimpleLoggingWorkflowInterceptor.kt b/workflow-runtime/src/main/java/com/squareup/workflow/SimpleLoggingWorkflowInterceptor.kt index 7e99abc4e3..d1d598fd77 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/SimpleLoggingWorkflowInterceptor.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/SimpleLoggingWorkflowInterceptor.kt @@ -53,7 +53,7 @@ open class SimpleLoggingWorkflowInterceptor : WorkflowInterceptor { proceed(old, new, state) } - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowInterceptor.kt b/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowInterceptor.kt index d4b888a84c..1a28f47a85 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowInterceptor.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowInterceptor.kt @@ -93,7 +93,7 @@ interface WorkflowInterceptor { /** * Intercepts calls to [StatefulWorkflow.render]. */ - fun onRender( + fun onRender( props: P, state: S, context: RenderContext, @@ -146,7 +146,7 @@ object NoopWorkflowInterceptor : WorkflowInterceptor * [WorkflowInterceptor]. */ @OptIn(ExperimentalWorkflowApi::class) -internal fun WorkflowInterceptor.intercept( +internal fun WorkflowInterceptor.intercept( workflow: StatefulWorkflow, workflowSession: WorkflowSession ): StatefulWorkflow = if (this === NoopWorkflowInterceptor) { diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/ChainedWorkflowInterceptor.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/ChainedWorkflowInterceptor.kt index d0b2170d17..d81eca7cd8 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/ChainedWorkflowInterceptor.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/ChainedWorkflowInterceptor.kt @@ -72,7 +72,7 @@ internal class ChainedWorkflowInterceptor( return chainedProceed(old, new, state) } - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/MaybeOutput.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/MaybeOutput.kt new file mode 100644 index 0000000000..58345259aa --- /dev/null +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/MaybeOutput.kt @@ -0,0 +1,41 @@ +/* + * 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. + */ +package com.squareup.workflow.internal + +/** + * Simple Optional type to hold outputs. Create with either [NONE] or [of]. + */ +@Suppress("UNCHECKED_CAST") +internal class MaybeOutput constructor(private val value: Any?) { + + inline val hasValue: Boolean get() = value !== NO_VALUE + + fun getValueOrThrow(): O { + if (value === NO_VALUE) throw NoSuchElementException() + return value as O + } + + inline fun withValue(block: (O) -> Unit) { + if (value !== NO_VALUE) block(value as O) + } + + companion object { + private val NO_VALUE = Any() + + fun none(): MaybeOutput = MaybeOutput(NO_VALUE) + fun of(value: O) = MaybeOutput(value) + } +} diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt index 7dd0e0b6ed..92273f00fd 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt @@ -30,15 +30,15 @@ import kotlinx.coroutines.channels.SendChannel * * Not for general application use. */ -class RealRenderContext( +class RealRenderContext( private val renderer: Renderer, private val workerRunner: WorkerRunner, private val sideEffectRunner: SideEffectRunner, private val eventActionsChannel: SendChannel> ) : RenderContext, Sink> { - interface Renderer { - fun render( + interface Renderer { + fun render( child: Workflow, props: ChildPropsT, key: String, @@ -46,7 +46,7 @@ class RealRenderContext( ): ChildRenderingT } - interface WorkerRunner { + interface WorkerRunner { fun runningWorker( worker: Worker, key: String, @@ -93,7 +93,7 @@ class RealRenderContext( eventActionsChannel.offer(value) } - override fun renderChild( + override fun renderChild( child: Workflow, props: ChildPropsT, key: String, diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt index c693e5645c..025707dd30 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt @@ -95,10 +95,10 @@ import kotlin.coroutines.EmptyCoroutineContext * @param snapshotCache */ @OptIn(ExperimentalWorkflowApi::class) -internal class SubtreeManager( +internal class SubtreeManager( snapshotCache: Map, private val contextForChildren: CoroutineContext, - private val emitActionToParent: (WorkflowAction) -> Any?, + private val emitActionToParent: (WorkflowAction) -> MaybeOutput, private val workflowSession: WorkflowSession? = null, private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, private val idCounter: IdCounter? = null, @@ -130,7 +130,7 @@ internal class SubtreeManager( } /* ktlint-disable parameter-list-wrapping */ - override fun render( + override fun render( child: Workflow, props: ChildPropsT, key: String, @@ -158,7 +158,7 @@ internal class SubtreeManager( * Uses [selector] to invoke [WorkflowNode.tick] for every running child workflow this instance * is managing. */ - fun tickChildren(selector: SelectBuilder) { + fun tickChildren(selector: SelectBuilder>) { children.forEachActive { child -> child.workflowNode.tick(selector) } @@ -173,7 +173,7 @@ internal class SubtreeManager( return snapshots } - private fun createChildNode( + private fun createChildNode( child: Workflow, initialProps: ChildPropsT, key: String, @@ -182,7 +182,7 @@ internal class SubtreeManager( val id = child.id(key) lateinit var node: WorkflowChildNode - fun acceptChildOutput(output: ChildOutputT): Any? { + fun acceptChildOutput(output: ChildOutputT): MaybeOutput { val action = node.acceptChildOutput(output) return emitActionToParent(action) } diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt index 7e6dff01b3..6f39d85ddb 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkerChildNode.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.channels.ReceiveChannel * after the worker finishes so that they aren't immediately restarted on the next render pass. This * flag indicates that the channel should not be polled on the next tick. */ -internal class WorkerChildNode( +internal class WorkerChildNode( val worker: Worker, val key: String, val channel: ReceiveChannel>, @@ -47,7 +47,7 @@ internal class WorkerChildNode( /** * Updates the handler function that will be invoked by [acceptUpdate]. */ - fun setHandler(newHandler: (T2) -> WorkflowAction) { + fun setHandler(newHandler: (T2) -> WorkflowAction) { @Suppress("UNCHECKED_CAST") handler = newHandler as (T) -> WorkflowAction } diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt index cea5ba0ea5..369e8eb045 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowChildNode.kt @@ -29,9 +29,9 @@ import com.squareup.workflow.internal.InlineLinkedList.InlineListNode /* ktlint-disable parameter-list-wrapping */ internal class WorkflowChildNode< ChildPropsT, - ChildOutputT : Any, + ChildOutputT, ParentStateT, - ParentOutputT : Any + ParentOutputT >( val workflow: Workflow<*, ChildOutputT, *>, private var handler: (ChildOutputT) -> WorkflowAction, @@ -55,7 +55,7 @@ internal class WorkflowChildNode< /** * Updates the handler function that will be invoked by [acceptChildOutput]. */ - fun setHandler(newHandler: (CO) -> WorkflowAction) { + fun setHandler(newHandler: (CO) -> WorkflowAction) { @Suppress("UNCHECKED_CAST") handler = newHandler as (ChildOutputT) -> WorkflowAction } @@ -78,6 +78,6 @@ internal class WorkflowChildNode< * Wrapper around [handler] that allows calling it with erased types. */ @Suppress("UNCHECKED_CAST") - fun acceptChildOutput(output: Any): WorkflowAction = + fun acceptChildOutput(output: Any?): WorkflowAction = handler(output as ChildOutputT) } diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt index 70c9e7cf86..bb911609de 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt @@ -48,21 +48,19 @@ import kotlin.coroutines.EmptyCoroutineContext * * @param emitOutputToParent A function that this node will call when it needs to emit an output * value to its parent. Returns either the output to be emitted from the root workflow, or null. - * @param initialState Allows unit tests to start the node from a given state, instead of calling - * [StatefulWorkflow.initialState]. * @param workerContext [CoroutineContext] that is appended to the end of the context used to launch * worker coroutines. This context will override anything from the workflow's scope and any other * hard-coded values added to worker contexts. It must not contain a [Job] element (it would violate * structured concurrency). */ @OptIn(ExperimentalWorkflowApi::class) -internal class WorkflowNode( +internal class WorkflowNode( val id: WorkflowNodeId, workflow: StatefulWorkflow, initialProps: PropsT, snapshot: TreeSnapshot, baseContext: CoroutineContext, - private val emitOutputToParent: (OutputT) -> Any? = { it }, + private val emitOutputToParent: (OutputT) -> MaybeOutput = { MaybeOutput.of(it) }, override val parent: WorkflowSession? = null, private val interceptor: WorkflowInterceptor = NoopWorkflowInterceptor, idCounter: IdCounter? = null, @@ -185,7 +183,7 @@ internal class WorkflowNode( * * It is an error to call this method after calling [cancel]. */ - fun tick(selector: SelectBuilder) { + fun tick(selector: SelectBuilder>) { // Listen for any child workflow updates. subtreeManager.tickChildren(selector) @@ -200,7 +198,7 @@ internal class WorkflowNode( // Set the tombstone flag so we don't continue to listen to the subscription. child.tombstone = true // Nothing to do on close other than update the session, so don't emit any output. - return@onReceive null + return@onReceive MaybeOutput.none() } else { val update = child.acceptUpdate(valueOrDone.value) @Suppress("UNCHECKED_CAST") @@ -279,11 +277,11 @@ internal class WorkflowNode( * Applies [action] to this workflow's [state] and * [emits an output to its parent][emitOutputToParent] if necessary. */ - private fun applyAction(action: WorkflowAction): T? { - val (newState, output) = action.applyTo(state) + private fun applyAction(action: WorkflowAction): MaybeOutput { + val (newState, tickResult) = action.applyTo(state, emitOutputToParent) state = newState @Suppress("UNCHECKED_CAST") - return output?.let(emitOutputToParent) as T? + return (tickResult ?: MaybeOutput.none()) as MaybeOutput } private fun createWorkerNode( diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt index 88a307978e..3c58ba61c1 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNodeId.kt @@ -67,5 +67,5 @@ internal data class WorkflowNodeId( } } -internal fun , I, O : Any, R> +internal fun , I, O, R> W.id(key: String = ""): WorkflowNodeId = WorkflowNodeId(this, key) diff --git a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt index e0eac044f8..4237e84856 100644 --- a/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt +++ b/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowRunner.kt @@ -32,7 +32,7 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @OptIn(ExperimentalCoroutinesApi::class, ExperimentalWorkflowApi::class) -internal class WorkflowRunner( +internal class WorkflowRunner( scope: CoroutineScope, protoWorkflow: Workflow, props: StateFlow, @@ -83,7 +83,7 @@ internal class WorkflowRunner( // Tick _might_ return an output, but if it returns null, it means the state or a child // probably changed, so we should re-render/snapshot and emit again. - suspend fun nextOutput(): OutputT? = select { + suspend fun nextOutput(): MaybeOutput = select { // Stop trying to read from the inputs channel after it's closed. if (!propsChannel.isClosedForReceive) { // TODO(https://github.com/square/workflow/issues/512) Replace with receiveOrClosed. @@ -95,7 +95,7 @@ internal class WorkflowRunner( } } // Return null to tell the caller to do another render pass, but not emit an output. - return@onReceiveOrNull null + return@onReceiveOrNull MaybeOutput.none() } } diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt index d88ef883ea..1c402b498a 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/RenderWorkflowInTest.kt @@ -159,6 +159,25 @@ class RenderWorkflowInTest { assertEquals(listOf("foo", "bar"), receivedOutputs) } + @Test fun `onOutput is not called when no output emitted`() { + val workflow = Workflow.stateless { props -> props } + var onOutputCalls = 0 + val props = MutableStateFlow(0) + val renderings = renderWorkflowIn(workflow, scope, props) { onOutputCalls++ } + assertEquals(0, renderings.value.rendering) + assertEquals(0, onOutputCalls) + + props.value = 1 + scope.advanceUntilIdle() + assertEquals(1, renderings.value.rendering) + assertEquals(0, onOutputCalls) + + props.value = 2 + scope.advanceUntilIdle() + assertEquals(2, renderings.value.rendering) + assertEquals(0, onOutputCalls) + } + /** * Since the initial render occurs before launching the coroutine, an exception thrown from it * doesn't implicitly cancel the scope. If it did, the reception would be reported twice: once to diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/WorkflowInterceptorTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/WorkflowInterceptorTest.kt index f09090ec8d..1808b05736 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/WorkflowInterceptorTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/WorkflowInterceptorTest.kt @@ -64,7 +64,7 @@ class WorkflowInterceptorTest { handler: (EventT) -> WorkflowAction ): (EventT) -> Unit = fail() - override fun renderChild( + override fun renderChild( child: Workflow, props: ChildPropsT, key: String, diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/internal/ChainedWorkflowInterceptorTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/ChainedWorkflowInterceptorTest.kt index d67554c792..abca68af0c 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/internal/ChainedWorkflowInterceptorTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/internal/ChainedWorkflowInterceptorTest.kt @@ -186,7 +186,7 @@ class ChainedWorkflowInterceptorTest { @Test fun `chains calls to onRender() in left-to-right order`() { val interceptor1 = object : WorkflowInterceptor { - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, @@ -200,7 +200,7 @@ class ChainedWorkflowInterceptorTest { )) as R } val interceptor2 = object : WorkflowInterceptor { - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, @@ -267,7 +267,7 @@ class ChainedWorkflowInterceptorTest { fail() } - override fun renderChild( + override fun renderChild( child: Workflow, props: ChildPropsT, key: String, diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt index 4ccedd16ac..83c1bb4490 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/internal/RealRenderContextTest.kt @@ -57,7 +57,7 @@ class RealRenderContextTest { ) @Suppress("UNCHECKED_CAST") - override fun render( + override fun render( child: Workflow, props: ChildPropsT, key: String, @@ -105,7 +105,7 @@ class RealRenderContextTest { } private class PoisonRenderer : Renderer { - override fun render( + override fun render( child: Workflow, props: ChildPropsT, key: String, @@ -224,9 +224,9 @@ class RealRenderContextTest { sink.send("foo") val update = eventActionsChannel.poll()!! - val (state, output) = update.applyTo("state") + val (state, output) = update.applyTo("state") { MaybeOutput.of(it) } assertEquals("state", state) - assertEquals("foo", output) + assertEquals("foo", output?.getValueOrThrow()) } @Test fun `renderChild works`() { @@ -242,9 +242,9 @@ class RealRenderContextTest { assertEquals("key", key) val (state, output) = handler.invoke("output") - .applyTo("state") + .applyTo("state") { MaybeOutput.of(it) } assertEquals("state", state) - assertEquals("output:output", output) + assertEquals("output:output", output?.getValueOrThrow()) } @Test fun `all methods throw after freeze`() { diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt index 711b8bb681..8a262190b0 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/internal/SubtreeManagerTest.kt @@ -176,9 +176,11 @@ class SubtreeManagerTest { assertFalse(tickOutput.isCompleted) eventHandler("event!") - val update = tickOutput.await()!! - val (_, output) = update.applyTo("state") - assertEquals("case output:workflow output:event!", output) + val update = tickOutput.await() + .getValueOrThrow() + + val (_, output) = update.applyTo("state") { MaybeOutput.of(it) } + assertEquals("case output:workflow output:event!", output?.getValueOrThrow()) } } @@ -194,18 +196,24 @@ class SubtreeManagerTest { render { action { setOutput("initial handler: $it") } } .let { rendering -> rendering.eventHandler("initial output") - val initialAction = manager.tickAction()!! - val (_, initialOutput) = initialAction.applyTo("") - assertEquals("initial handler: workflow output:initial output", initialOutput) + val initialAction = manager.tickAction() + .getValueOrThrow() + val (_, initialOutput) = initialAction.applyTo("") { MaybeOutput.of(it) } + assertEquals( + "initial handler: workflow output:initial output", initialOutput?.getValueOrThrow() + ) } // Do a second render + tick, but with a different handler function. render { action { setOutput("second handler: $it") } } .let { rendering -> rendering.eventHandler("second output") - val secondAction = manager.tickAction()!! - val (_, secondOutput) = secondAction.applyTo("") - assertEquals("second handler: workflow output:second output", secondOutput) + val secondAction = manager.tickAction() + .getValueOrThrow() + val (_, secondOutput) = secondAction.applyTo("") { MaybeOutput.of(it) } + assertEquals( + "second handler: workflow output:second output", secondOutput?.getValueOrThrow() + ) } } } @@ -240,9 +248,9 @@ class SubtreeManagerTest { assertEquals(1, workflow.serializes) } - private suspend fun SubtreeManager.tickAction(): WorkflowAction? = - select { tickChildren(this) } + private suspend fun SubtreeManager.tickAction() = + select>> { tickChildren(this) } private fun subtreeManagerForTest() = - SubtreeManager(emptyMap(), context, emitActionToParent = { it }) + SubtreeManager(emptyMap(), context, emitActionToParent = { MaybeOutput.of(it) }) } diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt index 8554821898..d60948704f 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowNodeTest.kt @@ -187,19 +187,22 @@ class WorkflowNodeTest { return "" } } - val node = WorkflowNode(workflow.id(), workflow, "", TreeSnapshot.NONE, context, { "tick:$it" }) + val node = WorkflowNode( + workflow.id(), workflow, "", TreeSnapshot.NONE, context, + emitOutputToParent = { MaybeOutput.of("tick:$it") } + ) node.render(workflow, "") sink.send("event") val result = runBlocking { withTimeout(10) { - select { + select> { node.tick(this) } } } - assertEquals("tick:event", result) + assertEquals("tick:event", result.getValueOrThrow()) } @Test fun `accepts events sent to stale renderings`() { @@ -222,7 +225,9 @@ class WorkflowNodeTest { return "" } } - val node = WorkflowNode(workflow.id(), workflow, "", TreeSnapshot.NONE, context, { "tick:$it" }) + val node = WorkflowNode(workflow.id(), workflow, "", TreeSnapshot.NONE, context, + emitOutputToParent = { MaybeOutput.of("tick:$it") } + ) node.render(workflow, "") sink.send("event") @@ -231,13 +236,13 @@ class WorkflowNodeTest { val result = runBlocking { withTimeout(10) { List(2) { - select { + select> { node.tick(this) } } } } - assertEquals(listOf("tick:event", "tick:event2"), result) + assertEquals(listOf("tick:event", "tick:event2"), result.map { it.getValueOrThrow() }) } @Test fun `send allows subsequent events on same rendering`() { @@ -333,7 +338,7 @@ class WorkflowNodeTest { val output = runBlocking { try { withTimeout(1) { - select { + select> { node.tick(this) } } @@ -345,14 +350,14 @@ class WorkflowNodeTest { channel.send("element") withTimeout(1) { - select { + select> { node.tick(this) } } } assertEquals("element", update) - assertEquals("update:element", output) + assertEquals("update:element", output.getValueOrThrow()) } @Test fun `worker is cancelled`() { @@ -400,7 +405,7 @@ class WorkflowNodeTest { // This tick will process the event handler, it won't close the channel yet. withTimeout(1) { - select { + select> { node.tick(this) } } @@ -519,13 +524,13 @@ class WorkflowNodeTest { val result = runBlocking { // Result should be available instantly, any delay at all indicates something is broken. withTimeout(1) { - select { + select> { node.tick(this) } } } - assertEquals("result", result) + assertEquals("result", result.getValueOrThrow()) } @Test fun `sideEffect is cancelled when stops being ran`() { @@ -1084,7 +1089,7 @@ class WorkflowNodeTest { lateinit var interceptedRendering: String lateinit var interceptedSession: WorkflowSession val interceptor = object : WorkflowInterceptor { - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, @@ -1169,7 +1174,7 @@ class WorkflowNodeTest { @Test fun `interceptor is propagated to children`() { val interceptor = object : WorkflowInterceptor { @Suppress("UNCHECKED_CAST") - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, @@ -1274,7 +1279,7 @@ class WorkflowNodeTest { sink.send("hello") runBlocking { - select { + select> { node.tick(this) } } @@ -1293,19 +1298,44 @@ class WorkflowNodeTest { initialProps = Unit, snapshot = TreeSnapshot.NONE, baseContext = Unconfined, - emitOutputToParent = { "output:$it" } + emitOutputToParent = { MaybeOutput.of("output:$it") } ) val rendering = node.render(workflow.asStatefulWorkflow(), Unit) rendering.send("hello") val output = runBlocking { - select { + select> { node.tick(this) } } - assertEquals("output:hello", output) + assertEquals("output:hello", output.getValueOrThrow()) + } + + @Test fun `actionSink action allows null output`() { + val workflow = Workflow.stateless> { + actionSink.contraMap { action { setOutput(null) } } + } + val node = WorkflowNode( + workflow.id(), + workflow.asStatefulWorkflow(), + initialProps = Unit, + snapshot = TreeSnapshot.NONE, + baseContext = Unconfined, + emitOutputToParent = { MaybeOutput.of(it) } + ) + val rendering = node.render(workflow.asStatefulWorkflow(), Unit) + + rendering.send("hello") + + val output = runBlocking { + select> { + node.tick(this) + } + } + + assertNull(output.getValueOrThrow()) } @Test fun `worker action changes state`() { @@ -1326,7 +1356,7 @@ class WorkflowNodeTest { node.render(workflow.asStatefulWorkflow(), Unit) runBlocking { - select { + select> { node.tick(this) } } @@ -1344,17 +1374,39 @@ class WorkflowNodeTest { initialProps = Unit, snapshot = TreeSnapshot.NONE, baseContext = Unconfined, - emitOutputToParent = { "output:$it" } + emitOutputToParent = { MaybeOutput.of("output:$it") } ) node.render(workflow.asStatefulWorkflow(), Unit) val output = runBlocking { - select { + select> { node.tick(this) } } - assertEquals("output:hello", output) + assertEquals("output:hello", output.getValueOrThrow()) + } + + @Test fun `worker action allows null output`() { + val workflow = Worker.from { null } + .asWorkflow() + val node = WorkflowNode( + workflow.id(), + workflow.asStatefulWorkflow(), + initialProps = Unit, + snapshot = TreeSnapshot.NONE, + baseContext = Unconfined, + emitOutputToParent = { MaybeOutput.of(it) } + ) + node.render(workflow.asStatefulWorkflow(), Unit) + + val output = runBlocking { + select> { + node.tick(this) + } + } + + assertNull(output.getValueOrThrow()) } @Test fun `child action changes state`() { @@ -1377,7 +1429,7 @@ class WorkflowNodeTest { node.render(workflow.asStatefulWorkflow(), Unit) runBlocking { - select { + select> { node.tick(this) } } @@ -1398,17 +1450,42 @@ class WorkflowNodeTest { initialProps = Unit, snapshot = TreeSnapshot.NONE, baseContext = Unconfined, - emitOutputToParent = { "output:$it" } + emitOutputToParent = { MaybeOutput.of("output:$it") } + ) + node.render(workflow.asStatefulWorkflow(), Unit) + + val output = runBlocking { + select> { + node.tick(this) + } + } + + assertEquals("output:child:hello", output.getValueOrThrow()) + } + + @Test fun `child action allows null output`() { + val child = Worker.from { null } + .asWorkflow() + val workflow = Workflow.stateless { + renderChild(child) { action { setOutput(null) } } + } + val node = WorkflowNode( + workflow.id(), + workflow.asStatefulWorkflow(), + initialProps = Unit, + snapshot = TreeSnapshot.NONE, + baseContext = Unconfined, + emitOutputToParent = { MaybeOutput.of(it) } ) node.render(workflow.asStatefulWorkflow(), Unit) val output = runBlocking { - select { + select> { node.tick(this) } } - assertEquals("output:child:hello", output) + assertNull(output.getValueOrThrow()) } private class TestSession(override val sessionId: Long = 0) : WorkflowSession { @@ -1417,7 +1494,7 @@ class WorkflowNodeTest { override val parent: WorkflowSession? = null } - private fun Worker.asWorkflow() = Workflow.stateless { + private fun Worker.asWorkflow() = Workflow.stateless { runningWorker(this@asWorkflow) { action { setOutput(it) } } } } diff --git a/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt index 3d50c67de3..3e6a536ab9 100644 --- a/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt +++ b/workflow-runtime/src/test/java/com/squareup/workflow/internal/WorkflowRunnerTest.kt @@ -16,10 +16,10 @@ package com.squareup.workflow.internal import com.squareup.workflow.ExperimentalWorkflowApi +import com.squareup.workflow.NoopWorkflowInterceptor import com.squareup.workflow.TreeSnapshot import com.squareup.workflow.Worker import com.squareup.workflow.Workflow -import com.squareup.workflow.NoopWorkflowInterceptor import com.squareup.workflow.action import com.squareup.workflow.runningWorker import com.squareup.workflow.stateful @@ -37,6 +37,7 @@ import org.junit.Test import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -100,7 +101,7 @@ class WorkflowRunnerTest { dispatcher.resumeDispatcher() assertTrue(output.isCompleted) - assertNull(output.getCompleted()) + assertFalse(output.getCompleted().hasValue) val rendering = runner.nextRendering().rendering assertEquals("changed", rendering) } @@ -126,7 +127,7 @@ class WorkflowRunnerTest { val output = scope.async { runner.nextOutput() } .getCompleted() - assertEquals("output: work", output) + assertEquals("output: work", output.getValueOrThrow()) val updatedRendering = runner.nextRendering().rendering assertEquals("state: work", updatedRendering) @@ -158,13 +159,13 @@ class WorkflowRunnerTest { val firstOutput = scope.async { runner.nextOutput() } .getCompleted() // First update will be props, so no output value. - assertNull(firstOutput) + assertFalse(firstOutput.hasValue) val secondRendering = runner.nextRendering().rendering assertEquals("changed props|initial state(initial props)", secondRendering) val secondOutput = scope.async { runner.nextOutput() } .getCompleted() - assertEquals("output: work", secondOutput) + assertEquals("output: work", secondOutput.getValueOrThrow()) val thirdRendering = runner.nextRendering().rendering assertEquals("changed props|state: work", thirdRendering) } diff --git a/workflow-testing/api/workflow-testing.api b/workflow-testing/api/workflow-testing.api index bf32762c83..4bb8d06c1f 100644 --- a/workflow-testing/api/workflow-testing.api +++ b/workflow-testing/api/workflow-testing.api @@ -20,7 +20,14 @@ public final class com/squareup/workflow/testing/RenderIdempotencyChecker : com/ public abstract interface class com/squareup/workflow/testing/RenderTestResult { public abstract fun verifyAction (Lkotlin/jvm/functions/Function1;)V + public abstract fun verifyActionOutput (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/testing/RenderTestResult; public abstract fun verifyActionResult (Lkotlin/jvm/functions/Function2;)V + public abstract fun verifyActionState (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/testing/RenderTestResult; + public abstract fun verifyNoActionOutput ()Lcom/squareup/workflow/testing/RenderTestResult; +} + +public final class com/squareup/workflow/testing/RenderTestResult$DefaultImpls { + public static fun verifyActionResult (Lcom/squareup/workflow/testing/RenderTestResult;Lkotlin/jvm/functions/Function2;)V } public abstract interface class com/squareup/workflow/testing/RenderTester { diff --git a/workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt index 102747eca5..4decf43573 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow/testing/RealRenderTester.kt @@ -28,7 +28,7 @@ import com.squareup.workflow.testing.RealRenderTester.Expectation.ExpectedWorker import com.squareup.workflow.testing.RealRenderTester.Expectation.ExpectedWorkflow import kotlin.reflect.KClass -internal class RealRenderTester( +internal class RealRenderTester( private val workflow: StatefulWorkflow, private val props: PropsT, private val state: StateT, @@ -44,7 +44,7 @@ internal class RealRenderTester( internal sealed class Expectation { open val output: EmittedOutput? = null - data class ExpectedWorkflow( + data class ExpectedWorkflow( val workflowType: KClass>, val key: String, val assertProps: (props: Any?) -> Unit, @@ -63,7 +63,7 @@ internal class RealRenderTester( override val actionSink: Sink> get() = this - override fun expectWorkflow( + override fun expectWorkflow( workflowType: KClass>, rendering: ChildRenderingT, key: String, @@ -122,7 +122,7 @@ internal class RealRenderTester( return this } - override fun renderChild( + override fun renderChild( child: Workflow, props: ChildPropsT, key: String, @@ -189,6 +189,7 @@ internal class RealRenderTester( processedAction = value } + @Suppress("OverridingDeprecatedMember") override fun onEvent( handler: (EventT) -> WorkflowAction ): (EventT) -> Unit = { event -> send(handler(event)) } @@ -198,10 +199,32 @@ internal class RealRenderTester( block(action) } - override fun verifyActionResult(block: (StateT, OutputT?) -> Unit) { + override fun verifyActionState(block: (newState: StateT) -> Unit) = apply { verifyAction { action -> - val (newState, output) = action.applyTo(state) - block(newState, output) + // Don't care about output. + val (newState, _) = action.applyTo(state, mapOutput = {}) + block(newState) + } + } + + override fun verifyActionOutput(block: (output: OutputT) -> Unit) = apply { + verifyAction { action -> + var outputWasSet = false + action.applyTo(state) { output -> + outputWasSet = true + block(output) + } + if (!outputWasSet) { + throw AssertionError("Expected action to set an output") + } + } + } + + override fun verifyNoActionOutput() = apply { + verifyAction { action -> + action.applyTo(state) { + throw AssertionError("Expected no output, but action set output to: $it") + } } } diff --git a/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderIdempotencyChecker.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderIdempotencyChecker.kt index 20cab9efa0..f7599b880f 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderIdempotencyChecker.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderIdempotencyChecker.kt @@ -34,7 +34,7 @@ import java.util.LinkedList */ @OptIn(ExperimentalWorkflowApi::class) object RenderIdempotencyChecker : WorkflowInterceptor { - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, @@ -59,7 +59,7 @@ object RenderIdempotencyChecker : WorkflowInterceptor { * A [RenderContext] that can record the result of rendering children over a render pass, and then * play them back over a second render pass that doesn't actually perform any actions. */ -private class RecordingRenderContext( +private class RecordingRenderContext( private val delegate: RenderContext ) : RenderContext { @@ -95,7 +95,7 @@ private class RecordingRenderContext( private val childRenderings = LinkedList() - override fun renderChild( + override fun renderChild( child: Workflow, props: ChildPropsT, key: String, diff --git a/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt index 922fe1a811..468daca99a 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTestResult.kt @@ -24,7 +24,7 @@ import com.squareup.workflow.WorkflowAction * @see verifyAction * @see verifyActionResult */ -interface RenderTestResult { +interface RenderTestResult { /** * Asserts that the render pass handled either a workflow/worker output or a rendering event, and @@ -37,6 +37,46 @@ interface RenderTestResult { */ fun verifyAction(block: (WorkflowAction) -> Unit) + /** + * If the render pass handled either a workflow/worker output or a rendering event, "executes" the + * action with the state passed to [renderTester], then invokes [block] with the resulting state + * value. + * + * If the workflow didn't process any actions, `newState` will be the initial state. + * + * Note that by using this method, you're also testing the implementation of your action. This can + * be useful if your actions are anonymous. If they are a sealed class or enum, use [verifyAction] + * instead and write separate unit tests for your action implementations. + */ + fun verifyActionState(block: (newState: StateT) -> Unit): RenderTestResult + + /** + * If the render pass handled either a workflow/worker output or a rendering event, "executes" the + * action with the state passed to [renderTester], verifies that the action set an output, then + * invokes [block] with the resulting output value. + * + * If the workflow didn't process any actions, or no output was set, an [AssertionError] will be + * thrown. + * + * Note that by using this method, you're also testing the implementation of your action. This can + * be useful if your actions are anonymous. If they are a sealed class or enum, use [verifyAction] + * instead and write separate unit tests for your action implementations. + */ + fun verifyActionOutput(block: (output: OutputT) -> Unit): RenderTestResult + + /** + * If the render pass handled either a workflow/worker output or a rendering event, "executes" the + * action with the state passed to [renderTester], and then verifies that the action did not set + * any output. + * + * If the workflow didn't process any actions, this method will do nothing. + * + * Note that by using this method, you're also testing the implementation of your action. This can + * be useful if your actions are anonymous. If they are a sealed class or enum, use [verifyAction] + * instead and write separate unit tests for your action implementations. + */ + fun verifyNoActionOutput(): RenderTestResult + /** * Asserts that the render pass handled either a workflow/worker output or a rendering event, * "executes" the action with the state passed to [renderTester], then invokes [block] with the @@ -46,8 +86,25 @@ interface RenderTestResult { * will be null. * * Note that by using this method, you're also testing the implementation of your action. This can - * be useful if your actions anonymous. If they are a sealed class or enum, use [verifyAction] + * be useful if your actions are anonymous. If they are a sealed class or enum, use [verifyAction] * instead and write separate unit tests for your action implementations. + * + * Note that if [OutputT] is nullable, this method does not distinguish between an no output and + * null output. Use [RenderTestResult.verifyActionOutput] and + * [RenderTestResult.verifyNoActionOutput] instead. */ - fun verifyActionResult(block: (newState: StateT, output: OutputT?) -> Unit) + @Deprecated("Use verifyActionState and verify(No)ActionOutput") + fun verifyActionResult(block: (newState: StateT, output: OutputT?) -> Unit) { + var state: StateT? = null + var output: OutputT? = null + verifyActionState { state = it } + try { + verifyActionOutput { output = it } + } catch (e: AssertionError) { + // No output was set, leave as null. + } + + @Suppress("UNCHECKED_CAST") + block(state as StateT, output) + } } diff --git a/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt index 668b0cf061..4980f481eb 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt @@ -28,7 +28,7 @@ import kotlin.reflect.KClass * See [RenderTester] for usage documentation. */ @Suppress("UNCHECKED_CAST") -fun Workflow.renderTester( +fun Workflow.renderTester( props: PropsT ): RenderTester { val statefulWorkflow = asStatefulWorkflow() as StatefulWorkflow @@ -44,7 +44,7 @@ fun Workflow.re * See [RenderTester] for usage documentation. */ /* ktlint-disable parameter-list-wrapping */ -fun +fun StatefulWorkflow.renderTester( props: PropsT, initialState: StateT @@ -190,7 +190,7 @@ fun * rendering.onCancelClicked() * ``` */ -interface RenderTester { +interface RenderTester { /** * Specifies that this render pass is expected to render a particular child workflow. @@ -208,7 +208,7 @@ interface RenderTester { * rendered. The [WorkflowAction] used to handle this output can be verified using methods on * [RenderTestResult]. */ - fun expectWorkflow( + fun expectWorkflow( workflowType: KClass>, rendering: ChildRenderingT, key: String = "", @@ -273,7 +273,7 @@ interface RenderTester { * [RenderTestResult]. */ /* ktlint-disable parameter-list-wrapping */ -fun +fun RenderTester.expectWorker( doesSameWorkAs: Worker<*>, key: String = "", diff --git a/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt b/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt index 93cf4c3073..a1e7124f8e 100644 --- a/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt +++ b/workflow-testing/src/main/java/com/squareup/workflow/testing/WorkflowTester.kt @@ -68,7 +68,7 @@ import kotlin.coroutines.EmptyCoroutineContext * - [sendProps] * - Send a new [PropsT] to the root workflow. */ -class WorkflowTester @TestOnly internal constructor( +class WorkflowTester @TestOnly internal constructor( private val props: MutableStateFlow, private val renderingsAndSnapshotsFlow: Flow>, private val outputs: ReceiveChannel @@ -188,7 +188,7 @@ class WorkflowTester @TestOnly internal const * All workflow-related coroutines are cancelled when the block exits. */ @TestOnly -fun Workflow.testFromStart( +fun Workflow.testFromStart( props: PropsT, testParams: WorkflowTestParams = WorkflowTestParams(), context: CoroutineContext = EmptyCoroutineContext, @@ -201,7 +201,7 @@ fun Workflow * All workflow-related coroutines are cancelled when the block exits. */ @TestOnly -fun Workflow.testFromStart( +fun Workflow.testFromStart( testParams: WorkflowTestParams = WorkflowTestParams(), context: CoroutineContext = EmptyCoroutineContext, block: WorkflowTester.() -> T @@ -216,7 +216,7 @@ fun Workflow.testFromS */ /* ktlint-disable parameter-list-wrapping */ @TestOnly -fun +fun StatefulWorkflow.testFromState( props: PropsT, initialState: StateT, @@ -234,7 +234,7 @@ fun */ /* ktlint-disable parameter-list-wrapping */ @TestOnly -fun +fun StatefulWorkflow.testFromState( initialState: StateT, context: CoroutineContext = EmptyCoroutineContext, @@ -250,7 +250,7 @@ fun @OptIn(ExperimentalWorkflowApi::class) @TestOnly /* ktlint-disable parameter-list-wrapping */ -fun +fun StatefulWorkflow.test( props: PropsT, testParams: WorkflowTestParams = WorkflowTestParams(), diff --git a/workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt b/workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt index 3f03545a4c..23c0f5adc9 100644 --- a/workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt +++ b/workflow-testing/src/test/java/com/squareup/workflow/testing/RealRenderTesterTest.kt @@ -672,6 +672,7 @@ class RealRenderTesterTest { } } + @Suppress("DEPRECATION") @Test fun `verifyAction and verifyActionResult pass when no action processed`() { val workflow = Workflow.stateless> { actionSink.contraMap { it } @@ -688,6 +689,7 @@ class RealRenderTesterTest { } } + @Suppress("DEPRECATION") @Test fun `verifyActionResult works`() { class TestAction : WorkflowAction { override fun Updater.apply() { @@ -711,6 +713,157 @@ class RealRenderTesterTest { } } + @Test fun `verifyActionState and verifyActionOutput chain`() { + class TestAction : WorkflowAction { + override fun Updater.apply() { + nextState = "new state" + setOutput("output") + } + } + + val workflow = Workflow.stateful>( + initialState = { "initial" }, + render = { _, _ -> actionSink.contraMap { it } } + ) + val testResult = workflow.renderTester(Unit) + .render { sink -> + sink.send(TestAction()) + } + + testResult + .verifyActionState { assertEquals("new state", it) } + .verifyActionOutput { assertEquals("output", it) } + } + + @Test fun `verifyActionState and verifyNoActionOutput chain`() { + class TestAction : WorkflowAction { + override fun Updater.apply() { + nextState = "new state" + } + } + + val workflow = Workflow.stateful>( + initialState = { "initial" }, + render = { _, _ -> actionSink.contraMap { it } } + ) + val testResult = workflow.renderTester(Unit) + .render { sink -> + sink.send(TestAction()) + } + + testResult + .verifyActionState { assertEquals("new state", it) } + .verifyNoActionOutput() + } + + @Test fun `verifyActionState allows no action`() { + val workflow = Workflow.stateless> { + actionSink.contraMap { it } + } + val testResult = workflow.renderTester(Unit) + .render { + // Don't send to sink! + } + + testResult.verifyAction { assertEquals(noAction(), it) } + testResult.verifyActionState { newState -> + assertSame(Unit, newState) + } + } + + @Test fun `verifyActionState handles new state`() { + class TestAction : WorkflowAction { + override fun Updater.apply() { + nextState = "new state" + } + } + + val workflow = Workflow.stateful>( + initialState = { "initial" }, + render = { _, _ -> actionSink.contraMap { it } } + ) + val testResult = workflow.renderTester(Unit) + .render { sink -> + sink.send(TestAction()) + } + + testResult.verifyActionState { state -> + assertEquals("new state", state) + } + } + + @Test fun `verifyActionOutput allows no action`() { + val workflow = Workflow.stateless> { + actionSink.contraMap { it } + } + val testResult = workflow.renderTester(Unit) + .render { + // Don't send to sink! + } + + testResult.verifyAction { assertEquals(noAction(), it) } + val error = assertFailsWith { + testResult.verifyActionOutput {} + } + assertEquals("Expected action to set an output", error.message) + } + + @Test fun `verifyActionOutput handles output`() { + class TestAction : WorkflowAction { + override fun Updater.apply() { + setOutput("output") + } + } + + val workflow = Workflow.stateful>( + initialState = { "initial" }, + render = { _, _ -> actionSink.contraMap { it } } + ) + val testResult = workflow.renderTester(Unit) + .render { sink -> + sink.send(TestAction()) + } + + testResult.verifyActionOutput { output -> + assertEquals("output", output) + } + } + + @Test fun `verifyNoActionOutput allows no action`() { + val workflow = Workflow.stateless> { + actionSink.contraMap { it } + } + val testResult = workflow.renderTester(Unit) + .render { + // Don't send to sink! + } + + testResult.verifyAction { assertEquals(noAction(), it) } + testResult.verifyNoActionOutput() + } + + @Test fun `verifyNoActionOutput fails when output is set`() { + class TestAction : WorkflowAction { + override fun Updater.apply() { + setOutput("output") + } + } + + val workflow = Workflow.stateful>( + initialState = { "initial" }, + render = { _, _ -> actionSink.contraMap { it } } + ) + val testResult = workflow.renderTester(Unit) + .render { sink -> + sink.send(TestAction()) + } + + val error = assertFailsWith { + testResult.verifyNoActionOutput() + } + assertEquals("Expected no output, but action set output to: output", error.message) + } + @Test fun `render is executed multiple times`() { var renderCount = 0 val workflow = Workflow.stateless { renderCount++ } diff --git a/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingWorkflowInterceptor.kt b/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingWorkflowInterceptor.kt index 1480af8100..804cfcb71b 100644 --- a/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingWorkflowInterceptor.kt +++ b/workflow-tracing/src/main/java/com/squareup/workflow/diagnostic/tracing/TracingWorkflowInterceptor.kt @@ -216,7 +216,7 @@ class TracingWorkflowInterceptor internal constructor( return newState } - override fun onRender( + override fun onRender( props: P, state: S, context: RenderContext, @@ -548,7 +548,7 @@ class TracingWorkflowInterceptor internal constructor( workerIdCounter++.toLong() .shl(32) xor session.sessionId - private inner class TracingRenderContext( + private inner class TracingRenderContext( private val delegate: RenderContext, private val session: WorkflowSession ) : RenderContext by delegate, Sink> { @@ -572,13 +572,18 @@ class TracingWorkflowInterceptor internal constructor( } } - private inner class TracingAction( + private inner class TracingAction( private val delegate: WorkflowAction, private val session: WorkflowSession ) : WorkflowAction { override fun Updater.apply() { val oldState = nextState - val (newState, output) = delegate.applyTo(nextState) + var output: O? = null + val (newState, _) = delegate.applyTo(nextState) { + output = it + setOutput(it) + } + nextState = newState onWorkflowAction( workflowId = session.sessionId, action = delegate, @@ -586,8 +591,6 @@ class TracingWorkflowInterceptor internal constructor( newState = newState, output = output ) - nextState = newState - output?.let(::setOutput) } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt index b7137e244f..6bac55a31e 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowFragment.kt @@ -53,7 +53,7 @@ import com.squareup.workflow.ui.WorkflowRunner.Config * } * } */ -abstract class WorkflowFragment : Fragment() { +abstract class WorkflowFragment : Fragment() { private lateinit var _runner: WorkflowRunner /** diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt index 01bd5bcb61..d54a577da2 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunner.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.StateFlow * It is simplest to use [Activity.setContentWorkflow][setContentWorkflow] * or subclass [WorkflowFragment] rather than instantiate a [WorkflowRunner] directly. */ -interface WorkflowRunner { +interface WorkflowRunner { /** * A stream of the rendering values emitted by the running [Workflow]. @@ -62,7 +62,7 @@ interface WorkflowRunner { * rendered by the runtime. */ @OptIn(ExperimentalCoroutinesApi::class, ExperimentalWorkflowApi::class) - class Config( + class Config( val workflow: Workflow, val props: StateFlow, val dispatcher: CoroutineDispatcher, @@ -87,7 +87,7 @@ interface WorkflowRunner { */ @OptIn(ExperimentalWorkflowApi::class) @Suppress("FunctionName") - fun Config( + fun Config( workflow: Workflow, dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate, interceptors: List = emptyList() @@ -103,7 +103,7 @@ interface WorkflowRunner { * @param configure function defining the root workflow and its environment. Called only * once per [lifecycle][FragmentActivity.getLifecycle], and always called from the UI thread. */ - fun startWorkflow( + fun startWorkflow( activity: FragmentActivity, configure: () -> Config ): WorkflowRunner { @@ -126,7 +126,7 @@ interface WorkflowRunner { * @param configure function defining the root workflow and its environment. Called only * once per [lifecycle][Fragment.getLifecycle], and always called from the UI thread. */ - fun startWorkflow( + fun startWorkflow( fragment: Fragment, configure: () -> Config ): WorkflowRunner { @@ -156,7 +156,7 @@ interface WorkflowRunner { * values, so this is also a good place from which to call [FragmentActivity.finish]. Called * only while the activity is active, and always called from the UI thread. */ -fun FragmentActivity.setContentWorkflow( +fun FragmentActivity.setContentWorkflow( viewEnvironment: ViewEnvironment, configure: () -> Config, onResult: (OutputT) -> Unit @@ -191,7 +191,7 @@ fun FragmentActivity.setContentWorkflow( * values, so this is also a good place from which to call [FragmentActivity.finish]. Called * only while the activity is active, and always called from the UI thread. */ -fun FragmentActivity.setContentWorkflow( +fun FragmentActivity.setContentWorkflow( registry: ViewRegistry, configure: () -> Config, onResult: (OutputT) -> Unit diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt index fb2935412e..284aa864ca 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/WorkflowRunnerViewModel.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.map import org.jetbrains.annotations.TestOnly @OptIn(ExperimentalCoroutinesApi::class) -internal class WorkflowRunnerViewModel( +internal class WorkflowRunnerViewModel( private val scope: CoroutineScope, private val result: Deferred, private val renderingsAndSnapshots: StateFlow> @@ -64,7 +64,7 @@ internal class WorkflowRunnerViewModel( } } - internal class Factory( + internal class Factory( private val snapshotSaver: SnapshotSaver, private val configure: () -> Config ) : ViewModelProvider.Factory {