From d824cc9e7ca6e86f194ecaf34d21ad4f54eae2c5 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Mon, 19 Aug 2019 10:19:39 -0700 Subject: [PATCH] makeSink > makeActionSink and makeEventSink, updates ToDo sample. --- .../squareup/sample/dungeon/PlayerWorkflow.kt | 2 +- .../helloworkflowfragment/HelloWorkflow.kt | 2 +- .../sample/helloworkflow/HelloWorkflow.kt | 2 +- .../sample/authworkflow/AuthWorkflow.kt | 2 +- .../sample/gameworkflow/RunGameWorkflow.kt | 2 +- .../sample/gameworkflow/TakeTurnsWorkflow.kt | 2 +- .../mainactivity/TodoEditorLayoutRunner.kt | 28 ++--- .../sample/todo/TodoEditorWorkflow.kt | 106 ++++++++++++------ .../squareup/sample/todo/TodoListsWorkflow.kt | 10 +- .../com/squareup/workflow/RenderContext.kt | 51 ++++++--- .../main/java/com/squareup/workflow/Sink.kt | 2 +- .../com/squareup/workflow/WorkflowAction.kt | 5 + .../workflow/internal/RealRenderContext.kt | 2 +- .../squareup/workflow/testing/RenderTester.kt | 4 +- 14 files changed, 136 insertions(+), 84 deletions(-) diff --git a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt b/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt index 944bf688e..31e6cd5f0 100644 --- a/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt +++ b/kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/PlayerWorkflow.kt @@ -67,7 +67,7 @@ class PlayerWorkflow( state: Movement, context: RenderContext ): Rendering { - val sink = context.makeSink() + val sink = context.makeActionSink() return Rendering( actorRendering = ActorRendering(avatar = avatar, movement = state), diff --git a/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt b/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt index 46ab3096b..dd5ab9612 100644 --- a/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt +++ b/kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflow.kt @@ -53,7 +53,7 @@ object HelloWorkflow : StatefulWorkflow() { state: State, context: RenderContext ): Rendering { - val sink = context.makeSink>() + val sink = context.makeActionSink>() return Rendering( message = state.name, onClick = { sink.send(enterState(state.theOtherState())) } diff --git a/kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt b/kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt index add8186ad..3af948e03 100644 --- a/kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt +++ b/kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflow.kt @@ -53,7 +53,7 @@ object HelloWorkflow : StatefulWorkflow() { state: State, context: RenderContext ): Rendering { - val sink = context.makeSink>() + val sink = context.makeActionSink>() return Rendering( message = state.name, onClick = { sink.send(enterState(state.theOtherState())) } diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt index 9cb9b3f9f..0cc5081bc 100644 --- a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt +++ b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/authworkflow/AuthWorkflow.kt @@ -155,7 +155,7 @@ class RealAuthWorkflow(private val authService: AuthService) : AuthWorkflow, context: RenderContext ): BackStackScreen<*> { - val sink = context.makeSink() + val sink = context.makeActionSink() return when (state) { is LoginPrompt -> { diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt index e1f59d40b..0311694c5 100644 --- a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt +++ b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt @@ -183,7 +183,7 @@ class RealRunGameWorkflow( state: RunGameState, context: RenderContext ): RunGameScreen { - val sink = context.makeSink() + val sink = context.makeActionSink() return when (state) { is NewGame -> { diff --git a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt index 08700edb2..a0945a62f 100644 --- a/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt +++ b/kotlin/samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/TakeTurnsWorkflow.kt @@ -86,7 +86,7 @@ class RealTakeTurnsWorkflow : TakeTurnsWorkflow, state: Turn, context: RenderContext ): GamePlayScreen { - val sink = context.makeSink() + val sink = context.makeActionSink() return GamePlayScreen( playerInfo = input.playerInfo, diff --git a/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt b/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt index bd9d5d9c3..a142fa67f 100644 --- a/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt +++ b/kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/TodoEditorLayoutRunner.kt @@ -15,16 +15,10 @@ */ package com.squareup.sample.mainactivity -import androidx.appcompat.widget.Toolbar import android.view.View import android.widget.EditText +import androidx.appcompat.widget.Toolbar import com.squareup.sample.todo.R -import com.squareup.sample.todo.TodoEvent -import com.squareup.sample.todo.TodoEvent.DeleteClicked -import com.squareup.sample.todo.TodoEvent.DoneClicked -import com.squareup.sample.todo.TodoEvent.GoBackClicked -import com.squareup.sample.todo.TodoEvent.TextChanged -import com.squareup.sample.todo.TodoEvent.TitleChanged import com.squareup.sample.todo.TodoRendering import com.squareup.workflow.ui.ExperimentalWorkflowUi import com.squareup.workflow.ui.LayoutRunner @@ -57,27 +51,19 @@ internal class TodoEditorLayoutRunner(private val view: View) : LayoutRunner - onEvent(DoneClicked(index)) + rendering.onDoneClicked(index) } itemContainer.onTextChangedListener = { index, text -> - onEvent(TextChanged(index, text)) + rendering.onTextChanged(index, text) } itemContainer.onDeleteClickedListener = { index -> - onEvent(DeleteClicked(index)) + rendering.onDeleteClicked(index) } } diff --git a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt b/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt index d2508c2da..44076b22a 100644 --- a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt +++ b/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoEditorWorkflow.kt @@ -15,16 +15,18 @@ */ package com.squareup.sample.todo +import com.squareup.sample.todo.TodoAction.GoBackClicked +import com.squareup.sample.todo.TodoAction.ListAction.DeleteClicked +import com.squareup.sample.todo.TodoAction.ListAction.DoneClicked +import com.squareup.sample.todo.TodoAction.ListAction.TextChanged +import com.squareup.sample.todo.TodoAction.ListAction.TitleChanged import com.squareup.sample.todo.TodoEditorOutput.Done import com.squareup.sample.todo.TodoEditorOutput.ListUpdated -import com.squareup.sample.todo.TodoEvent.DeleteClicked -import com.squareup.sample.todo.TodoEvent.DoneClicked -import com.squareup.sample.todo.TodoEvent.GoBackClicked -import com.squareup.sample.todo.TodoEvent.TextChanged -import com.squareup.sample.todo.TodoEvent.TitleChanged import com.squareup.workflow.RenderContext +import com.squareup.workflow.Sink import com.squareup.workflow.StatelessWorkflow -import com.squareup.workflow.WorkflowAction.Companion.emitOutput +import com.squareup.workflow.WorkflowAction +import com.squareup.workflow.WorkflowAction.Mutator data class TodoList( val title: String, @@ -36,9 +38,52 @@ data class TodoRow( val done: Boolean = false ) -data class TodoRendering( +sealed class TodoAction : WorkflowAction { + object GoBackClicked : TodoAction() + + sealed class ListAction : TodoAction() { + abstract val list: TodoList + + class TitleChanged( + override val list: TodoList, + val newTitle: String + ) : ListAction() + + class DoneClicked( + override val list: TodoList, + val index: Int + ) : ListAction() + + class TextChanged( + override val list: TodoList, + val index: Int, + val newText: String + ) : ListAction() + + class DeleteClicked( + override val list: TodoList, + val index: Int + ) : ListAction() + } + + override fun Mutator.apply(): TodoEditorOutput { + return when (this@TodoAction) { + is GoBackClicked -> Done + is TitleChanged -> ListUpdated(list.copy(title = newTitle)) + is DoneClicked -> ListUpdated(list.updateRow(index) { copy(done = !done) }) + is TextChanged -> ListUpdated(list.updateRow(index) { copy(text = newText) }) + is DeleteClicked -> ListUpdated(list.removeRow(index)) + } + } +} + +class TodoRendering( val list: TodoList, - val onEvent: (TodoEvent) -> Unit + val onTitleChanged: (title: String) -> Unit, + val onDoneClicked: (index: Int) -> Unit, + val onTextChanged: (index: Int, text: String) -> Unit, + val onDeleteClicked: (index: Int) -> Unit, + val onGoBackClicked: () -> Unit ) sealed class TodoEditorOutput { @@ -46,40 +91,31 @@ sealed class TodoEditorOutput { object Done : TodoEditorOutput() } -sealed class TodoEvent { - data class TitleChanged(val title: String) : TodoEvent() - data class DoneClicked(val index: Int) : TodoEvent() - data class TextChanged( - val index: Int, - val text: String - ) : TodoEvent() - - data class DeleteClicked(val index: Int) : TodoEvent() - object GoBackClicked : TodoEvent() -} - class TodoEditorWorkflow : StatelessWorkflow() { override fun render( input: TodoList, context: RenderContext ): TodoRendering { + val rawSink = context.makeActionSink() + + // Make event handling idempotent until https://github.com/square/workflow/issues/541 is fixed. + var eventFired = false + val sink = object : Sink { + override fun send(value: TodoAction) { + if (eventFired) return + eventFired = true + rawSink.send(value) + } + } + return TodoRendering( - list = input.copy(rows = input.rows + TodoRow("")), - onEvent = context.onEvent { - println("got event: $it") - when (it) { - is GoBackClicked -> emitOutput(Done) - is TitleChanged -> emitOutput(ListUpdated(input.copy(title = it.title))) - is DoneClicked -> emitOutput(ListUpdated(input.updateRow(it.index) { - copy(done = !done) - })) - is TextChanged -> emitOutput(ListUpdated(input.updateRow(it.index) { - copy(text = it.text) - })) - is DeleteClicked -> emitOutput(ListUpdated(input.removeRow(it.index))) - } - } + input.copy(rows = input.rows + TodoRow("")), + onTitleChanged = { sink.send(TitleChanged(input, it)) }, + onDoneClicked = { sink.send(DoneClicked(input, it)) }, + onTextChanged = { index, newText -> sink.send(TextChanged(input, index, newText)) }, + onDeleteClicked = { sink.send(DeleteClicked(input, it)) }, + onGoBackClicked = { sink.send(GoBackClicked) } ) } } diff --git a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt b/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt index 6ad3077ed..303821d09 100644 --- a/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt +++ b/kotlin/samples/todo-android/common/src/main/java/com/squareup/sample/todo/TodoListsWorkflow.kt @@ -16,8 +16,9 @@ package com.squareup.sample.todo import com.squareup.workflow.RenderContext +import com.squareup.workflow.Sink import com.squareup.workflow.StatelessWorkflow -import com.squareup.workflow.WorkflowAction.Companion.emitOutput +import com.squareup.workflow.makeEventSink data class TodoListsRendering( val lists: List, @@ -29,8 +30,9 @@ class TodoListsWorkflow : StatelessWorkflow, Int, TodoListsRender input: List, context: RenderContext ): TodoListsRendering { - return TodoListsRendering(input, context.onEvent { row -> - emitOutput(row) - }) + // A sink that emits the given index as the result of this workflow. + val sink: Sink = context.makeEventSink { index: Int -> index } + + return TodoListsRendering(lists = input, onRowClicked = sink::send) } } diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt index 0945e4843..25ea05f33 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/RenderContext.kt @@ -21,6 +21,7 @@ import com.squareup.workflow.Worker.OutputOrFinished import com.squareup.workflow.Worker.OutputOrFinished.Finished import com.squareup.workflow.Worker.OutputOrFinished.Output import com.squareup.workflow.WorkflowAction.Companion.noAction +import com.squareup.workflow.WorkflowAction.Mutator /** * Facilities for a [Workflow] to interact with other [Workflow]s and the outside world from inside @@ -28,7 +29,7 @@ import com.squareup.workflow.WorkflowAction.Companion.noAction * * ## Handling Events * - * See [onEvent]. + * See [makeActionSink]. * * ## Performing Asynchronous Work * @@ -40,12 +41,20 @@ import com.squareup.workflow.WorkflowAction.Companion.noAction */ interface RenderContext { - @Deprecated("Use makeSink.") + @Deprecated("Use makeActionSink.") fun onEvent( handler: (EventT) -> WorkflowAction ): (EventT) -> Unit - fun > makeSink(): Sink + /** + * Creates a sink that will a single [WorkflowAction] of the given type. + * Invokes that action by calling [WorkflowAction.apply] to update the current + * state, and optionally emit the returned output value if it is non-null. + * + * Note that only a single action can be processed the sink (or sinks) created + * during a `render` call. Redundant calls to [Sink.send] will + */ + fun > makeActionSink(): Sink /** * Ensures [child] is running as a child of this workflow, and returns the result of its @@ -96,10 +105,10 @@ fun // Intellij refuses to format this parameter list correctly because of the weird line break, // and detekt will complain about it. // @formatter:off - child: Workflow, - key: String = "", - handler: (ChildOutputT) -> WorkflowAction - ): ChildRenderingT = renderChild(child, Unit, key, handler) + child: Workflow, + key: String = "", + handler: (ChildOutputT) -> WorkflowAction +): ChildRenderingT = renderChild(child, Unit, key, handler) // @formatter:on /** @@ -111,10 +120,10 @@ fun // Intellij refuses to format this parameter list correctly because of the weird line break, // and detekt will complain about it. // @formatter:off - child: Workflow, - input: InputT, - key: String = "" - ): ChildRenderingT = renderChild(child, input, key) { noAction() } + child: Workflow, + input: InputT, + key: String = "" +): ChildRenderingT = renderChild(child, input, key) { noAction() } // @formatter:on /** @@ -126,9 +135,9 @@ fun // Intellij refuses to format this parameter list correctly because of the weird line break, // and detekt will complain about it. // @formatter:off - child: Workflow, - key: String = "" - ): ChildRenderingT = renderChild(child, Unit, key) { noAction() } + child: Workflow, + key: String = "" +): ChildRenderingT = renderChild(child, Unit, key) { noAction() } // @formatter:on /** @@ -166,3 +175,17 @@ fun RenderContext.runningWorker( throw AssertionError("Worker emitted $it") } } + +/** + * Alternative to [RenderContext.makeActionSink] that allows externally defined + * event types to be mapped to anonymous [WorkflowAction]s. + */ +fun RenderContext.makeEventSink( + block: Mutator.(EventT) -> OutputT +): Sink { + val actionSink = makeActionSink>() + + return actionSink.contraMap { event -> + WorkflowAction { block.invoke(this, event) } + } +} diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Sink.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/Sink.kt index c95952e5b..7903a278a 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/Sink.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/Sink.kt @@ -2,7 +2,7 @@ package com.squareup.workflow /** * An object that receives values (commonly events or [WorkflowAction]). - * Use [RenderContext.makeSink] to create instances. + * Use [RenderContext.makeActionSink] to create instances. */ interface Sink { fun send(value: T) diff --git a/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt b/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt index 867fcc289..109b9098c 100644 --- a/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt +++ b/kotlin/workflow-core/src/main/java/com/squareup/workflow/WorkflowAction.kt @@ -24,6 +24,11 @@ import com.squareup.workflow.WorkflowAction.Mutator interface WorkflowAction { class Mutator(var state: S) + /** + * Called with the current state of a workflow, allowing the state to be updated. + * + * @return an output value for the workflow to emit, or null to emit nothing. + */ fun Mutator.apply(): OutputT? companion object { diff --git a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt b/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt index 6f5a7a7ad..d53a01620 100644 --- a/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt +++ b/kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt @@ -70,7 +70,7 @@ class RealRenderContext( } } - override fun > makeSink(): Sink { + override fun > makeActionSink(): Sink { checkNotFrozen() return object : Sink { diff --git a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt b/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt index 9cf7176e3..bb7aa2d6a 100644 --- a/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt +++ b/kotlin/workflow-testing/src/main/java/com/squareup/workflow/testing/RenderTester.kt @@ -289,7 +289,7 @@ private class TestOnlyRenderContext : RenderContext, Renderer< handler: (EventT) -> WorkflowAction ): EventHandler = realContext.onEvent(handler) - override fun > makeSink(): Sink = realContext.makeSink() + override fun > makeActionSink(): Sink = realContext.makeActionSink() override fun renderChild( child: Workflow, @@ -329,7 +329,7 @@ private object NoopRenderContext : RenderContext { throw UnsupportedOperationException() } - override fun > makeSink(): Sink { + override fun > makeActionSink(): Sink { throw UnsupportedOperationException() }