Skip to content

Commit

Permalink
makeSink > makeActionSink and makeEventSink, updates ToDo sample.
Browse files Browse the repository at this point in the history
  • Loading branch information
rjrjr committed Aug 19, 2019
1 parent 847e41f commit d824cc9
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 84 deletions.
Expand Up @@ -67,7 +67,7 @@ class PlayerWorkflow(
state: Movement,
context: RenderContext<Movement, Nothing>
): Rendering {
val sink = context.makeSink<Action>()
val sink = context.makeActionSink<Action>()

return Rendering(
actorRendering = ActorRendering(avatar = avatar, movement = state),
Expand Down
Expand Up @@ -53,7 +53,7 @@ object HelloWorkflow : StatefulWorkflow<Unit, State, Nothing, Rendering>() {
state: State,
context: RenderContext<State, Nothing>
): Rendering {
val sink = context.makeSink<WorkflowAction<State, Nothing>>()
val sink = context.makeActionSink<WorkflowAction<State, Nothing>>()
return Rendering(
message = state.name,
onClick = { sink.send(enterState(state.theOtherState())) }
Expand Down
Expand Up @@ -53,7 +53,7 @@ object HelloWorkflow : StatefulWorkflow<Unit, State, Nothing, Rendering>() {
state: State,
context: RenderContext<State, Nothing>
): Rendering {
val sink = context.makeSink<WorkflowAction<State, Nothing>>()
val sink = context.makeActionSink<WorkflowAction<State, Nothing>>()
return Rendering(
message = state.name,
onClick = { sink.send(enterState(state.theOtherState())) }
Expand Down
Expand Up @@ -155,7 +155,7 @@ class RealAuthWorkflow(private val authService: AuthService) : AuthWorkflow,
context: RenderContext<AuthState, AuthResult>
): BackStackScreen<*> {

val sink = context.makeSink<Action>()
val sink = context.makeActionSink<Action>()

return when (state) {
is LoginPrompt -> {
Expand Down
Expand Up @@ -183,7 +183,7 @@ class RealRunGameWorkflow(
state: RunGameState,
context: RenderContext<RunGameState, RunGameResult>
): RunGameScreen {
val sink = context.makeSink<Action>()
val sink = context.makeActionSink<Action>()

return when (state) {
is NewGame -> {
Expand Down
Expand Up @@ -86,7 +86,7 @@ class RealTakeTurnsWorkflow : TakeTurnsWorkflow,
state: Turn,
context: RenderContext<Turn, CompletedGame>
): GamePlayScreen {
val sink = context.makeSink<Action>()
val sink = context.makeActionSink<Action>()

return GamePlayScreen(
playerInfo = input.playerInfo,
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -57,27 +51,19 @@ internal class TodoEditorLayoutRunner(private val view: View) : LayoutRunner<Tod
titleText.text.replace(0, titleText.text.length, rendering.list.title)
itemContainer.setRows(rendering.list.rows.map { Pair(it.done, it.text) })

// Make event handling idempotent.
var eventFired = false
fun onEvent(event: TodoEvent) {
if (eventFired) return
eventFired = true
rendering.onEvent(event)
}

toolbar.setNavigationOnClickListener { onEvent(GoBackClicked) }
view.setBackHandler { onEvent(GoBackClicked) }
toolbar.setNavigationOnClickListener { rendering.onGoBackClicked() }
view.setBackHandler { rendering.onGoBackClicked() }

titleText.setTextChangedListener { onEvent(TitleChanged(it)) }
titleText.setTextChangedListener { rendering.onTitleChanged(it) }

itemContainer.onDoneClickedListener = { index ->
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)
}
}

Expand Down
Expand Up @@ -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,
Expand All @@ -36,50 +38,84 @@ data class TodoRow(
val done: Boolean = false
)

data class TodoRendering(
sealed class TodoAction : WorkflowAction<Nothing, TodoEditorOutput> {
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<Nothing>.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 {
data class ListUpdated(val newList: TodoList) : 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<TodoList, TodoEditorOutput, TodoRendering>() {

override fun render(
input: TodoList,
context: RenderContext<Nothing, TodoEditorOutput>
): TodoRendering {
val rawSink = context.makeActionSink<TodoAction>()

// Make event handling idempotent until https://github.com/square/workflow/issues/541 is fixed.
var eventFired = false
val sink = object : Sink<TodoAction> {
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) }
)
}
}
Expand Down
Expand Up @@ -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<TodoList>,
Expand All @@ -29,8 +30,9 @@ class TodoListsWorkflow : StatelessWorkflow<List<TodoList>, Int, TodoListsRender
input: List<TodoList>,
context: RenderContext<Nothing, Int>
): 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<Int> = context.makeEventSink { index: Int -> index }

return TodoListsRendering(lists = input, onRowClicked = sink::send)
}
}
Expand Up @@ -21,14 +21,15 @@ 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
* a `render` function.
*
* ## Handling Events
*
* See [onEvent].
* See [makeActionSink].
*
* ## Performing Asynchronous Work
*
Expand All @@ -40,12 +41,20 @@ import com.squareup.workflow.WorkflowAction.Companion.noAction
*/
interface RenderContext<StateT, in OutputT : Any> {

@Deprecated("Use makeSink.")
@Deprecated("Use makeActionSink.")
fun <EventT : Any> onEvent(
handler: (EventT) -> WorkflowAction<StateT, OutputT>
): (EventT) -> Unit

fun <A : WorkflowAction<StateT, OutputT>> makeSink(): Sink<A>
/**
* 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 <A : WorkflowAction<StateT, OutputT>> makeActionSink(): Sink<A>

/**
* Ensures [child] is running as a child of this workflow, and returns the result of its
Expand Down Expand Up @@ -96,10 +105,10 @@ fun <StateT, OutputT : Any, ChildOutputT : Any, ChildRenderingT>
// Intellij refuses to format this parameter list correctly because of the weird line break,
// and detekt will complain about it.
// @formatter:off
child: Workflow<Unit, ChildOutputT, ChildRenderingT>,
key: String = "",
handler: (ChildOutputT) -> WorkflowAction<StateT, OutputT>
): ChildRenderingT = renderChild(child, Unit, key, handler)
child: Workflow<Unit, ChildOutputT, ChildRenderingT>,
key: String = "",
handler: (ChildOutputT) -> WorkflowAction<StateT, OutputT>
): ChildRenderingT = renderChild(child, Unit, key, handler)
// @formatter:on

/**
Expand All @@ -111,10 +120,10 @@ fun <InputT, StateT, OutputT : Any, ChildRenderingT>
// Intellij refuses to format this parameter list correctly because of the weird line break,
// and detekt will complain about it.
// @formatter:off
child: Workflow<InputT, Nothing, ChildRenderingT>,
input: InputT,
key: String = ""
): ChildRenderingT = renderChild(child, input, key) { noAction() }
child: Workflow<InputT, Nothing, ChildRenderingT>,
input: InputT,
key: String = ""
): ChildRenderingT = renderChild(child, input, key) { noAction() }
// @formatter:on

/**
Expand All @@ -126,9 +135,9 @@ fun <StateT, OutputT : Any, ChildRenderingT>
// Intellij refuses to format this parameter list correctly because of the weird line break,
// and detekt will complain about it.
// @formatter:off
child: Workflow<Unit, Nothing, ChildRenderingT>,
key: String = ""
): ChildRenderingT = renderChild(child, Unit, key) { noAction() }
child: Workflow<Unit, Nothing, ChildRenderingT>,
key: String = ""
): ChildRenderingT = renderChild(child, Unit, key) { noAction() }
// @formatter:on

/**
Expand Down Expand Up @@ -166,3 +175,17 @@ fun <StateT, OutputT : Any> RenderContext<StateT, OutputT>.runningWorker(
throw AssertionError("Worker<Nothing> emitted $it")
}
}

/**
* Alternative to [RenderContext.makeActionSink] that allows externally defined
* event types to be mapped to anonymous [WorkflowAction]s.
*/
fun <EventT, StateT, OutputT : Any> RenderContext<StateT, OutputT>.makeEventSink(
block: Mutator<StateT>.(EventT) -> OutputT
): Sink<EventT> {
val actionSink = makeActionSink<WorkflowAction<StateT, OutputT>>()

return actionSink.contraMap { event ->
WorkflowAction { block.invoke(this, event) }
}
}
Expand Up @@ -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<T> {
fun send(value: T)
Expand Down
Expand Up @@ -24,6 +24,11 @@ import com.squareup.workflow.WorkflowAction.Mutator
interface WorkflowAction<StateT, out OutputT : Any> {
class Mutator<S>(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<StateT>.apply(): OutputT?

companion object {
Expand Down
Expand Up @@ -70,7 +70,7 @@ class RealRenderContext<StateT, OutputT : Any>(
}
}

override fun <A : WorkflowAction<StateT, OutputT>> makeSink(): Sink<A> {
override fun <A : WorkflowAction<StateT, OutputT>> makeActionSink(): Sink<A> {
checkNotFrozen()

return object : Sink<A> {
Expand Down

0 comments on commit d824cc9

Please sign in to comment.