Skip to content

Commit

Permalink
safeAction, safeEventHandler
Browse files Browse the repository at this point in the history
People are confused by the fact that a `WorkflowAction` can't assume that a sealed class / interface `StateT` is the same subtype that it was at render time when the action fires. And those that do understand it resent this boilerplate:

```kotlin
action {
  (state as? SpecificState)?.let { currentState ->
    // whatever
  }
}
```

So we introduce `StatefulWorkflow.safeAction` and `StatefulWorkflow.RenderContext.safeEventHandler` as conveniences to do that cast for you.
  • Loading branch information
rjrjr committed Sep 24, 2024
1 parent 24f45a7 commit 6b001b0
Show file tree
Hide file tree
Showing 3 changed files with 390 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@file:OptIn(WorkflowUiExperimentalApi::class)
@file:OptIn(WorkflowUiExperimentalApi::class, WorkflowUiExperimentalApi::class)

package com.squareup.sample.gameworkflow

Expand All @@ -18,7 +18,6 @@ import com.squareup.sample.gameworkflow.SyncState.SAVING
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.action
import com.squareup.workflow1.runningWorker
import com.squareup.workflow1.rx2.asWorker
import com.squareup.workflow1.ui.Screen
Expand Down Expand Up @@ -60,6 +59,7 @@ typealias RunGameWorkflow =
* confirm quit screen, and offers a chance to play again. Delegates to [TakeTurnsWorkflow]
* for the actual playing of the game.
*/
@OptIn(WorkflowUiExperimentalApi::class)
class RealRunGameWorkflow(
private val takeTurnsWorkflow: TakeTurnsWorkflow,
private val gameLog: GameLog
Expand Down Expand Up @@ -88,8 +88,12 @@ class RealRunGameWorkflow(
namePrompt = NewGameScreen(
renderState.defaultXName,
renderState.defaultOName,
onCancel = context.eventHandler { setOutput(CanceledStart) },
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
onCancel = context.safeEventHandler<NewGame> {
setOutput(CanceledStart)
},
onStartGame = context.safeEventHandler<NewGame, String, String> { _, x, o ->
state = Playing(PlayerInfo(x, o))
}
)
)
}
Expand Down Expand Up @@ -119,15 +123,11 @@ class RealRunGameWorkflow(
message = "Do you really want to concede the game?",
positive = "I Quit",
negative = "No",
confirmQuit = context.eventHandler {
(state as? MaybeQuitting)?.let { oldState ->
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
}
confirmQuit = context.safeEventHandler<MaybeQuitting> { oldState ->
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
},
continuePlaying = context.eventHandler {
(state as? MaybeQuitting)?.let { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
continuePlaying = context.safeEventHandler<MaybeQuitting> { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
)
)
Expand All @@ -142,15 +142,11 @@ class RealRunGameWorkflow(
message = "Really?",
positive = "Yes!!",
negative = "Sigh, no",
confirmQuit = context.eventHandler {
(state as? MaybeQuittingForSure)?.let { oldState ->
state = GameOver(oldState.playerInfo, oldState.completedGame)
}
confirmQuit = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
state = GameOver(oldState.playerInfo, oldState.completedGame)
},
continuePlaying = context.eventHandler {
(state as? MaybeQuittingForSure)?.let { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
continuePlaying = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
}
)
)
Expand All @@ -169,43 +165,37 @@ class RealRunGameWorkflow(
renderState,
onTrySaveAgain = context.trySaveAgain(),
onPlayAgain = context.playAgain(),
onExit = context.eventHandler { setOutput(FinishedPlaying) }
onExit = context.safeEventHandler<GameOver> { setOutput(FinishedPlaying) }
)
)
}
}

