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

Commit 8e3b85c

Browse files
Make renderAsState public, make WorkflowContainer take a ViewEnvironment.
This narrows the scope of `WorkflowContainer` to be only for showing a workflow's renderings using a `ViewEnvironment`. This elimnates the need to have separate overloads for workflows with rendering type `ComposeRendering`, and is more idiomatic – the old `WorkflowContainer` wasn't a "container" in any Compose-y sense of the word, and it was really weird that it took a content function. Now this composable is actually a container for all workflow-related composables, and so I think the name is actually appropriate (closes #22). To render a workflow without a `ViewEnvironment`, `renderAsState` is now public. This is also much more idiomatic, as it resembles APIs like `Flow<T>.collectAsState` and `Observable.subscribeAsState`.
1 parent 240cbe7 commit 8e3b85c

File tree

3 files changed

+198
-255
lines changed

3 files changed

+198
-255
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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("NOTHING_TO_INLINE")
17+
18+
package com.squareup.workflow.ui.compose
19+
20+
import androidx.annotation.VisibleForTesting
21+
import androidx.compose.Composable
22+
import androidx.compose.Pivotal
23+
import androidx.compose.State
24+
import androidx.compose.onDispose
25+
import androidx.compose.remember
26+
import androidx.compose.state
27+
import androidx.ui.core.CoroutineContextAmbient
28+
import androidx.ui.savedinstancestate.Saver
29+
import androidx.ui.savedinstancestate.SaverScope
30+
import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient
31+
import androidx.ui.savedinstancestate.savedInstanceState
32+
import com.squareup.workflow.Snapshot
33+
import com.squareup.workflow.Workflow
34+
import com.squareup.workflow.diagnostic.WorkflowDiagnosticListener
35+
import com.squareup.workflow.launchWorkflowIn
36+
import kotlinx.coroutines.CoroutineScope
37+
import kotlinx.coroutines.Dispatchers
38+
import kotlinx.coroutines.cancel
39+
import kotlinx.coroutines.channels.Channel
40+
import kotlinx.coroutines.flow.consumeAsFlow
41+
import kotlinx.coroutines.flow.distinctUntilChanged
42+
import kotlinx.coroutines.flow.launchIn
43+
import kotlinx.coroutines.flow.onEach
44+
import okio.ByteString
45+
import kotlin.coroutines.CoroutineContext
46+
47+
/**
48+
* TODO write documentation
49+
*/
50+
@Composable
51+
fun <PropsT, OutputT : Any, RenderingT> Workflow<PropsT, OutputT, RenderingT>.renderAsState(
52+
props: PropsT,
53+
onOutput: (OutputT) -> Unit,
54+
diagnosticListener: WorkflowDiagnosticListener? = null
55+
): State<RenderingT> {
56+
@Suppress("DEPRECATION")
57+
val coroutineContext = CoroutineContextAmbient.current + Dispatchers.Main.immediate
58+
return renderAsStateImpl(this, props, onOutput, diagnosticListener, coroutineContext)
59+
}
60+
61+
/**
62+
* TODO write documentation
63+
*/
64+
@Composable
65+
inline fun <OutputT : Any, RenderingT> Workflow<Unit, OutputT, RenderingT>.renderAsState(
66+
noinline onOutput: (OutputT) -> Unit,
67+
diagnosticListener: WorkflowDiagnosticListener? = null
68+
): State<RenderingT> = renderAsState(Unit, onOutput, diagnosticListener)
69+
70+
/**
71+
* TODO write documentation
72+
*/
73+
@Composable
74+
inline fun <PropsT, RenderingT> Workflow<PropsT, Nothing, RenderingT>.renderAsState(
75+
props: PropsT,
76+
diagnosticListener: WorkflowDiagnosticListener? = null
77+
): State<RenderingT> = renderAsState(props, {}, diagnosticListener)
78+
79+
/**
80+
* TODO write documentation
81+
*/
82+
@Composable
83+
inline fun <RenderingT> Workflow<Unit, Nothing, RenderingT>.renderAsState(
84+
diagnosticListener: WorkflowDiagnosticListener? = null
85+
): State<RenderingT> = renderAsState(Unit, {}, diagnosticListener)
86+
87+
/**
88+
* @param snapshotKey Allows tests to pass in a custom key to use to save/restore the snapshot from
89+
* the [UiSavedStateRegistryAmbient]. If null, will use the default key based on source location.
90+
*/
91+
@Suppress("EXPERIMENTAL_API_USAGE")
92+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
93+
@Composable internal fun <PropsT, OutputT : Any, RenderingT> renderAsStateImpl(
94+
@Pivotal workflow: Workflow<PropsT, OutputT, RenderingT>,
95+
props: PropsT,
96+
onOutput: (OutputT) -> Unit,
97+
@Pivotal diagnosticListener: WorkflowDiagnosticListener?,
98+
@Pivotal coroutineContext: CoroutineContext,
99+
snapshotKey: String? = null
100+
): State<RenderingT> {
101+
// This can be a StateFlow once coroutines is upgraded to 1.3.6.
102+
val propsChannel = remember { Channel<PropsT>(capacity = Channel.CONFLATED) }
103+
propsChannel.offer(props)
104+
105+
// Need a mutable holder for onOutput so the outputs subscriber created in the onActive block
106+
// will always be able to see the latest value.
107+
val outputCallback = remember { OutputCallback(onOutput) }
108+
outputCallback.onOutput = onOutput
109+
110+
val renderingState = state<RenderingT?> { null }
111+
val snapshotState = savedInstanceState(key = snapshotKey, saver = SnapshotSaver) { null }
112+
113+
// We can't use onActive/on(Pre)Commit because they won't run their callback until after this
114+
// function returns, and we need to run this immediately so we get the rendering synchronously.
115+
val workflowScope = remember {
116+
val coroutineScope = CoroutineScope(coroutineContext + Dispatchers.Main.immediate)
117+
val propsFlow = propsChannel.consumeAsFlow()
118+
.distinctUntilChanged()
119+
120+
launchWorkflowIn(coroutineScope, workflow, propsFlow, snapshotState.value) { session ->
121+
session.diagnosticListener = diagnosticListener
122+
123+
// Don't call onOutput directly, since out captured reference won't be changed if the
124+
// if a different argument is passed to observeWorkflow.
125+
session.outputs.onEach { outputCallback.onOutput(it) }
126+
.launchIn(this)
127+
128+
session.renderingsAndSnapshots
129+
.onEach { (rendering, snapshot) ->
130+
renderingState.value = rendering
131+
snapshotState.value = snapshot
132+
}
133+
.launchIn(this)
134+
}
135+
136+
return@remember coroutineScope
137+
}
138+
139+
onDispose {
140+
workflowScope.cancel()
141+
}
142+
143+
// The value is guaranteed to be set before returning, so this cast is fine.
144+
@Suppress("UNCHECKED_CAST")
145+
return renderingState as State<RenderingT>
146+
}
147+
148+
private object SnapshotSaver : Saver<Snapshot?, ByteArray> {
149+
override fun SaverScope.save(value: Snapshot?): ByteArray {
150+
return value?.bytes?.toByteArray() ?: ByteArray(0)
151+
}
152+
153+
override fun restore(value: ByteArray): Snapshot? {
154+
return value.takeUnless { it.isEmpty() }
155+
?.let { bytes -> Snapshot.of(ByteString.of(*bytes)) }
156+
}
157+
}
158+
159+
private class OutputCallback<OutputT>(var onOutput: (OutputT) -> Unit)

0 commit comments

Comments
 (0)