Skip to content

Commit 95ff11b

Browse files
cleanup scopes and allow prop updates to children
1 parent 82600bb commit 95ff11b

File tree

3 files changed

+345
-319
lines changed

3 files changed

+345
-319
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package com.squareup.sample.thingy
2+
3+
import com.squareup.workflow1.StatefulWorkflow
4+
import com.squareup.workflow1.Workflow
5+
import com.squareup.workflow1.ui.Screen
6+
import com.squareup.workflow1.ui.navigation.BackStackScreen
7+
import kotlinx.coroutines.CoroutineScope
8+
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.StateFlow
10+
import kotlinx.coroutines.flow.flowOf
11+
12+
/**
13+
* Creates a [BackStackWorkflow].
14+
*/
15+
public inline fun <PropsT, OutputT> backStackWorkflow(
16+
crossinline block: suspend BackStackScope<OutputT>.(props: StateFlow<PropsT>) -> Unit
17+
): Workflow<PropsT, OutputT, BackStackScreen<Screen>> =
18+
object : BackStackWorkflow<PropsT, OutputT>() {
19+
override suspend fun BackStackScope<OutputT>.runBackStack(props: StateFlow<PropsT>) {
20+
block(props)
21+
}
22+
}
23+
24+
/**
25+
* Returns a [Workflow] that renders a [BackStackScreen] whose frames are controlled by the code
26+
* in [runBackStack].
27+
*
28+
* [runBackStack] can render child workflows by calling [BackStackScope.showWorkflow]. It can emit
29+
* outputs to its parent by calling [BackStackScope.emitOutput], and access its props via
30+
* the parameter passed to [runBackStack].
31+
*
32+
* # Examples
33+
*
34+
* The backstack is represented by _nesting_ `showWorkflow` calls. Consider this example:
35+
* ```
36+
* backStackWorkflow {
37+
* showWorkflow(child1) {
38+
* showWorkflow(child2) {
39+
* showWorkflow(child3) {
40+
* // goBack()
41+
* }
42+
* }
43+
* }
44+
* }
45+
* ```
46+
* This eventually represents a backstack of `[child1, child2, child3]`. `child2` will be pushed
47+
* onto the stack when `child1` emits an output, and `child3` pushed when `child2` emits. The
48+
* lambdas for `child2` and `child3` can call `goBack` to pop the stack and cancel the lambdas that
49+
* called their `showWorkflow`, until the next output is emitted.
50+
*
51+
* Contrast with calls in series:
52+
* ```
53+
* backStackWorkflow {
54+
* showWorkflow(child1) { finishWith(Unit) }
55+
* showWorkflow(child2) { finishWith(Unit) }
56+
* showWorkflow(child3) { }
57+
* }
58+
* ```
59+
* `child1` will be shown immediately, but when it emits an output, instead of pushing `child2` onto
60+
* the stack, `child1` will be removed from the stack and replaced with `child2`.
61+
*
62+
* These can be combined:
63+
* ```
64+
* backStackWorkflow {
65+
* showWorkflow(child1) {
66+
* showWorkflow(child2) {
67+
* // goBack(), or
68+
* finishWith(Unit)
69+
* }
70+
* showWorkflow(child3) {
71+
* // goBack()
72+
* }
73+
* }
74+
* }
75+
* ```
76+
* This code will show `child1` immediately, then when it emits an output show `child2`. When
77+
* `child2` emits an output, it can decide to call `goBack` to show `child1` again, or call
78+
* `finishWith` to replace itself with `child3`. `child3` can also call `goBack` to show `child`
79+
* again.
80+
*/
81+
public abstract class BackStackWorkflow<PropsT, OutputT> :
82+
Workflow<PropsT, OutputT, BackStackScreen<Screen>> {
83+
84+
abstract suspend fun BackStackScope<OutputT>.runBackStack(props: StateFlow<PropsT>)
85+
86+
final override fun asStatefulWorkflow():
87+
StatefulWorkflow<PropsT, *, OutputT, BackStackScreen<Screen>> =
88+
BackStackWorkflowImpl(this)
89+
}
90+
91+
@DslMarker
92+
annotation class BackStackWorkflowDsl
93+
94+
@BackStackWorkflowDsl
95+
public sealed interface BackStackScope<OutputT> : CoroutineScope {
96+
97+
/**
98+
* Emits an output to the [backStackWorkflow]'s parent.
99+
*/
100+
fun emitOutput(output: OutputT)
101+
102+
/**
103+
* Starts rendering [workflow] and pushes its rendering onto the top of the backstack.
104+
*
105+
* Whenever [workflow] emits an output, [onOutput] is launched into a new coroutine. If one call
106+
* doesn't finish before another output is emitted, multiple callbacks can run concurrently.
107+
*
108+
* When [onOutput] calls [BackStackNestedScope.finishWith], this workflow stops rendering, its
109+
* rendering is removed from the backstack, and any running output handlers are cancelled.
110+
*
111+
* When [onOutput] calls [BackStackNestedScope.goBack], if this [showWorkflow] call is nested in
112+
* another, then this workflow will stop rendering, any of its still-running output handlers will
113+
* be cancelled, and the output handler that called this [showWorkflow] will be cancelled.
114+
* If this is a top-level workflow in the [BackStackWorkflow], the whole
115+
* [BackStackWorkflow.runBackStack] is cancelled and restarted, since "back" is only a concept
116+
* that is relevant within a backstack, and it's not possible to know whether the parent supports
117+
* back. What you probably want is to emit an output instead to tell the parent to go back.
118+
*
119+
* @param props The props passed to [workflow] when rendering it. This method will suspend until
120+
* the first value is emitted. Consider transforming the [BackStackWorkflow.runBackStack] props
121+
* [StateFlow] or using [flowOf].
122+
*/
123+
suspend fun <ChildPropsT, ChildOutputT, R> showWorkflow(
124+
workflow: Workflow<ChildPropsT, ChildOutputT, Screen>,
125+
props: Flow<ChildPropsT>,
126+
onOutput: suspend BackStackNestedScope<OutputT, R>.(output: ChildOutputT) -> Unit
127+
): R
128+
}
129+
130+
/**
131+
* Scope receiver used for all [showWorkflow] calls. This has all the capabilities of
132+
* [BackStackScope] with the additional ability to [finish][finishWith] a nested workflow or
133+
* [go back][goBack] to its outer workflow.
134+
*/
135+
@BackStackWorkflowDsl
136+
public sealed interface BackStackNestedScope<OutputT, R> : BackStackScope<OutputT> {
137+
138+
/**
139+
* Causes the [showWorkflow] call that ran the output handler that was passed this scope to return
140+
* [value] and cancels any output handlers still running for that workflow. The workflow is
141+
* removed from the stack and will no longer be rendered.
142+
*/
143+
fun finishWith(value: R): Nothing
144+
145+
/**
146+
* Removes all workflows started by the parent workflow's handler that invoked this [showWorkflow]
147+
* from the stack, and cancels that parent output handler coroutine (and thus all child workflow
148+
* coroutines as well).
149+
*/
150+
fun goBack(): Nothing
151+
}
152+
153+
public suspend inline fun <OutputT, ChildOutputT, R> BackStackScope<OutputT>.showWorkflow(
154+
workflow: Workflow<Unit, ChildOutputT, Screen>,
155+
noinline onOutput: suspend BackStackNestedScope<OutputT, R>.(output: ChildOutputT) -> Unit
156+
): R = showWorkflow(workflow, props = flowOf(Unit), onOutput)
157+
158+
public suspend inline fun <ChildPropsT> BackStackScope<*>.showWorkflow(
159+
workflow: Workflow<ChildPropsT, Nothing, Screen>,
160+
props: Flow<ChildPropsT>,
161+
): Nothing = showWorkflow(workflow, props = props) { error("Cannot call") }
162+
163+
public suspend inline fun BackStackScope<*>.showWorkflow(
164+
workflow: Workflow<Unit, Nothing, Screen>,
165+
): Nothing = showWorkflow(workflow, props = flowOf(Unit)) { error("Cannot call") }

0 commit comments

Comments
 (0)