Skip to content

Commit 10f5ef7

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 5e1c32b commit 10f5ef7

File tree

4 files changed

+470
-65
lines changed

4 files changed

+470
-65
lines changed

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

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -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
@@ -88,8 +87,12 @@ class RealRunGameWorkflow(
8887
namePrompt = NewGameScreen(
8988
renderState.defaultXName,
9089
renderState.defaultOName,
91-
onCancel = context.eventHandler { setOutput(CanceledStart) },
92-
onStartGame = context.eventHandler { x, o -> state = Playing(PlayerInfo(x, o)) }
90+
onCancel = context.safeEventHandler<NewGame> {
91+
setOutput(CanceledStart)
92+
},
93+
onStartGame = context.safeEventHandler<NewGame, String, String> { _, x, o ->
94+
state = Playing(PlayerInfo(x, o))
95+
}
9396
)
9497
)
9598
}
@@ -119,15 +122,11 @@ class RealRunGameWorkflow(
119122
message = "Do you really want to concede the game?",
120123
positive = "I Quit",
121124
negative = "No",
122-
confirmQuit = context.eventHandler {
123-
(state as? MaybeQuitting)?.let { oldState ->
124-
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
125-
}
125+
confirmQuit = context.safeEventHandler<MaybeQuitting> { oldState ->
126+
state = MaybeQuittingForSure(oldState.playerInfo, oldState.completedGame)
126127
},
127-
continuePlaying = context.eventHandler {
128-
(state as? MaybeQuitting)?.let { oldState ->
129-
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
130-
}
128+
continuePlaying = context.safeEventHandler<MaybeQuitting> { oldState ->
129+
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
131130
}
132131
)
133132
)
@@ -142,15 +141,11 @@ class RealRunGameWorkflow(
142141
message = "Really?",
143142
positive = "Yes!!",
144143
negative = "Sigh, no",
145-
confirmQuit = context.eventHandler {
146-
(state as? MaybeQuittingForSure)?.let { oldState ->
147-
state = GameOver(oldState.playerInfo, oldState.completedGame)
148-
}
144+
confirmQuit = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
145+
state = GameOver(oldState.playerInfo, oldState.completedGame)
149146
},
150-
continuePlaying = context.eventHandler {
151-
(state as? MaybeQuittingForSure)?.let { oldState ->
152-
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
153-
}
147+
continuePlaying = context.safeEventHandler<MaybeQuittingForSure> { oldState ->
148+
state = Playing(oldState.playerInfo, oldState.completedGame.lastTurn)
154149
}
155150
)
156151
)
@@ -169,43 +164,37 @@ class RealRunGameWorkflow(
169164
renderState,
170165
onTrySaveAgain = context.trySaveAgain(),
171166
onPlayAgain = context.playAgain(),
172-
onExit = context.eventHandler { setOutput(FinishedPlaying) }
167+
onExit = context.safeEventHandler<GameOver> { setOutput(FinishedPlaying) }
173168
)
174169
)
175170
}
176171
}
177172

