|
| 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