Skip to content
This repository was archived by the owner on Feb 5, 2021. It is now read-only.

Commit e58d34d

Browse files
Introduce WorkflowContainer for running a workflow inside a Compose app.
This is the third flavor of integration, it replaces `setContentWorkflow`, `WorkflowLayout`, `WorkflowRunnerViewModel`, etc., and makes it really easy to run a `Workflow` inside a pure Compose app. It's compatible with `ViewEnvironment`/`ViewRegistry`, but doesn't require it – it gives you the root rendering, and you can do whatever you want with it. It also supports running root `ComposeWorkflow`s directly, since they are self-rendering.
1 parent 039f1c8 commit e58d34d

File tree

13 files changed

+658
-1
lines changed

13 files changed

+658
-1
lines changed

buildSrc/src/main/java/Dependencies.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ object Dependencies {
3737
const val foundation = "androidx.ui:ui-foundation:${Versions.compose}"
3838
const val layout = "androidx.ui:ui-layout:${Versions.compose}"
3939
const val material = "androidx.ui:ui-material:${Versions.compose}"
40+
const val savedstate = "androidx.ui:ui-saved-instance-state:${Versions.compose}"
4041
const val test = "androidx.ui:ui-test:${Versions.compose}"
4142
const val tooling = "androidx.ui:ui-tooling:${Versions.compose}"
4243
}

core-compose/api/core-compose.api

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ public final class com/squareup/workflow/ui/compose/ViewEnvironmentsKt {
5151
public static synthetic fun showRendering$default (Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Landroidx/compose/Composer;ILjava/lang/Object;)V
5252
}
5353

