@@ -23,18 +23,31 @@ import kotlinx.coroutines.launch
2323import kotlinx.coroutines.suspendCancellableCoroutine
2424import kotlin.coroutines.resume
2525
26+ /* *
27+ * Creates a [BackStackWorkflow].
28+ */
29+ public inline fun <PropsT , OutputT > backStackWorkflow (
30+ crossinline block : suspend RootScope <PropsT , OutputT >.() -> Unit
31+ ): Workflow <PropsT , OutputT , BackStackScreen <Screen >> =
32+ object : BackStackWorkflow <PropsT , OutputT >() {
33+ override suspend fun RootScope <PropsT , OutputT >.runBackStack () {
34+ block()
35+ }
36+ }
37+
2638/* *
2739 * Returns a [Workflow] that renders a [BackStackScreen] whose frames are controlled by the code
28- * in [block ].
40+ * in [runBackStack ].
2941 *
30- * [block] can render child workflows by calling [RootScope.showWorkflow]. It can emit outputs to
31- * its parent by calling [RootScope.emitOutput], and access its props via [RootScope.props].
42+ * [runBackStack] can render child workflows by calling [RootScope.showWorkflow]. It can emit
43+ * outputs to its parent by calling [RootScope.emitOutput], and access its props via
44+ * [RootScope.props].
3245 *
3346 * # Examples
3447 *
3548 * The backstack is represented by _nesting_ `showWorkflow` calls. Consider this example:
3649 * ```
37- * thingyWorkflow {
50+ * backStackWorkflow {
3851 * showWorkflow(child1) {
3952 * showWorkflow(child2) {
4053 * showWorkflow(child3) {
@@ -51,7 +64,7 @@ import kotlin.coroutines.resume
5164 *
5265 * Contrast with calls in series:
5366 * ```
54- * thingyWorkflow {
67+ * backStackWorkflow {
5568 * showWorkflow(child1) { finishWith(Unit) }
5669 * showWorkflow(child2) { finishWith(Unit) }
5770 * showWorkflow(child3) { }
@@ -62,7 +75,7 @@ import kotlin.coroutines.resume
6275 *
6376 * These can be combined:
6477 * ```
65- * thingyWorkflow {
78+ * backStackWorkflow {
6679 * showWorkflow(child1) {
6780 * showWorkflow(child2) {
6881 * // goBack(), or
@@ -79,19 +92,25 @@ import kotlin.coroutines.resume
7992 * `finishWith` to replace itself with `child3`. `child3` can also call `goBack` to show `child`
8093 * again.
8194 */
82- public fun <PropsT , OutputT > thingyWorkflow (
83- block : suspend RootScope <PropsT , OutputT >.() -> Unit
84- ): Workflow <PropsT , OutputT , BackStackScreen <Screen >> = ThingyWorkflow (block)
95+ public abstract class BackStackWorkflow <PropsT , OutputT > :
96+ Workflow <PropsT , OutputT , BackStackScreen <Screen >> {
97+
98+ abstract suspend fun RootScope <PropsT , OutputT >.runBackStack ()
99+
100+ final override fun asStatefulWorkflow ():
101+ StatefulWorkflow <PropsT , * , OutputT , BackStackScreen <Screen >> =
102+ BackStackWorkflowImpl (this )
103+ }
85104
86105@DslMarker
87- annotation class ThingyDsl
106+ annotation class BackStackWorkflowDsl
88107
89- @ThingyDsl
108+ @BackStackWorkflowDsl
90109public interface RootScope <PropsT , OutputT > : CoroutineScope {
91110 val props: StateFlow <PropsT >
92111
93112 /* *
94- * Emits an output to the [thingyWorkflow ]'s parent.
113+ * Emits an output to the [backStackWorkflow ]'s parent.
95114 */
96115 fun emitOutput (output : OutputT )
97116
@@ -104,7 +123,7 @@ public interface RootScope<PropsT, OutputT> : CoroutineScope {
104123 * When [onOutput] calls [ShowWorkflowScope.finishWith], this workflow stops rendering, its
105124 * rendering is removed from the backstack, and any running output handlers are cancelled.
106125 *
107- * Note that top-level workflows inside a [thingyWorkflow ] cannot call
126+ * Note that top-level workflows inside a [backStackWorkflow ] cannot call
108127 * [ShowWorkflowChildScope.goBack] because the parent doesn't necessarily support that operation.
109128 */
110129 suspend fun <ChildPropsT , ChildOutputT , R > showWorkflow (
@@ -114,11 +133,11 @@ public interface RootScope<PropsT, OutputT> : CoroutineScope {
114133 ): R
115134}
116135
117- @ThingyDsl
136+ @BackStackWorkflowDsl
118137public sealed interface ShowWorkflowScope <OutputT , R > : CoroutineScope {
119138
120139 /* *
121- * Emits an output to the [thingyWorkflow ]'s parent.
140+ * Emits an output to the [backStackWorkflow ]'s parent.
122141 */
123142 fun emitOutput (output : OutputT )
124143
@@ -147,7 +166,7 @@ public sealed interface ShowWorkflowScope<OutputT, R> : CoroutineScope {
147166 ): R
148167}
149168
150- @ThingyDsl
169+ @BackStackWorkflowDsl
151170public sealed interface ShowWorkflowChildScope <OutputT , R > : ShowWorkflowScope <OutputT , R > {
152171 /* *
153172 * Removes all workflows started by the parent workflow's handler that invoked this [showWorkflow]
@@ -187,7 +206,7 @@ public suspend inline fun ShowWorkflowScope<*, *>.showWorkflow(
187206
188207private class RootScopeImpl <PropsT , OutputT >(
189208 override val props : MutableStateFlow <PropsT >,
190- private val actionSink : Sink <WorkflowAction <PropsT , ThingyState , OutputT >>,
209+ private val actionSink : Sink <WorkflowAction <PropsT , BackStackState , OutputT >>,
191210 coroutineScope : CoroutineScope ,
192211) : RootScope<PropsT, OutputT>, CoroutineScope by coroutineScope {
193212
@@ -210,8 +229,64 @@ private class RootScopeImpl<PropsT, OutputT>(
210229 )
211230}
212231
232+ private class BackStackWorkflowImpl <PropsT , OutputT >(
233+ private val workflow : BackStackWorkflow <PropsT , OutputT >
234+ ) : StatefulWorkflow<
235+ PropsT ,
236+ BackStackState ,
237+ OutputT ,
238+ BackStackScreen <Screen >
239+ > () {
240+
241+ override fun initialState (
242+ props : PropsT ,
243+ snapshot : Snapshot ?
244+ ): BackStackState {
245+ return BackStackState (
246+ stack = emptyList(),
247+ props = MutableStateFlow (props)
248+ )
249+ }
250+
251+ override fun onPropsChanged (
252+ old : PropsT ,
253+ new : PropsT ,
254+ state : BackStackState
255+ ): BackStackState = state.apply {
256+ props.value = new
257+ }
258+
259+ override fun render (
260+ renderProps : PropsT ,
261+ renderState : BackStackState ,
262+ context : RenderContext <PropsT , BackStackState , OutputT >
263+ ): BackStackScreen <Screen > {
264+ context.runningSideEffect(" main" ) {
265+ @Suppress(" UNCHECKED_CAST" )
266+ val scope = RootScopeImpl (
267+ props = renderState.props as MutableStateFlow <PropsT >,
268+ actionSink = context.actionSink,
269+ coroutineScope = this ,
270+ )
271+ with (workflow) {
272+ scope.runBackStack()
273+ }
274+ }
275+
276+ val renderings = renderState.stack.map { frame ->
277+ @Suppress(" UNCHECKED_CAST" )
278+ (frame as Frame <PropsT , OutputT , * , * , * >).renderWorkflow(context)
279+ }
280+
281+ // TODO show a loading screen if renderings is empty.
282+ return renderings.toBackStackScreen()
283+ }
284+
285+ override fun snapshotState (state : BackStackState ): Snapshot ? = null
286+ }
287+
213288private class ShowWorkflowChildScopeImpl <PropsT , OutputT , R >(
214- private val actionSink : Sink <WorkflowAction <PropsT , ThingyState , OutputT >>,
289+ private val actionSink : Sink <WorkflowAction <PropsT , BackStackState , OutputT >>,
215290 coroutineScope : CoroutineScope ,
216291 private val onFinish : (R ) -> Unit ,
217292 private val thisFrame : Frame <* , * , * , * , * >,
@@ -257,15 +332,15 @@ private class Frame<PropsT, OutputT, ChildPropsT, ChildOutputT, R>(
257332 private val callerJob : Job ,
258333 val frameScope : CoroutineScope ,
259334 private val onOutput : suspend ShowWorkflowChildScopeImpl <PropsT , OutputT , R >.(ChildOutputT ) -> Unit ,
260- private val actionSink : Sink <WorkflowAction <PropsT , ThingyState , OutputT >>,
335+ private val actionSink : Sink <WorkflowAction <PropsT , BackStackState , OutputT >>,
261336 private val parent : Frame <* , * , * , * , * >? ,
262337) {
263338 private val result = CompletableDeferred <R >(parent = frameScope.coroutineContext.job)
264339
265340 suspend fun awaitResult (): R = result.await()
266341
267342 fun renderWorkflow (
268- context : StatefulWorkflow .RenderContext <PropsT , ThingyState , OutputT >
343+ context : StatefulWorkflow .RenderContext <PropsT , BackStackState , OutputT >
269344 ): Screen = context.renderChild(
270345 child = workflow,
271346 props = props,
@@ -294,11 +369,11 @@ private class Frame<PropsT, OutputT, ChildPropsT, ChildOutputT, R>(
294369 })
295370 }
296371
297- private fun onOutput (output : ChildOutputT ): WorkflowAction <PropsT , ThingyState , OutputT > {
372+ private fun onOutput (output : ChildOutputT ): WorkflowAction <PropsT , BackStackState , OutputT > {
298373 var canAcceptAction = true
299- var action: WorkflowAction <PropsT , ThingyState , OutputT >? = null
300- val sink = object : Sink <WorkflowAction <PropsT , ThingyState , OutputT >> {
301- override fun send (value : WorkflowAction <PropsT , ThingyState , OutputT >) {
374+ var action: WorkflowAction <PropsT , BackStackState , OutputT >? = null
375+ val sink = object : Sink <WorkflowAction <PropsT , BackStackState , OutputT >> {
376+ override fun send (value : WorkflowAction <PropsT , BackStackState , OutputT >) {
302377 val sendToSink = synchronized(result) {
303378 if (canAcceptAction) {
304379 action = value
@@ -349,7 +424,7 @@ private suspend fun <PropsT, OutputT, ChildPropsT, ChildOutputT, R> showWorkflow
349424 workflow : Workflow <ChildPropsT , ChildOutputT , Screen >,
350425 props : ChildPropsT ,
351426 onOutput : suspend ShowWorkflowChildScopeImpl <PropsT , OutputT , R >.(ChildOutputT ) -> Unit ,
352- actionSink : Sink <WorkflowAction <PropsT , ThingyState , OutputT >>,
427+ actionSink : Sink <WorkflowAction <PropsT , BackStackState , OutputT >>,
353428 parentFrame : Frame <* , * , * , * , * >? ,
354429): R {
355430 val callerContext = currentCoroutineContext()
@@ -381,12 +456,12 @@ private suspend fun <PropsT, OutputT, ChildPropsT, ChildOutputT, R> showWorkflow
381456 }
382457}
383458
384- private class ThingyState (
459+ private class BackStackState (
385460 val stack : List <Frame <* , * , * , * , * >>,
386461 val props : MutableStateFlow <Any ?>,
387462) {
388463
389- fun copy (stack : List <Frame <* , * , * , * , * >> = this.stack) = ThingyState (
464+ fun copy (stack : List <Frame <* , * , * , * , * >> = this.stack) = BackStackState (
390465 stack = stack,
391466 props = props
392467 )
@@ -395,60 +470,6 @@ private class ThingyState(
395470 fun removeFrame (frame : Frame <* , * , * , * , * >) = copy(stack = stack - frame)
396471}
397472
398- private class ThingyWorkflow <PropsT , OutputT >(
399- private val block : suspend RootScope <PropsT , OutputT >.() -> Unit
400- ) : StatefulWorkflow<
401- PropsT ,
402- ThingyState ,
403- OutputT ,
404- BackStackScreen <Screen >
405- > () {
406-
407- override fun initialState (
408- props : PropsT ,
409- snapshot : Snapshot ?
410- ): ThingyState {
411- return ThingyState (
412- stack = emptyList(),
413- props = MutableStateFlow (props)
414- )
415- }
416-
417- override fun onPropsChanged (
418- old : PropsT ,
419- new : PropsT ,
420- state : ThingyState
421- ): ThingyState = state.apply {
422- props.value = new
423- }
424-
425- override fun render (
426- renderProps : PropsT ,
427- renderState : ThingyState ,
428- context : RenderContext <PropsT , ThingyState , OutputT >
429- ): BackStackScreen <Screen > {
430- context.runningSideEffect(" main" ) {
431- @Suppress(" UNCHECKED_CAST" )
432- val scope = RootScopeImpl (
433- props = renderState.props as MutableStateFlow <PropsT >,
434- actionSink = context.actionSink,
435- coroutineScope = this ,
436- )
437- block(scope)
438- }
439-
440- val renderings = renderState.stack.map { frame ->
441- @Suppress(" UNCHECKED_CAST" )
442- (frame as Frame <PropsT , OutputT , * , * , * >).renderWorkflow(context)
443- }
444-
445- // TODO show a loading screen if renderings is empty.
446- return renderings.toBackStackScreen()
447- }
448-
449- override fun snapshotState (state : ThingyState ): Snapshot ? = null
450- }
451-
452473private suspend fun cancelSelf (): Nothing {
453474 val job = currentCoroutineContext().job
454475 job.cancel()
0 commit comments