@@ -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,8 @@ 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
+
45
47
@OptIn(WorkflowExperimentalApi ::class )
46
48
internal class ComposeWorkflowChildNode <PropsT , OutputT , RenderingT >(
47
49
override val id : WorkflowNodeId ,
@@ -74,7 +76,7 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
74
76
private var lastProps by mutableStateOf(initialProps)
75
77
private val saveableStateRegistry: SaveableStateRegistry
76
78
private var snapshotCache = snapshot?.childTreeSnapshots
77
- private val nodesToSnapshot = mutableVectorOf<ComposeChildNode <* , * , * >>()
79
+ private val childNodes = mutableVectorOf<ComposeChildNode <* , * , * >>()
78
80
79
81
private val outputsChannel = Channel <OutputT >(capacity = OUTPUT_QUEUE_LIMIT )
80
82
@@ -94,11 +96,7 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
94
96
* Function invoked when [onNextAction] receives an output from [outputsChannel].
95
97
*/
96
98
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()
99
+ log(" got output from channel: $output " )
102
100
103
101
val applied = ActionApplied (
104
102
output = WorkflowOutput (output),
@@ -109,7 +107,10 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
109
107
)
110
108
111
109
// Invoke the parent's handler to propagate the output up the workflow tree.
112
- emitAppliedActionToParent(applied)
110
+ log(" sending output to parent: $applied " )
111
+ emitAppliedActionToParent(applied).also {
112
+ log(" finished sending output to parent, result was: $it " )
113
+ }
113
114
}
114
115
115
116
init {
@@ -140,6 +141,7 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
140
141
workflow : Workflow <PropsT , OutputT , RenderingT >,
141
142
props : PropsT
142
143
): RenderingT {
144
+ log(" rendering workflow: props=$props " )
143
145
workflow as ComposeWorkflow
144
146
return withCompositionLocals(
145
147
LocalSaveableStateRegistry provides saveableStateRegistry,
@@ -217,7 +219,13 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
217
219
218
220
@OptIn(ExperimentalCoroutinesApi ::class , DelicateCoroutinesApi ::class )
219
221
override fun onNextAction (selector : SelectBuilder <ActionProcessingResult >): Boolean {
220
- val empty = outputsChannel.isEmpty || outputsChannel.isClosedForReceive
222
+ var empty = childNodes.fold(true ) { empty, child ->
223
+ // Do this separately so the compiler doesn't avoid it if empty is already false.
224
+ val childEmpty = child.onNextAction(selector)
225
+ empty && childEmpty
226
+ }
227
+
228
+ empty = empty && (outputsChannel.isEmpty || outputsChannel.isClosedForReceive)
221
229
with (selector) {
222
230
outputsChannel.onReceive(processOutputFromChannel)
223
231
}
@@ -285,15 +293,15 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
285
293
}
286
294
287
295
private fun addChildNode (childNode : ComposeChildNode <* , * , * >) {
288
- nodesToSnapshot + = childNode
296
+ childNodes + = childNode
289
297
}
290
298
291
299
private fun removeChildNode (childNode : ComposeChildNode <* , * , * >) {
292
- nodesToSnapshot - = childNode
300
+ childNodes - = childNode
293
301
}
294
302
295
303
private fun createChildSnapshots (): Map <WorkflowNodeId , TreeSnapshot > = buildMap {
296
- nodesToSnapshot .forEach { child ->
304
+ childNodes .forEach { child ->
297
305
put(child.id, child.snapshot())
298
306
}
299
307
}
@@ -318,19 +326,20 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
318
326
appliedActionFromChild : ActionApplied <ChildOutputT >,
319
327
onOutput : ((ChildOutputT ) -> Unit )?
320
328
): ActionProcessingResult {
329
+ log(" handling child output: $appliedActionFromChild " )
321
330
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
- }
331
+ // if (outputFromChild == null || onOutput == null) {
332
+ // // The child didn't actually emit anything or we don't care, so we don't need to
333
+ // // propagate anything to the parent. We halt the action cascade by simply returning
334
+ // // here without calling emitAppliedActionToParent.
335
+ // //
336
+ // // NOTE: SubtreeManager has an additional case for PARTIAL_TREE_RENDERING, but we
337
+ // // can just assume that using ComposeWorkflow at all implies that optimization.
338
+ // //
339
+ // // If our child state changed, we need to report that ours did too, as per the
340
+ // // comment in StatefulWorkflowNode.applyAction.
341
+ // return appliedActionFromChild.withOutput(null)
342
+ // }
334
343
335
344
// The child DID emit an output, so we need to call our handler, which will zero or
336
345
// more of two things: (1) change our state, (2) emit an output.
@@ -340,18 +349,29 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
340
349
// directly. But only for the first call to emitOutput – subsequent calls will need to
341
350
// be handled as usual.
342
351
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
352
348
- // Immediately allow any future emissions in the same onOutput call to pass through.
353
+ if (outputFromChild != null && onOutput != null ) {
354
+ onEmitOutputOverride = { output ->
355
+ // We can't know if our own state changed, so just propagate from the child.
356
+ val applied = appliedActionFromChild.withOutput(WorkflowOutput (output))
357
+ log(" handler emitted output, propagating to parent…" )
358
+ maybeParentResult = emitAppliedActionToParent(applied)
359
+
360
+ // Immediately allow any future emissions in the same onOutput call to pass through.
361
+ onEmitOutputOverride = null
362
+ }
363
+ // Ask this workflow to handle the child's output. It may write snapshot state or call
364
+ // emitOutput.
365
+ onOutput(outputFromChild.value)
349
366
onEmitOutputOverride = null
350
367
}
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
368
+
369
+ if (maybeParentResult == null ) {
370
+ // onOutput did not call emitOutput, but we need to propagate the action cascade anyway to
371
+ // check if state changed.
372
+ log(" handler did not emitOutput, propagating to parent anyway…" )
373
+ maybeParentResult = emitAppliedActionToParent(appliedActionFromChild.withOutput(null ))
374
+ }
355
375
356
376
// If maybeParentResult is not null then onOutput called emitOutput.
357
377
return maybeParentResult ? : appliedActionFromChild.withOutput(null )
0 commit comments