178-
private fun stopPlaying(game: CompletedGame) = action {
179-
val oldState = state as Playing
173+
private fun stopPlaying(game: CompletedGame) = safeAction<Playing>("stopPlaying") { oldState ->
180174
state = when (game.ending) {
181175
Quitted -> MaybeQuitting(oldState.playerInfo, game)
182176
else -> GameOver(oldState.playerInfo, game)
183177
}
184178
}
185179

186-
private fun handleLogGame(result: GameLog.LogResult) = action {
187-
val oldState = state as GameOver
180+
private fun handleLogGame(result: GameLog.LogResult) = safeAction<GameOver> { oldState ->
188181
state = when (result) {
189182
TRY_LATER -> oldState.copy(syncState = SAVE_FAILED)
190183
LOGGED -> oldState.copy(syncState = SAVED)
191184
}
192185
}
193186

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

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)
192+
private fun RenderContext.trySaveAgain() = safeEventHandler<GameOver> { oldState ->
193+
check(oldState.syncState == SAVE_FAILED) {
194+
"Should only fire trySaveAgain in syncState $SAVE_FAILED, " +
195+
"was ${oldState.syncState}"
208196
}
197+
state = oldState.copy(syncState = SAVING)
209198
}
210199

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

workflow-core/api/workflow-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public final class com/squareup/workflow1/Snapshots {
155155
public abstract class com/squareup/workflow1/StatefulWorkflow : com/squareup/workflow1/IdCacheable, com/squareup/workflow1/Workflow {
156156
public fun <init> ()V
157157
public final fun asStatefulWorkflow ()Lcom/squareup/workflow1/StatefulWorkflow;
158+
public final fun defaultOnFailedCast (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Object;)V
158159
public fun getCachedIdentifier ()Lcom/squareup/workflow1/WorkflowIdentifier;
159160
public abstract fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;)Ljava/lang/Object;
160161
public fun initialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;)Ljava/lang/Object;

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

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,45 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
130130
* given [update] function, and immediately passes it to [actionSink]. Handy for
131131
* attaching event handlers to renderings.
132132
*
133+
* It is important to understand that the [update] lambda you provide here
134+
* may not run synchronously. This function and its overloads provide a short cut
135+
* that lets you replace this snippet:
136+
*
137+
* return SomeScreen(
138+
* onClick = {
139+
* context.actionSink.send(
140+
* action { state = SomeNewState }
141+
* }
142+
* }
143+
* )
144+
*
145+
* with this:
146+
*
147+
* return SomeScreen(
148+
* onClick = context.eventHandler { state = SomeNewState }
149+
* )
150+
*
151+
* Notice how your [update] function is passed to the [actionSink][BaseRenderContext.actionSink]
152+
* to be eventually executed as the body of a [WorkflowAction]. If several actions get stacked
153+
* up at once (think about accidental rapid taps on a button), that could take a while.
154+
*
155+
* If you require something to happen the instant a UI action happens, [eventHandler]
156+
* is the wrong choice. You'll want to write your own call to `actionSink.send`:
157+
*
158+
* return SomeScreen(
159+
* onClick = {
160+
* // This happens immediately.
161+
* MyAnalytics.log("SomeScreen was clicked")
162+
*
163+
* context.actionSink.send(
164+
* action {
165+
* // This happens eventually.
166+
* state = SomeNewState
167+
* }
168+
* }
169+
* }
170+
* )
171+
*
133172
* @param name A string describing the update, included in the action's [toString]
134173
* as a debugging aid
135174
* @param update Function that defines the workflow update.
@@ -264,7 +303,7 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
264303
public fun <E1, E2, E3, E4, E5, E6, E7, E8, E9> eventHandler(
265304
name: () -> String = { "eventHandler" },
266305
update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>
267-
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
306+
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit
268307
): (E1, E2, E3, E4, E5, E6, E7, E8, E9) -> Unit {
269308
return { e1, e2, e3, e4, e5, e6, e7, e8, e9 ->
270309
actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9) })
@@ -274,7 +313,7 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
274313
public fun <E1, E2, E3, E4, E5, E6, E7, E8, E9, E10> eventHandler(
275314
name: () -> String = { "eventHandler" },
276315
update: WorkflowAction<@UnsafeVariance PropsT, StateT, @UnsafeVariance OutputT>
277-
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit
316+
.Updater.(E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit
278317
): (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10) -> Unit {
279318
return { e1, e2, e3, e4, e5, e6, e7, e8, e9, e10 ->
280319
actionSink.send(action(name) { update(e1, e2, e3, e4, e5, e6, e7, e8, e9, e10) })
@@ -287,30 +326,30 @@ public interface BaseRenderContext<out PropsT, StateT, in OutputT> {
287326
*/
288327
public fun <PropsT, StateT, OutputT, ChildOutputT, ChildRenderingT>
289328
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)
329+
child: Workflow<Unit, ChildOutputT, ChildRenderingT>,
330+
key: String = "",
331+
handler: (ChildOutputT) -> WorkflowAction<PropsT, StateT, OutputT>
332+
): ChildRenderingT = renderChild(child, Unit, key, handler)
294333

295334
/**
296335
* Convenience alias of [BaseRenderContext.renderChild] for workflows that don't emit output.
297336
*/
298337
public fun <PropsT, ChildPropsT, StateT, OutputT, ChildRenderingT>
299338
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
300-
child: Workflow<ChildPropsT, Nothing, ChildRenderingT>,
301-
props: ChildPropsT,
302-
key: String = ""
303-
): ChildRenderingT = renderChild(child, props, key) { noAction() }
339+
child: Workflow<ChildPropsT, Nothing, ChildRenderingT>,
340+
props: ChildPropsT,
341+
key: String = ""
342+
): ChildRenderingT = renderChild(child, props, key) { noAction() }
304343

