Skip to content

Commit 6b001b0

Browse files
committed
safeAction, safeEventHandler
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.
1 parent 24f45a7 commit 6b001b0

File tree

3 files changed

+390
-72
lines changed

3 files changed

+390
-72
lines changed

samples/tictactoe/common/src/main/java/com/squareup/sample/gameworkflow/RunGameWorkflow.kt

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@file:OptIn(WorkflowUiExperimentalApi::class)
1+
@file:OptIn(WorkflowUiExperimentalApi::class, WorkflowUiExperimentalApi::class)
22

33
package com.squareup.sample.gameworkflow
44

@@ -18,7 +18,6 @@ import com.squareup.sample.gameworkflow.SyncState.SAVING
1818
import com.squareup.workflow1.Snapshot
1919
import com.squareup.workflow1.StatefulWorkflow
2020
import com.squareup.workflow1.Workflow
21-
import com.squareup.workflow1.action
2221
import com.squareup.workflow1.runningWorker
2322
import com.squareup.workflow1.rx2.asWorker
2423
import com.squareup.workflow1.ui.Screen
@@ -60,6 +59,7 @@ typealias RunGameWorkflow =
6059
* confirm quit screen, and offers a chance to play again. Delegates to [TakeTurnsWorkflow]
6160
* for the actual playing of the game.
6261
*/
62+
@OptIn(WorkflowUiExperimentalApi::class)
6363
class RealRunGameWorkflow(
6464
private val takeTurnsWorkflow: TakeTurnsWorkflow,
6565
private val gameLog: GameLog
@@ -88,8 +88,12 @@ class RealRunGameWorkflow(
8888
namePrompt = NewGameScreen(
8989
renderState.defaultXName,
9090
renderState.defaultOName,
91-
onCancel = context.eventHandler { setOutput(CanceledStart) },
92-
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
91+
onCancel = context.safeEventHandler<NewGame> {
92+
setOutput(CanceledStart)
93+
},
94+
onStartGame = context.safeEventHandler<NewGame, String, String> { _, x, o ->
95+
state = Playing(PlayerInfo(x, o))
96+
}
9397
)
9498
)
9599
}
@@ -119,15 +123,11 @@ class RealRunGameWorkflow(
119123
message = "Do you really want to concede the game?",
120124
positive = "I Quit",
121125
negative = "No",
122-
confirmQuit = context.eventHandler {
123-
(state as? MaybeQuitting)?.let { oldState ->
124-
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
125-
}
126+
confirmQuit = context.safeEventHandler<MaybeQuitting> { oldState ->
127+
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
126128
},
127-
continuePlaying = context.eventHandler {
128-
(state as? MaybeQuitting)?.let { oldState ->
129-
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
130-
}
129+
continuePlaying = context.safeEventHandler<MaybeQuitting> { oldState ->
130+
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
131131
}
132132
)
133133
)
@@ -142,15 +142,11 @@ class RealRunGameWorkflow(
142142
message = "Really?",
143143
positive = "Yes!!",
144144
negative = "Sigh, no",
145-
confirmQuit = context.eventHandler {
146-
(state as? MaybeQuittingForSure)?.let { oldState ->
147-
state = GameOver(oldState.playerInfo, oldState.completedGame)
148-
}
145+
confirmQuit = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
146+
state = GameOver(oldState.playerInfo, oldState.completedGame)
149147
},
150-
continuePlaying = context.eventHandler {
151-
(state as? MaybeQuittingForSure)?.let { oldState ->
152-
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
153-
}
148+
continuePlaying = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
149+
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
154150
}
155151
)
156152
)
@@ -169,43 +165,37 @@ class RealRunGameWorkflow(
169165
renderState,
170166
onTrySaveAgain = context.trySaveAgain(),
171167
onPlayAgain = context.playAgain(),
172-
onExit = context.eventHandler { setOutput(FinishedPlaying) }
168+
onExit = context.safeEventHandler<GameOver> { setOutput(FinishedPlaying) }
173169
)
174170
)
175171
}
176172
}
177173

