@@ -13,7 +13,6 @@ import androidx.compose.runtime.rememberUpdatedState
13
13
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
14
14
import androidx.compose.runtime.saveable.SaveableStateRegistry
15
15
import androidx.compose.runtime.setValue
16
- import androidx.compose.runtime.snapshots.Snapshot
17
16
import com.squareup.workflow1.ActionApplied
18
17
import com.squareup.workflow1.ActionProcessingResult
19
18
import com.squareup.workflow1.NoopWorkflowInterceptor
@@ -32,6 +31,7 @@ import com.squareup.workflow1.compose.WorkflowComposableRenderer
32
31
import com.squareup.workflow1.identifier
33
32
import com.squareup.workflow1.internal.IdCounter
34
33
import com.squareup.workflow1.internal.WorkflowNodeId
34
+ import com.squareup.workflow1.internal.compose.coroutines.requireSend
35
35
import com.squareup.workflow1.internal.createId
36
36
import com.squareup.workflow1.workflowSessionToString
37
37
import kotlinx.coroutines.CoroutineName
@@ -42,6 +42,11 @@ import kotlinx.coroutines.channels.Channel
42
42
import kotlinx.coroutines.selects.SelectBuilder
43
43
import kotlin.coroutines.CoroutineContext
44
44
45
+ private const val OUTPUT_QUEUE_LIMIT = 1_000
46
+
47
+ /* *
48
+ * Representation and implementation of a single [ComposeWorkflow].
49
+ */
45
50
@OptIn(WorkflowExperimentalApi ::class )
46
51
internal class ComposeWorkflowChildNode <PropsT , OutputT , RenderingT >(
47
52
override val id : WorkflowNodeId ,
@@ -74,7 +79,7 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
74
79
private var lastProps by mutableStateOf(initialProps)
75
80
private val saveableStateRegistry: SaveableStateRegistry
76
81
private var snapshotCache = snapshot?.childTreeSnapshots
77
- private val nodesToSnapshot = mutableVectorOf<ComposeChildNode <* , * , * >>()
82
+ private val childNodes = mutableVectorOf<ComposeChildNode <* , * , * >>()
78
83
79
84
private val outputsChannel = Channel <OutputT >(capacity = OUTPUT_QUEUE_LIMIT )
80
85
@@ -94,11 +99,7 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
94
99
* Function invoked when [onNextAction] receives an output from [outputsChannel].
95
100
*/
96
101
private val processOutputFromChannel: (OutputT ) -> ActionProcessingResult = { output ->
97
- // Ensure any state updates performed by the output sender gets to invalidate any
98
- // compositions that read them, so we can check hasInvalidations below.
99
- // If no frame has been requested yet this will request a frame. If the dispatcher is
100
- // Main.immediate the frame will be requested synchronously.
101
- Snapshot .sendApplyNotifications()
102
+ log(" got output from channel: $output " )
102
103
103
104
val applied = ActionApplied (
104
105
output = WorkflowOutput (output),
@@ -109,11 +110,15 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
109
110
)
110
111
111
112
// Invoke the parent's handler to propagate the output up the workflow tree.
112
- emitAppliedActionToParent(applied)
113
+ log(" sending output to parent: $applied " )
114
+ emitAppliedActionToParent(applied).also {
115
+ log(" finished sending output to parent, result was: $it " )
116
+ }
113
117
}
114
118
115
119
init {
116
120
interceptor.onSessionStarted(workflowScope = this , session = this )
121
+
117
122
val workflowSnapshot = snapshot?.workflowSnapshot
118
123
var restoredRegistry: SaveableStateRegistry ? = null
119
124
// Don't care about this return value, our state is separate.
@@ -140,6 +145,9 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
140
145
workflow : Workflow <PropsT , OutputT , RenderingT >,
141
146
props : PropsT
142
147
): RenderingT {
148
+ // No need to key anything on `this`, since either this is at the root of the composition, or
149
+ // inside a renderChild call and renderChild does the keying.
150
+ log(" rendering workflow: props=$props " )
143
151
workflow as ComposeWorkflow
144
152
return withCompositionLocals(
145
153
LocalSaveableStateRegistry provides saveableStateRegistry,
@@ -217,7 +225,13 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
217
225
218
226
@OptIn(ExperimentalCoroutinesApi ::class , DelicateCoroutinesApi ::class )
219
227
override fun onNextAction (selector : SelectBuilder <ActionProcessingResult >): Boolean {
220
- val empty = outputsChannel.isEmpty || outputsChannel.isClosedForReceive
228
+ var empty = childNodes.fold(true ) { empty, child ->
229
+ // Do this separately so the compiler doesn't avoid it if empty is already false.
230
+ val childEmpty = child.onNextAction(selector)
231
+ empty && childEmpty
232
+ }
233
+
234
+ empty = empty && (outputsChannel.isEmpty || outputsChannel.isClosedForReceive)
221
235
with (selector) {
222
236
outputsChannel.onReceive(processOutputFromChannel)
223
237
}
@@ -285,15 +299,15 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
285
299
}
286
300
287
301
private fun addChildNode (childNode : ComposeChildNode <* , * , * >) {
288
- nodesToSnapshot + = childNode
302
+ childNodes + = childNode
289
303
}
290
304
291
305
private fun removeChildNode (childNode : ComposeChildNode <* , * , * >) {
292
- nodesToSnapshot - = childNode
306
+ childNodes - = childNode
293
307
}
294
308
295
309
private fun createChildSnapshots (): Map <WorkflowNodeId , TreeSnapshot > = buildMap {
296
- nodesToSnapshot .forEach { child ->
310
+ childNodes .forEach { child ->
297
311
put(child.id, child.snapshot())
298
312
}
299
313
}
@@ -318,19 +332,20 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
318
332
appliedActionFromChild : ActionApplied <ChildOutputT >,
319
333
onOutput : ((ChildOutputT ) -> Unit )?
320
334
): ActionProcessingResult {
335
+ log(" handling child output: $appliedActionFromChild " )
321
336
val outputFromChild = appliedActionFromChild.output
322
- if (outputFromChild == null || onOutput == null ) {
323
- // The child didn't actually emit anything or we don't care, so we don't need to
324
- // propagate anything to the parent. We halt the action cascade by simply returning
325
- // here without calling emitAppliedActionToParent.
326
- //
327
- // NOTE: SubtreeManager has an additional case for PARTIAL_TREE_RENDERING, but we
328
- // can just assume that using ComposeWorkflow at all implies that optimization.
329
- //
330
- // If our child state changed, we need to report that ours did too, as per the
331
- // comment in StatefulWorkflowNode.applyAction.
332
- return appliedActionFromChild.withOutput(null )
333
- }
337
+ // if (outputFromChild == null || onOutput == null) {
338
+ // // The child didn't actually emit anything or we don't care, so we don't need to
339
+ // // propagate anything to the parent. We halt the action cascade by simply returning
340
+ // // here without calling emitAppliedActionToParent.
341
+ // //
342
+ // // NOTE: SubtreeManager has an additional case for PARTIAL_TREE_RENDERING, but we
343
+ // // can just assume that using ComposeWorkflow at all implies that optimization.
344
+ // //
345
+ // // If our child state changed, we need to report that ours did too, as per the
346
+ // // comment in StatefulWorkflowNode.applyAction.
347
+ // return appliedActionFromChild.withOutput(null)
348
+ // }
334
349
335
350
// The child DID emit an output, so we need to call our handler, which will zero or
336
351
// more of two things: (1) change our state, (2) emit an output.
@@ -340,18 +355,29 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
340
355
// directly. But only for the first call to emitOutput – subsequent calls will need to
341
356
// be handled as usual.
342
357
var maybeParentResult: ActionProcessingResult ? = null
343
- onEmitOutputOverride = { output ->
344
- // We can't know if our own state changed, so just propagate from the child.
345
- val applied = appliedActionFromChild.withOutput(WorkflowOutput (output))
346
- maybeParentResult = emitAppliedActionToParent(applied)
347
358
348
- // Immediately allow any future emissions in the same onOutput call to pass through.
359
+ if (outputFromChild != null && onOutput != null ) {
360
+ onEmitOutputOverride = { output ->
361
+ // We can't know if our own state changed, so just propagate from the child.
362
+ val applied = appliedActionFromChild.withOutput(WorkflowOutput (output))
363
+ log(" handler emitted output, propagating to parent…" )
364
+ maybeParentResult = emitAppliedActionToParent(applied)
365
+
366
+ // Immediately allow any future emissions in the same onOutput call to pass through.
367
+ onEmitOutputOverride = null
368
+ }
369
+ // Ask this workflow to handle the child's output. It may write snapshot state or call
370
+ // emitOutput.
371
+ onOutput(outputFromChild.value)
349
372
onEmitOutputOverride = null
350
373
}
351
- // Ask this workflow to handle the child's output. It may write snapshot state or call
352
- // emitOutput.
353
- onOutput(outputFromChild.value)
354
- onEmitOutputOverride = null
374
+
375
+ if (maybeParentResult == null ) {
376
+ // onOutput did not call emitOutput, but we need to propagate the action cascade anyway to
377
+ // check if state changed.
378
+ log(" handler did not emitOutput, propagating to parent anyway…" )
379
+ maybeParentResult = emitAppliedActionToParent(appliedActionFromChild.withOutput(null ))
380
+ }
355
381
356
382
// If maybeParentResult is not null then onOutput called emitOutput.
357
383
return maybeParentResult ? : appliedActionFromChild.withOutput(null )
0 commit comments