54+
public final class com/squareup/workflow/ui/compose/WorkflowContainerKt {
55+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
56+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
57+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
58+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
59+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;)V
60+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
61+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
62+
public static final fun WorkflowContainer (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;)V
63+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
64+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
65+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
66+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
67+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lcom/squareup/workflow/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Landroidx/compose/Composer;ILjava/lang/Object;)V
68+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
69+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
70+
public static synthetic fun WorkflowContainer$default (Lcom/squareup/workflow/Workflow;Lkotlin/jvm/functions/Function1;Landroidx/ui/core/Modifier;Lcom/squareup/workflow/diagnostic/WorkflowDiagnosticListener;Lkotlin/jvm/functions/Function2;Landroidx/compose/Composer;ILjava/lang/Object;)V
71+
}
72+
5473
public final class com/squareup/workflow/ui/core/compose/BuildConfig {
5574
public static final field BUILD_TYPE Ljava/lang/String;
5675
public static final field DEBUG Z

core-compose/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,6 @@ dependencies {
4040

4141
implementation(Dependencies.Compose.foundation)
4242
implementation(Dependencies.Compose.layout)
43-
implementation(Dependencies.Compose.tooling)
43+
implementation(Dependencies.Compose.savedstate)
4444
implementation(Dependencies.Workflow.runtime)
4545
}
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/*
2+
* Copyright 2020 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
@file:Suppress(
17+
"EXPERIMENTAL_API_USAGE",
18+
"RemoveEmptyParenthesesFromAnnotationEntry",
19+
"FunctionNaming"
20+
)
21+
22+
package com.squareup.workflow.ui.compose
23+
24+
import androidx.compose.Composable
25+
import androidx.compose.Direct
26+
import androidx.compose.Pivotal
27+
import androidx.compose.onDispose
28+
import androidx.compose.remember
29+
import androidx.compose.state
30+
import androidx.ui.core.CoroutineContextAmbient
31+
import androidx.ui.core.Modifier
32+
import androidx.ui.foundation.Box
33+
import androidx.ui.savedinstancestate.Saver
34+
import androidx.ui.savedinstancestate.SaverScope
35+
import androidx.ui.savedinstancestate.savedInstanceState
36+
import com.squareup.workflow.Snapshot
37+
import com.squareup.workflow.Workflow
38+
import com.squareup.workflow.compose.ComposeRendering
39+
import com.squareup.workflow.diagnostic.WorkflowDiagnosticListener
40+
import com.squareup.workflow.launchWorkflowIn
41+
import com.squareup.workflow.ui.ViewEnvironment
42+
import kotlinx.coroutines.CoroutineScope
43+
import kotlinx.coroutines.Dispatchers
44+
import kotlinx.coroutines.cancel
45+
import kotlinx.coroutines.channels.Channel
46+
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
47+
import kotlinx.coroutines.flow.consumeAsFlow
48+
import kotlinx.coroutines.flow.distinctUntilChanged
49+
import kotlinx.coroutines.flow.launchIn
50+
import kotlinx.coroutines.flow.onEach
51+
import okio.ByteString
52+
import kotlin.coroutines.CoroutineContext
53+
54+
// TODO this is probably WAY too many overloads
55+
56+
/**
57+
* Render a [Workflow]'s renderings.
58+
*
59+
* When this function is first composed it will start a new runtime. This runtime will be restarted
60+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
61+
* changes. The runtime will be cancelled when this function stops composing.
62+
*
63+
* @param workflow The [Workflow] to render.
64+
* @param props The props to render the root workflow with. If this value changes between calls,
65+
* the workflow runtime will re-render with the new props.
66+
* @param onOutput A function that will be invoked any time the root workflow emits an output.
67+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
68+
* @param content A [Composable] function that gets executed every time the root workflow spits
69+
* out a new rendering.
70+
*/
71+
@Composable fun <PropsT, OutputT : Any, RenderingT> WorkflowContainer(
72+
workflow: Workflow<PropsT, OutputT, RenderingT>,
73+
props: PropsT,
74+
onOutput: (OutputT) -> Unit,
75+
modifier: Modifier = Modifier,
76+
diagnosticListener: WorkflowDiagnosticListener? = null,
77+
content: @Composable() (rendering: RenderingT) -> Unit
78+
) {
79+
@Suppress("DEPRECATION")
80+
val rendering =
81+
observeWorkflow(workflow, props, onOutput, CoroutineContextAmbient.current, diagnosticListener)
82+
83+
Box(modifier = modifier) {
84+
content(rendering)
85+
}
86+
}
87+
88+
/**
89+
* Render a [Workflow]'s renderings.
90+
*
91+
* When this function is first composed it will start a new runtime. This runtime will be restarted
92+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
93+
* changes. The runtime will be cancelled when this function stops composing.
94+
*
95+
* @param workflow The [Workflow] to render.
96+
* @param onOutput A function that will be invoked any time the root workflow emits an output.
97+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
98+
* @param content A [Composable] function that gets executed every time the root workflow spits
99+
* out a new rendering.
100+
*/
101+
@Direct
102+
@Composable fun <OutputT : Any, RenderingT> WorkflowContainer(
103+
workflow: Workflow<Unit, OutputT, RenderingT>,
104+
onOutput: (OutputT) -> Unit,
105+
modifier: Modifier = Modifier,
106+
diagnosticListener: WorkflowDiagnosticListener? = null,
107+
content: @Composable() (rendering: RenderingT) -> Unit
108+
) {
109+
WorkflowContainer(workflow, Unit, onOutput, modifier, diagnosticListener, content)
110+
}
111+
112+
/**
113+
* Render a [Workflow]'s renderings.
114+
*
115+
* When this function is first composed it will start a new runtime. This runtime will be restarted
116+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
117+
* changes. The runtime will be cancelled when this function stops composing.
118+
*
119+
* @param workflow The [Workflow] to render.
120+
* @param props The props to render the root workflow with. If this value changes between calls,
121+
* the workflow runtime will re-render with the new props.
122+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
123+
* @param content A [Composable] function that gets executed every time the root workflow spits
124+
* out a new rendering.
125+
*/
126+
@Direct
127+
@Composable fun <PropsT, RenderingT> WorkflowContainer(
128+
workflow: Workflow<PropsT, Nothing, RenderingT>,
129+
props: PropsT,
130+
modifier: Modifier = Modifier,
131+
diagnosticListener: WorkflowDiagnosticListener? = null,
132+
content: @Composable() (rendering: RenderingT) -> Unit
133+
) {
134+
WorkflowContainer(workflow, props, {}, modifier, diagnosticListener, content)
135+
}
136+
137+
/**
138+
* Render a [Workflow]'s renderings.
139+
*
140+
* When this function is first composed it will start a new runtime. This runtime will be restarted
141+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
142+
* changes. The runtime will be cancelled when this function stops composing.
143+
*
144+
* @param workflow The [Workflow] to render.
145+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
146+
* @param content A [Composable] function that gets executed every time the root workflow spits
147+
* out a new rendering.
148+
*/
149+
@Direct
150+
@Composable fun <RenderingT> WorkflowContainer(
151+
workflow: Workflow<Unit, Nothing, RenderingT>,
152+
modifier: Modifier = Modifier,
153+
diagnosticListener: WorkflowDiagnosticListener? = null,
154+
content: @Composable() (rendering: RenderingT) -> Unit
155+
) {
156+
WorkflowContainer(workflow, Unit, {}, modifier, diagnosticListener, content)
157+
}
158+
159+
/**
160+
* Render a [Workflow]'s renderings.
161+
*
162+
* When this function is first composed it will start a new runtime. This runtime will be restarted
163+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
164+
* changes. The runtime will be cancelled when this function stops composing.
165+
*
166+
* @param workflow The [Workflow] to render.
167+
* @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by
168+
* the workflow.
169+
* @param props The props to render the root workflow with. If this value changes between calls,
170+
* the workflow runtime will re-render with the new props.
171+
* @param onOutput A function that will be invoked any time the root workflow emits an output.
172+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
173+
*/
174+
@Composable fun <PropsT, OutputT : Any> WorkflowContainer(
175+
workflow: Workflow<PropsT, OutputT, ComposeRendering>,
176+
viewEnvironment: ViewEnvironment,
177+
props: PropsT,
178+
onOutput: (OutputT) -> Unit,
179+
modifier: Modifier = Modifier,
180+
diagnosticListener: WorkflowDiagnosticListener? = null
181+
) {
182+
WorkflowContainer(workflow, props, onOutput, modifier, diagnosticListener) { rendering ->
183+
rendering.render(viewEnvironment)
184+
}
185+
}
186+
187+
/**
188+
* Render a [Workflow]'s renderings.
189+
*
190+
* When this function is first composed it will start a new runtime. This runtime will be restarted
191+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
192+
* changes. The runtime will be cancelled when this function stops composing.
193+
*
194+
* @param workflow The [Workflow] to render.
195+
* @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by
196+
* the workflow.
197+
* @param props The props to render the root workflow with. If this value changes between calls,
198+
* the workflow runtime will re-render with the new props.
199+
* @param onOutput A function that will be invoked any time the root workflow emits an output.
200+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
201+
*/
202+
@Direct
203+
@Composable fun <OutputT : Any> WorkflowContainer(
204+
workflow: Workflow<Unit, OutputT, ComposeRendering>,
205+
viewEnvironment: ViewEnvironment,
206+
onOutput: (OutputT) -> Unit,
207+
modifier: Modifier = Modifier,
208+
diagnosticListener: WorkflowDiagnosticListener? = null
209+
) {
210+
WorkflowContainer(workflow, viewEnvironment, Unit, onOutput, modifier, diagnosticListener)
211+
}
212+
213+
/**
214+
* Render a [Workflow]'s renderings.
215+
*
216+
* When this function is first composed it will start a new runtime. This runtime will be restarted
217+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
218+
* changes. The runtime will be cancelled when this function stops composing.
219+
*
220+
* @param workflow The [Workflow] to render.
221+
* @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by
222+
* the workflow.
223+
* @param props The props to render the root workflow with. If this value changes between calls,
224+
* the workflow runtime will re-render with the new props.
225+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
226+
*/
227+
@Direct
228+
@Composable fun <PropsT> WorkflowContainer(
229+
workflow: Workflow<PropsT, Nothing, ComposeRendering>,
230+
viewEnvironment: ViewEnvironment,
231+
props: PropsT,
232+
modifier: Modifier = Modifier,
233+
diagnosticListener: WorkflowDiagnosticListener? = null
234+
) {
235+
WorkflowContainer(workflow, viewEnvironment, props, {}, modifier, diagnosticListener)
236+
}
237+
238+
/**
239+
* Render a [Workflow]'s renderings.
240+
*
241+
* When this function is first composed it will start a new runtime. This runtime will be restarted
242+
* any time [workflow], [diagnosticListener], or the `CoroutineContext`
243+
* changes. The runtime will be cancelled when this function stops composing.
244+
*
245+
* @param workflow The [Workflow] to render.
246+
* @param viewEnvironment The [ViewEnvironment] used to show the [ComposeRendering]s emitted by
247+
* the workflow.
248+
* @param diagnosticListener A [WorkflowDiagnosticListener] to configure on the runtime.
249+
*/
250+
@Direct
251+
@Composable fun WorkflowContainer(
252+
workflow: Workflow<Unit, Nothing, ComposeRendering>,
253+
viewEnvironment: ViewEnvironment,
254+
modifier: Modifier = Modifier,
255+
diagnosticListener: WorkflowDiagnosticListener? = null
256+
) {
257+
WorkflowContainer(workflow, viewEnvironment, Unit, {}, modifier, diagnosticListener)
258+
}
259+
260+
@Composable private fun <PropsT, OutputT : Any, RenderingT> observeWorkflow(
261+
@Pivotal workflow: Workflow<PropsT, OutputT, RenderingT>,
262+
props: PropsT,
263+
onOutput: (OutputT) -> Unit,
264+
@Pivotal coroutineContext: CoroutineContext,
265+
@Pivotal diagnosticListener: WorkflowDiagnosticListener? = null
266+
): RenderingT {
267+
// This can be a StateFlow once coroutines is upgraded to 1.3.6.
268+
val propsChannel = remember { Channel<PropsT>(capacity = CONFLATED) }
269+
propsChannel.offer(props)
270+
271+
// Need a mutable holder for onOutput so the outputs subscriber created in the onActive block
272+
// will always be able to see the latest value.
273+
val outputCallback = remember { OutputCallback(onOutput) }
274+
outputCallback.onOutput = onOutput
275+
276+
val renderingState = state<RenderingT?> { null }
277+
val snapshotState = savedInstanceState(saver = SnapshotSaver) { null }
278+
279+
// We can't use onActive/on(Pre)Commit because they won't run their callback until after this
280+
// function returns, and we need to run this immediately so we get the rendering synchronously.
281+
val workflowScope = remember {
282+
val coroutineScope = CoroutineScope(coroutineContext + Dispatchers.Main.immediate)
283+
val propsFlow = propsChannel.consumeAsFlow()
284+
.distinctUntilChanged()
285+
286+
launchWorkflowIn(coroutineScope, workflow, propsFlow, snapshotState.value) { session ->
287+
session.diagnosticListener = diagnosticListener
288+
289+
// Don't call onOutput directly, since out captured reference won't be changed if the
290+
// if a different argument is passed to observeWorkflow.
291+
session.outputs.onEach { outputCallback.onOutput(it) }
292+
.launchIn(this)
293+
294+
session.renderingsAndSnapshots
295+
.onEach { (rendering, snapshot) ->
296+
renderingState.value = rendering
297+
snapshotState.value = snapshot
298+
}
299+
.launchIn(this)
300+
}
301+
302+
return@remember coroutineScope
303+
}
304+
305+
onDispose {
306+
workflowScope.cancel()
307+
}
308+
309+
return renderingState.value!!
310+
}
311+
312+
private object SnapshotSaver : Saver<Snapshot?, ByteArray> {
313+
override fun SaverScope.save(value: Snapshot?): ByteArray {
314+
return value?.bytes?.toByteArray() ?: ByteArray(0)
315+
}
316+
317+
override fun restore(value: ByteArray): Snapshot? {
318+
return value.takeUnless { it.isEmpty() }
319+
?.let { bytes -> Snapshot.of(ByteString.of(*bytes)) }
320+
}
321+
}
322+
323+
private class OutputCallback<OutputT>(var onOutput: (OutputT) -> Unit)

0 commit comments

Comments
 (0)