305344
/**
306345
* Convenience alias of [BaseRenderContext.renderChild] for children that don't take props or emit
307346
* output.
308347
*/
309348
public fun <PropsT, StateT, OutputT, ChildRenderingT>
310349
BaseRenderContext<PropsT, StateT, OutputT>.renderChild(
311-
child: Workflow<Unit, Nothing, ChildRenderingT>,
312-
key: String = ""
313-
): ChildRenderingT = renderChild(child, Unit, key) { noAction() }
350+
child: Workflow<Unit, Nothing, ChildRenderingT>,
351+
key: String = ""
352+
): ChildRenderingT = renderChild(child, Unit, key) { noAction() }
314353

315354
/**
316355
* Ensures a [LifecycleWorker] is running. Since [worker] can't emit anything,
@@ -323,9 +362,9 @@ public fun <PropsT, StateT, OutputT, ChildRenderingT>
323362
*/
324363
public inline fun <reified W : LifecycleWorker, PropsT, StateT, OutputT>
325364
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
326-
worker: W,
327-
key: String = ""
328-
) {
365+
worker: W,
366+
key: String = ""
367+
) {
329368
runningWorker(worker, key) {
330369
// The compiler thinks this code is unreachable, and it is correct. But we have to pass a lambda
331370
// here so we might as well check at runtime as well.
@@ -348,9 +387,9 @@ public inline fun <reified W : LifecycleWorker, PropsT, StateT, OutputT>
348387
)
349388
public inline fun <reified W : Worker<Nothing>, PropsT, StateT, OutputT>
350389
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
351-
worker: W,
352-
key: String = ""
353-
) {
390+
worker: W,
391+
key: String = ""
392+
) {
354393
runningWorker(worker, key) {
355394
// The compiler thinks this code is unreachable, and it is correct. But we have to pass a lambda
356395
// here so we might as well check at runtime as well.
@@ -378,10 +417,10 @@ public inline fun <reified W : Worker<Nothing>, PropsT, StateT, OutputT>
378417
*/
379418
public inline fun <T, reified W : Worker<T>, PropsT, StateT, OutputT>
380419
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
381-
worker: W,
382-
key: String = "",
383-
noinline handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
384-
) {
420+
worker: W,
421+
key: String = "",
422+
noinline handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
423+
) {
385424
runningWorker(worker, typeOf<W>(), key, handler)
386425
}
387426

@@ -396,11 +435,11 @@ public inline fun <T, reified W : Worker<T>, PropsT, StateT, OutputT>
396435
@PublishedApi
397436
internal fun <T, PropsT, StateT, OutputT>
398437
BaseRenderContext<PropsT, StateT, OutputT>.runningWorker(
399-
worker: Worker<T>,
400-
workerType: KType,
401-
key: String = "",
402-
handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
403-
) {
438+
worker: Worker<T>,
439+
workerType: KType,
440+
key: String = "",
441+
handler: (T) -> WorkflowAction<PropsT, StateT, OutputT>
442+
) {
404443
val workerWorkflow = WorkerWorkflow<T>(workerType, key)
405444
renderChild(workerWorkflow, props = worker, key = key, handler = handler)
406445
}

0 commit comments

Comments
 (0)