178-
private fun stopPlaying(game: CompletedGame) = action {
179-
val oldState = state as Playing
174+
private fun stopPlaying(game: CompletedGame) = safeAction<Playing>("stopPlaying") { oldState ->
180175
state = when (game.ending) {
181176
Quitted -> MaybeQuitting(oldState.playerInfo, game)
182177
else -> GameOver(oldState.playerInfo, game)
183178
}
184179
}
185180

186-
private fun handleLogGame(result: GameLog.LogResult) = action {
187-
val oldState = state as GameOver
181+
private fun handleLogGame(result: GameLog.LogResult) = safeAction<GameOver> { oldState ->
188182
state = when (result) {
189183
TRY_LATER -> oldState.copy(syncState = SAVE_FAILED)
190184
LOGGED -> oldState.copy(syncState = SAVED)
191185
}
192186
}
193187

194-
private fun RenderContext.playAgain() = eventHandler {
195-
(state as? GameOver)?.let { oldState ->
196-
val (x, o) = oldState.playerInfo
197-
state = NewGame(x, o)
198-
}
188+
private fun RenderContext.playAgain() = safeEventHandler<GameOver> { oldState ->
189+
val (x, o) = oldState.playerInfo
190+
state = NewGame(x, o)
199191
}
200192

201-
private fun RenderContext.trySaveAgain() = eventHandler {
202-
(state as? GameOver)?.let { oldState ->
203-
check(oldState.syncState == SAVE_FAILED) {
204-
"Should only fire trySaveAgain in syncState $SAVE_FAILED, " +
205-
"was ${oldState.syncState}"
206-
}
207-
state = oldState.copy(syncState = SAVING)
193+
private fun RenderContext.trySaveAgain() = safeEventHandler<GameOver> { oldState ->
194+
check(oldState.syncState == SAVE_FAILED) {
195+
"Should only fire trySaveAgain in syncState $SAVE_FAILED, " +
196+
"was ${oldState.syncState}"
208197
}
198+
state = oldState.copy(syncState = SAVING)
209199
}
210200

211201
override fun snapshotState(state: RunGameState): Snapshot = state.toSnapshot()

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/BaseRenderContext.kt

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
264264
public fun <E1, E2, E3, E4, E5, E6, E7, E8, E9> eventHandler(
265265
name: () -> String = { "eventHandler" },
266266
update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>
267-
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
267+
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
268268
): (E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit {
269269
return { e1, e2, e3, e4, e5, e6, e7, e8, e9 ->
270270
actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) })
@@ -274,7 +274,7 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
274274
public fun <E1, E2, E3, E4, E5, E6, E7, E8, E9, E10> eventHandler(
275275
name: () -> String = { "eventHandler" },
276276
update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>
277-
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit
277+
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit
278278
): (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit {
279279
return { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 ->
280280
actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) })
@@ -287,30 +287,30 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
287287
*/
288288
public fun <PropsT, StateT, OutputT, ChildOutputT, ChildRenderingT>
289289
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
290-
child: Workflow<Unit, ChildOutputT, ChildRenderingT>,
291-
key: String = "",
292-
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
293-
): ChildRenderingT = renderChild(child, Unit, key, handler)
290+
child: Workflow<Unit, ChildOutputT, ChildRenderingT>,
291+
key: String = "",
292+
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
293+
): ChildRenderingT = renderChild(child, Unit, key, handler)
294294

295295
/**
296296
* Convenience alias of [BaseRenderContext.renderChild] for workflows that don't emit output.
297297
*/
298298
public fun <PropsT, ChildPropsT, StateT, OutputT, ChildRenderingT>
299299
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
300-
child: Workflow<ChildPropsT, Nothing, ChildRenderingT>,
301-
props: ChildPropsT,
302-
key: String = ""
303-
): ChildRenderingT = renderChild(child, props, key) { noAction() }
300+
child: Workflow<ChildPropsT, Nothing, ChildRenderingT>,
301+
props: ChildPropsT,
302+
key: String = ""
303+
): ChildRenderingT = renderChild(child, props, key) { noAction() }
304304

305305
/**
306306
* Convenience alias of [BaseRenderContext.renderChild] for children that don't take props or emit
307307
* output.
308308
*/
309309
public fun <PropsT, StateT, OutputT, ChildRenderingT>
310310
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
311-
child: Workflow<Unit, Nothing, ChildRenderingT>,
312-
key: String = ""
313-
): ChildRenderingT = renderChild(child, Unit, key) { noAction() }
311+
child: Workflow<Unit, Nothing, ChildRenderingT>,
312+
key: String = ""
313+
): ChildRenderingT = renderChild(child, Unit, key) { noAction() }
314314

315315
/**
316316
* Ensures a [LifecycleWorker] is running. Since [worker] can't emit anything,
@@ -323,9 +323,9 @@ public fun <PropsT, StateT, OutputT, ChildRenderingT>
323323
*/
324324
public inline fun <reified W : LifecycleWorker, PropsT, StateT, OutputT>
325325
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
326-
worker: W,
327-
key: String = ""
328-
) {
326+
worker: W,
327+
key: String = ""
328+
) {
329329
runningWorker(worker, key) {
330330
// The compiler thinks this code is unreachable, and it is correct. But we have to pass a lambda
331331
// here so we might as well check at runtime as well.
@@ -348,9 +348,9 @@ public inline fun <reified W : LifecycleWorker, PropsT, StateT, OutputT>
348348
)
349349
public inline fun <reified W : Worker<Nothing>, PropsT, StateT, OutputT>
350350
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
351-
worker: W,
352-
key: String = ""
353-
) {
351+
worker: W,
352+
key: String = ""
353+
) {
354354
runningWorker(worker, key) {
355355
// The compiler thinks this code is unreachable, and it is correct. But we have to pass a lambda
356356
// here so we might as well check at runtime as well.
@@ -378,10 +378,10 @@ public inline fun <reified W : Worker<Nothing>, PropsT, StateT, OutputT>
378378
*/
379379
public inline fun <T, reified W : Worker<T>, PropsT, StateT, OutputT>
380380
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
381-
worker: W,
382-
key: String = "",
383-
noinline handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
384-
) {
381+
worker: W,
382+
key: String = "",
383+
noinline handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
384+
) {
385385
runningWorker(worker, typeOf<W>(), key, handler)
386386
}
387387

@@ -396,11 +396,11 @@ public inline fun <T, reified W : Worker<T>, PropsT, StateT, OutputT>
396396
@PublishedApi
397397
internal fun <T, PropsT, StateT, OutputT>
398398
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
399-
worker: Worker<T>,
400-
workerType: KType,
401-
key: String = "",
402-
handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
403-
) {
399+
worker: Worker<T>,
400+
workerType: KType,
401+
key: String = "",
402+
handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
403+
) {
404404
val workerWorkflow = WorkerWorkflow<T>(workerType, key)
405405
renderChild(workerWorkflow, props = worker, key = key, handler = handler)
406406
}

0 commit comments

Comments
 (0)