Skip to content

Commit 739fbea

Browse files
split up into multiple files, wrote design for better state management
1 parent 1a5bdeb commit 739fbea

File tree

10 files changed

+690
-455
lines changed

10 files changed

+690
-455
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.squareup.sample.thingy
2+
3+
import com.squareup.workflow1.ui.Screen
4+
import com.squareup.workflow1.ui.navigation.BackStackScreen
5+
import com.squareup.workflow1.ui.navigation.toBackStackScreen
6+
import kotlin.coroutines.AbstractCoroutineContextElement
7+
import kotlin.coroutines.CoroutineContext
8+
9+
public fun interface BackStackFactory {
10+
11+
/**
12+
* Responsible for converting a list of [Screen]s into a [BackStackScreen]. This function *must*
13+
* handle the case where [screens] is empty, since [BackStackScreen] must always have at least
14+
* one screen. It *should* handle the case where [isTopIdle] is true, which indicates that the
15+
* top (last) screen in [screens] is doing some work that may eventually show another screen.
16+
*
17+
* @see toBackStackScreen
18+
*/
19+
fun createBackStack(
20+
screens: List<Screen>,
21+
isTopIdle: Boolean
22+
): BackStackScreen<Screen>
23+
24+
companion object {
25+
internal val ThrowOnIdle
26+
get() = showLoadingScreen {
27+
error("No BackStackFactory provided")
28+
}
29+
30+
/**
31+
* Returns a [BackStackFactory] that shows a [loading screen][createLoadingScreen] when
32+
* [BackStackWorkflow.runBackStack] has not shown anything yet or when a workflow's output
33+
* handler is idle (not showing an active screen).
34+
*/
35+
fun showLoadingScreen(
36+
name: String = "",
37+
createLoadingScreen: () -> Screen
38+
): BackStackFactory = BackStackFactory { screens, isTopIdle ->
39+
val mutableScreens = screens.toMutableList()
40+
if (mutableScreens.isEmpty() || isTopIdle) {
41+
mutableScreens += createLoadingScreen()
42+
}
43+
mutableScreens.toBackStackScreen(name)
44+
}
45+
}
46+
}
47+
48+
/**
49+
* Returns a [CoroutineContext.Element] that will store this [BackStackFactory] in a
50+
* [CoroutineContext] to later be retrieved by [backStackFactory].
51+
*/
52+
public fun BackStackFactory.asContextElement(): CoroutineContext.Element =
53+
BackStackFactoryContextElement(this)
54+
55+
/**
56+
* Looks for a [BackStackFactory] stored the current context via [asContextElement].
57+
*/
58+
public val CoroutineContext.backStackFactory: BackStackFactory?
59+
get() = this[BackStackFactoryContextElement]?.factory
60+
61+
private class BackStackFactoryContextElement(
62+
val factory: BackStackFactory
63+
) : AbstractCoroutineContextElement(Key) {
64+
companion object Key : CoroutineContext.Key<BackStackFactoryContextElement>
65+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.squareup.sample.thingy
2+
3+
import com.squareup.workflow1.Sink
4+
import com.squareup.workflow1.StatefulWorkflow.RenderContext
5+
import com.squareup.workflow1.Workflow
6+
import com.squareup.workflow1.WorkflowAction
7+
import com.squareup.workflow1.WorkflowAction.Companion
8+
import com.squareup.workflow1.ui.Screen
9+
import kotlinx.coroutines.CompletableDeferred
10+
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
12+
import kotlinx.coroutines.Job
13+
import kotlinx.coroutines.cancel
14+
import kotlinx.coroutines.currentCoroutineContext
15+
import kotlinx.coroutines.ensureActive
16+
import kotlinx.coroutines.job
17+
import kotlinx.coroutines.launch
18+
19+
internal sealed interface BackStackFrame<R> {
20+
fun cancelCaller()
21+
suspend fun awaitResult(): R
22+
suspend fun cancelSelf(): Nothing
23+
fun cancel()
24+
}
25+
26+
/**
27+
* Represents a call to [BackStackScope.showWorkflow].
28+
*/
29+
internal class WorkflowFrame<PropsT, OutputT, ChildPropsT, ChildOutputT, R> private constructor(
30+
private val workflow: Workflow<ChildPropsT, ChildOutputT, Screen>,
31+
private val props: ChildPropsT,
32+
private val callerJob: Job,
33+
private val frameScope: CoroutineScope,
34+
private val onOutput: suspend BackStackWorkflowScope.(ChildOutputT) -> R,
35+
private val actionSink: Sink<WorkflowAction<PropsT, BackStackState, OutputT>>,
36+
private val parent: BackStackFrame<*>?,
37+
private val result: CompletableDeferred<R>,
38+
) : BackStackFrame<R> {
39+
40+
constructor(
41+
workflow: Workflow<ChildPropsT, ChildOutputT, Screen>,
42+
initialProps: ChildPropsT,
43+
callerJob: Job,
44+
frameScope: CoroutineScope,
45+
onOutput: suspend BackStackWorkflowScope.(ChildOutputT) -> R,
46+
actionSink: Sink<WorkflowAction<PropsT, BackStackState, OutputT>>,
47+
parent: BackStackFrame<*>?,
48+
) : this(
49+
workflow = workflow,
50+
props = initialProps,
51+
callerJob = callerJob,
52+
frameScope = frameScope,
53+
onOutput = onOutput,
54+
actionSink = actionSink,
55+
parent = parent,
56+
result = CompletableDeferred(parent = frameScope.coroutineContext.job)
57+
)
58+
59+
fun copy(
60+
props: ChildPropsT = this.props,
61+
): WorkflowFrame<PropsT, OutputT, ChildPropsT, ChildOutputT, R> = WorkflowFrame(
62+
workflow = workflow,
63+
props = props,
64+
callerJob = callerJob,
65+
frameScope = frameScope,
66+
onOutput = onOutput,
67+
actionSink = actionSink,
68+
parent = parent,
69+
result = result
70+
)
71+
72+
override suspend fun awaitResult(): R = result.await()
73+
74+
override fun cancelCaller() {
75+
callerJob.cancel()
76+
}
77+
78+
private suspend fun finishWith(value: R): Nothing {
79+
result.complete(value)
80+
cancelSelf()
81+
}
82+
83+
override suspend fun cancelSelf(): Nothing {
84+
cancel()
85+
val currentContext = currentCoroutineContext()
86+
currentContext.cancel()
87+
currentContext.ensureActive()
88+
error("Nonsense")
89+
}
90+
91+
override fun cancel() {
92+
frameScope.cancel()
93+
}
94+
95+
fun renderWorkflow(
96+
context: RenderContext<PropsT, BackStackState, OutputT>
97+
): Screen = context.renderChild(
98+
child = workflow,
99+
props = props,
100+
handler = ::onOutput
101+
)
102+
103+
private fun onOutput(output: ChildOutputT): WorkflowAction<PropsT, BackStackState, OutputT> {
104+
var canAcceptAction = true
105+
var action: WorkflowAction<PropsT, BackStackState, OutputT>? = null
106+
val sink = object : Sink<WorkflowAction<PropsT, BackStackState, OutputT>> {
107+
override fun send(value: WorkflowAction<PropsT, BackStackState, OutputT>) {
108+
val sendToSink = synchronized(result) {
109+
if (canAcceptAction) {
110+
action = value
111+
canAcceptAction = false
112+
false
113+
} else {
114+
true
115+
}
116+
}
117+
if (sendToSink) {
118+
actionSink.send(value)
119+
}
120+
}
121+
}
122+
123+
// Run synchronously until first suspension point since in many cases it will immediately
124+
// either call showWorkflow, finishWith, or goBack, and so then we can just return that action
125+
// immediately instead of needing a whole separate render pass.
126+
frameScope.launch(start = UNDISPATCHED) {
127+
val showScope = BackStackWorkflowScopeImpl(
128+
actionSink = sink,
129+
coroutineScope = this,
130+
thisFrame = this@WorkflowFrame,
131+
parentFrame = parent
132+
)
133+
finishWith(onOutput(showScope, output))
134+
}
135+
// TODO collect WorkflowAction
136+
137+
// Once the coroutine has suspended, all sends must go to the real sink.
138+
return synchronized(result) {
139+
canAcceptAction = false
140+
action ?: WorkflowAction.noAction()
141+
}
142+
}
143+
}
144+
145+
/**
146+
* Represents a call to [BackStackScope.showScreen].
147+
*/
148+
internal class ScreenFrame<OutputT, R>(
149+
private val callerJob: Job,
150+
private val frameScope: CoroutineScope,
151+
private val actionSink: Sink<WorkflowAction<Any?, BackStackState, OutputT>>,
152+
private val parent: BackStackFrame<*>?,
153+
) : BackStackFrame<R> {
154+
private val result = CompletableDeferred<R>()
155+
156+
lateinit var screen: Screen
157+
private set
158+
159+
fun initScreen(screenFactory: BackStackScreenScope<R>.() -> Screen) {
160+
val factoryScope = BackStackScreenScopeImpl<Any?, OutputT, R>(
161+
actionSink = actionSink,
162+
coroutineScope = frameScope,
163+
thisFrame = this,
164+
parentFrame = parent
165+
)
166+
screen = screenFactory(factoryScope)
167+
}
168+
169+
override suspend fun awaitResult(): R = result.await()
170+
171+
override fun cancelCaller() {
172+
callerJob.cancel()
173+
}
174+
175+
fun continueWith(value: R) {
176+
result.complete(value)
177+
cancel()
178+
}
179+
180+
override suspend fun cancelSelf(): Nothing {
181+
cancel()
182+
val currentContext = currentCoroutineContext()
183+
currentContext.cancel()
184+
currentContext.ensureActive()
185+
error("Nonsense")
186+
}
187+
188+
override fun cancel() {
189+
frameScope.cancel()
190+
}
191+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.squareup.sample.thingy
2+
3+
import com.squareup.workflow1.Sink
4+
import com.squareup.workflow1.Workflow
5+
import com.squareup.workflow1.WorkflowAction
6+
import com.squareup.workflow1.ui.Screen
7+
import kotlinx.coroutines.CoroutineScope
8+
import kotlinx.coroutines.flow.Flow
9+
10+
internal class BackStackScopeImpl<OutputT>(
11+
coroutineScope: CoroutineScope,
12+
) : BackStackScope, CoroutineScope by coroutineScope {
13+
// TODO set this
14+
lateinit var actionSink: Sink<WorkflowAction<Any?, BackStackState, OutputT>>
15+
16+
override suspend fun <ChildPropsT, ChildOutputT, R> showWorkflow(
17+
workflow: Workflow<ChildPropsT, ChildOutputT, Screen>,
18+
props: Flow<ChildPropsT>,
19+
onOutput: suspend BackStackWorkflowScope.(ChildOutputT) -> R
20+
): R = showWorkflowImpl(
21+
workflow = workflow,
22+
props = props,
23+
onOutput = onOutput,
24+
actionSink = actionSink,
25+
parentFrame = null
26+
)
27+
28+
override suspend fun <R> showScreen(
29+
screenFactory: BackStackScreenScope<R>.() -> Screen
30+
): R = showScreenImpl(
31+
screenFactory = screenFactory,
32+
actionSink = actionSink,
33+
parentFrame = null
34+
)
35+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.squareup.sample.thingy
2+
3+
import com.squareup.workflow1.Sink
4+
import com.squareup.workflow1.Workflow
5+
import com.squareup.workflow1.WorkflowAction
6+
import com.squareup.workflow1.action
7+
import com.squareup.workflow1.ui.Screen
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.flow.Flow
10+
11+
internal class BackStackScreenScopeImpl<PropsT, OutputT, R>(
12+
private val actionSink: Sink<WorkflowAction<PropsT, BackStackState, OutputT>>,
13+
coroutineScope: CoroutineScope,
14+
private val thisFrame: ScreenFrame<OutputT, R>,
15+
private val parentFrame: BackStackFrame<*>?,
16+
) : BackStackScreenScope<R>, CoroutineScope by coroutineScope {
17+
18+
override suspend fun <ChildPropsT, ChildOutputT, R> showWorkflow(
19+
workflow: Workflow<ChildPropsT, ChildOutputT, Screen>,
20+
props: Flow<ChildPropsT>,
21+
onOutput: suspend BackStackWorkflowScope.(ChildOutputT) -> R
22+
): R = showWorkflowImpl(
23+
workflow = workflow,
24+
props = props,
25+
onOutput = onOutput,
26+
actionSink = actionSink,
27+
parentFrame = thisFrame,
28+
)
29+
30+
@Suppress("UNCHECKED_CAST")
31+
override suspend fun <R> showScreen(
32+
screenFactory: BackStackScreenScope<R>.() -> Screen
33+
): R = showScreenImpl(
34+
screenFactory = screenFactory,
35+
actionSink = actionSink as Sink<WorkflowAction<Any?, BackStackState, OutputT>>,
36+
parentFrame = thisFrame,
37+
)
38+
39+
override fun continueWith(value: R) {
40+
thisFrame.continueWith(value)
41+
}
42+
43+
override fun cancelScreen() {
44+
// If parent is null, goBack will not be exposed and will never be called.
45+
val parent = checkNotNull(parentFrame) { "goBack called on root scope" }
46+
actionSink.send(action("popTo") {
47+
state = state.popToFrame(parent)
48+
})
49+
thisFrame.cancel()
50+
}
51+
}

0 commit comments

Comments
 (0)