private fun stopPlaying(game: CompletedGame) = action {
val oldState = state as Playing
private fun stopPlaying(game: CompletedGame) = safeAction<Playing>("stopPlaying") { oldState ->
state = when (game.ending) {
Quitted -> MaybeQuitting(oldState.playerInfo, game)
else -> GameOver(oldState.playerInfo, game)
}
}

private fun handleLogGame(result: GameLog.LogResult) = action {
val oldState = state as GameOver
private fun handleLogGame(result: GameLog.LogResult) = safeAction<GameOver> { oldState ->
state = when (result) {
TRY_LATER -> oldState.copy(syncState = SAVE_FAILED)
LOGGED -> oldState.copy(syncState = SAVED)
}
}

private fun RenderContext.playAgain() = eventHandler {
(state as? GameOver)?.let { oldState ->
val (x, o) = oldState.playerInfo
state = NewGame(x, o)
}
private fun RenderContext.playAgain() = safeEventHandler<GameOver> { oldState ->
val (x, o) = oldState.playerInfo
state = NewGame(x, o)
}

private fun RenderContext.trySaveAgain() = eventHandler {
(state as? GameOver)?.let { oldState ->
check(oldState.syncState == SAVE_FAILED) {
"Should only fire trySaveAgain in syncState $SAVE_FAILED, " +
"was ${oldState.syncState}"
}
state = oldState.copy(syncState = SAVING)
private fun RenderContext.trySaveAgain() = safeEventHandler<GameOver> { oldState ->
check(oldState.syncState == SAVE_FAILED) {
"Should only fire trySaveAgain in syncState $SAVE_FAILED, " +
"was ${oldState.syncState}"
}
state = oldState.copy(syncState = SAVING)
}

override fun snapshotState(state: RunGameState): Snapshot = state.toSnapshot()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
public fun <E1, E2, E3, E4, E5, E6, E7, E8, E9> eventHandler(
name: () -> String = { "eventHandler" },
update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
): (E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit {
return { e1, e2, e3, e4, e5, e6, e7, e8, e9 ->
actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) })
Expand All @@ -274,7 +274,7 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
public fun <E1, E2, E3, E4, E5, E6, E7, E8, E9, E10> eventHandler(
name: () -> String = { "eventHandler" },
update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit
): (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit {
return { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 ->
actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) })
Expand All @@ -287,30 +287,30 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
*/
public fun <PropsT, StateT, OutputT, ChildOutputT, ChildRenderingT>
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
child: Workflow<Unit, ChildOutputT, ChildRenderingT>,
key: String = "",
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
): ChildRenderingT = renderChild(child, Unit, key, handler)
child: Workflow<Unit, ChildOutputT, ChildRenderingT>,
key: String = "",
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
): ChildRenderingT = renderChild(child, Unit, key, handler)

/**
* Convenience alias of [BaseRenderContext.renderChild] for workflows that don't emit output.
*/
public fun <PropsT, ChildPropsT, StateT, OutputT, ChildRenderingT>
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
child: Workflow<ChildPropsT, Nothing, ChildRenderingT>,
props: ChildPropsT,
key: String = ""
): ChildRenderingT = renderChild(child, props, key) { noAction() }
child: Workflow<ChildPropsT, Nothing, ChildRenderingT>,
props: ChildPropsT,
key: String = ""
): ChildRenderingT = renderChild(child, props, key) { noAction() }

/**
* Convenience alias of [BaseRenderContext.renderChild] for children that don't take props or emit
* output.
*/
public fun <PropsT, StateT, OutputT, ChildRenderingT>
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
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() }

/**
* Ensures a [LifecycleWorker] is running. Since [worker] can't emit anything,
Expand All @@ -323,9 +323,9 @@ public fun <PropsT, StateT, OutputT, ChildRenderingT>
*/
public inline fun <reified W : LifecycleWorker, PropsT, StateT, OutputT>
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
worker: W,
key: String = ""
) {
worker: W,
key: String = ""
) {
runningWorker(worker, key) {
// The compiler thinks this code is unreachable, and it is correct. But we have to pass a lambda
// here so we might as well check at runtime as well.
Expand All @@ -348,9 +348,9 @@ public inline fun <reified W : LifecycleWorker, PropsT, StateT, OutputT>
)
public inline fun <reified W : Worker<Nothing>, PropsT, StateT, OutputT>
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
worker: W,
key: String = ""
) {
worker: W,
key: String = ""
) {
runningWorker(worker, key) {
// The compiler thinks this code is unreachable, and it is correct. But we have to pass a lambda
// here so we might as well check at runtime as well.
Expand Down Expand Up @@ -378,10 +378,10 @@ public inline fun <reified W : Worker<Nothing>, PropsT, StateT, OutputT>
*/
public inline fun <T, reified W : Worker<T>, PropsT, StateT, OutputT>
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
worker: W,
key: String = "",
noinline handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
) {
worker: W,
key: String = "",
noinline handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
) {
runningWorker(worker, typeOf<W>(), key, handler)
}

Expand All @@ -396,11 +396,11 @@ public inline fun <T, reified W : Worker<T>, PropsT, StateT, OutputT>
@PublishedApi
internal fun <T, PropsT, StateT, OutputT>
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
worker: Worker<T>,
workerType: KType,
key: String = "",
handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
) {
worker: Worker<T>,
workerType: KType,
key: String = "",
handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
) {
val workerWorkflow = WorkerWorkflow<T>(workerType, key)
renderChild(workerWorkflow, props = worker, key = key, handler = handler)
}
Loading

0 comments on commit 6b001b0

Please sign in to comment.