Skip to content

Commit 159aae8

Browse files
Try harder to get a frame from the recomposer on render, regardless of the underlying dispatcher.
1 parent c327c61 commit 159aae8

File tree

9 files changed

+632
-199
lines changed

9 files changed

+632
-199
lines changed

samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveViewFactory.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
package com.squareup.sample.compose.nestedrenderings
44

5+
import androidx.compose.animation.Animatable
6+
import androidx.compose.animation.core.FastOutLinearInEasing
7+
import androidx.compose.animation.core.LinearOutSlowInEasing
8+
import androidx.compose.animation.core.keyframes
9+
import androidx.compose.foundation.gestures.awaitEachGesture
10+
import androidx.compose.foundation.gestures.detectTapGestures
11+
import androidx.compose.foundation.interaction.MutableInteractionSource
12+
import androidx.compose.foundation.interaction.PressInteraction
513
import androidx.compose.foundation.layout.Arrangement.SpaceEvenly
614
import androidx.compose.foundation.layout.Column
715
import androidx.compose.foundation.layout.Row
@@ -13,12 +21,17 @@ import androidx.compose.material.Card
1321
import androidx.compose.material.Text
1422
import androidx.compose.runtime.Composable
1523
import androidx.compose.runtime.CompositionLocalProvider
24+
import androidx.compose.runtime.LaunchedEffect
1625
import androidx.compose.runtime.compositionLocalOf
26+
import androidx.compose.runtime.getValue
27+
import androidx.compose.runtime.mutableIntStateOf
1728
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.setValue
1830
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
1931
import androidx.compose.ui.Modifier
2032
import androidx.compose.ui.graphics.Color
2133
import androidx.compose.ui.graphics.compositeOver
34+
import androidx.compose.ui.input.pointer.pointerInput
2235
import androidx.compose.ui.res.dimensionResource
2336
import androidx.compose.ui.tooling.preview.Preview
2437
import com.squareup.sample.compose.R
@@ -27,6 +40,7 @@ import com.squareup.workflow1.ui.Screen
2740
import com.squareup.workflow1.ui.compose.ScreenComposableFactory
2841
import com.squareup.workflow1.ui.compose.WorkflowRendering
2942
import com.squareup.workflow1.ui.compose.tooling.Preview
43+
import kotlin.time.DurationUnit.MILLISECONDS
3044

3145
/**
3246
* Composition local of [Color] to use as the background color for a [RecursiveComposableFactory].
@@ -44,7 +58,26 @@ val RecursiveComposableFactory = ScreenComposableFactory<Rendering> { rendering
4458
.compositeOver(Color.Black)
4559
}
4660

47-
Card(backgroundColor = color) {
61+
var lastFlashedTrigger by remember { mutableIntStateOf(rendering.flashTrigger) }
62+
val flashAlpha = remember { Animatable(Color(0x00FFFFFF)) }
63+
64+
// Flash the card white when asked.
65+
LaunchedEffect(rendering.flashTrigger) {
66+
if (rendering.flashTrigger != 0) {
67+
lastFlashedTrigger = rendering.flashTrigger
68+
flashAlpha.animateTo(Color(0x00FFFFFF), animationSpec = keyframes {
69+
Color.White at (rendering.flashTime / 7).toInt(MILLISECONDS) using FastOutLinearInEasing
70+
Color(0x00FFFFFF) at rendering.flashTime.toInt(MILLISECONDS) using LinearOutSlowInEasing
71+
})
72+
}
73+
}
74+
75+
Card(
76+
backgroundColor = flashAlpha.value.compositeOver(color),
77+
modifier = Modifier.pointerInput(rendering) {
78+
detectTapGestures(onPress = { rendering.onSelfClicked() })
79+
}
80+
) {
4881
Column(
4982
Modifier
5083
.padding(dimensionResource(R.dimen.recursive_padding))
@@ -76,10 +109,14 @@ fun RecursiveViewFactoryPreview() {
76109
StringRendering("foo"),
77110
Rendering(
78111
children = listOf(StringRendering("bar")),
112+
flashTrigger = 0,
113+
onSelfClicked = {},
79114
onAddChildClicked = {},
80115
onResetClicked = {}
81116
)
82117
),
118+
flashTrigger = 0,
119+
onSelfClicked = {},
83120
onAddChildClicked = {},
84121
onResetClicked = {}
85122
),

samples/compose-samples/src/main/java/com/squareup/sample/compose/nestedrenderings/RecursiveWorkflow.kt

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package com.squareup.sample.compose.nestedrenderings
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.DisposableEffect
5+
import androidx.compose.runtime.LaunchedEffect
46
import androidx.compose.runtime.getValue
7+
import androidx.compose.runtime.mutableIntStateOf
58
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.remember
10+
import androidx.compose.runtime.rememberCoroutineScope
611
import androidx.compose.runtime.saveable.rememberSaveable
712
import androidx.compose.runtime.setValue
813
import com.squareup.sample.compose.databinding.LegacyViewBinding
@@ -14,6 +19,12 @@ import com.squareup.workflow1.compose.renderChild
1419
import com.squareup.workflow1.ui.AndroidScreen
1520
import com.squareup.workflow1.ui.Screen
1621
import com.squareup.workflow1.ui.ScreenViewFactory
22+
import kotlinx.coroutines.CoroutineDispatcher
23+
import kotlinx.coroutines.delay
24+
import kotlinx.coroutines.launch
25+
import kotlin.time.Duration
26+
import kotlin.time.Duration.Companion.ZERO
27+
import kotlin.time.Duration.Companion.seconds
1728

1829
/**
1930
* A simple workflow that produces [Rendering]s of zero or more children.
@@ -24,7 +35,7 @@ import com.squareup.workflow1.ui.ScreenViewFactory
2435
* through Composable renderings as well as adapting in both directions.
2536
*/
2637
@OptIn(WorkflowExperimentalApi::class)
27-
object RecursiveWorkflow : ComposeWorkflow<Unit, Nothing, Screen>() {
38+
object RecursiveWorkflow : ComposeWorkflow<Unit, Unit, Screen>() {
2839

2940
/**
3041
* A rendering from a [RecursiveWorkflow].
@@ -35,8 +46,11 @@ object RecursiveWorkflow : ComposeWorkflow<Unit, Nothing, Screen>() {
3546
*/
3647
data class Rendering(
3748
val children: List<Screen>,
38-
val onAddChildClicked: () -> Unit,
39-
val onResetClicked: () -> Unit
49+
val flashTrigger: Int = 0,
50+
val flashTime: Duration = ZERO,
51+
val onSelfClicked: () -> Unit = {},
52+
val onAddChildClicked: () -> Unit = {},
53+
val onResetClicked: () -> Unit = {}
4054
) : Screen
4155

4256
/**
@@ -51,16 +65,43 @@ object RecursiveWorkflow : ComposeWorkflow<Unit, Nothing, Screen>() {
5165
)
5266
}
5367

68+
@OptIn(ExperimentalStdlibApi::class)
5469
@Composable override fun produceRendering(
5570
props: Unit,
56-
emitOutput: (Nothing) -> Unit
71+
emitOutput: (Unit) -> Unit
5772
): Screen {
5873
var children by rememberSaveable { mutableStateOf(0) }
74+
var flashTrigger by remember { mutableIntStateOf(0) }
75+
val coroutineScope = rememberCoroutineScope()
76+
77+
DisposableEffect(Unit) {
78+
println("OMG coroutineScope dispatcher: ${coroutineScope.coroutineContext[CoroutineDispatcher]}")
79+
onDispose {}
80+
}
81+
82+
LaunchedEffect(Unit) {
83+
println("OMG LaunchedEffect dispatcher: ${coroutineScope.coroutineContext[CoroutineDispatcher]}")
84+
}
85+
5986
return Rendering(
6087
children = List(children) { i ->
61-
val child = renderChild(RecursiveWorkflow)
88+
val child = renderChild(RecursiveWorkflow, onOutput = {
89+
// When a child is clicked, cascade the flash up.
90+
coroutineScope.launch {
91+
delay(0.1.seconds)
92+
flashTrigger++
93+
emitOutput(Unit)
94+
}
95+
})
6296
if (i % 2 == 0) child else LegacyRendering(child)
6397
},
98+
flashTrigger = flashTrigger,
99+
flashTime = 0.5.seconds,
100+
// Trigger a cascade of flashes when clicked.
101+
onSelfClicked = {
102+
flashTrigger++
103+
emitOutput(Unit)
104+
},
64105
onAddChildClicked = { children++ },
65106
onResetClicked = { children = 0 }
66107
)

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/SimpleLoggingWorkflowInterceptor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ public open class SimpleLoggingWorkflowInterceptor : WorkflowInterceptor {
8585
props: PropsT,
8686
onOutput: ((OutputT) -> Unit)?
8787
): RenderingT {
88-
logMethod("onRenderChild", session, "workflow" to childWorkflow, "props" to props) {
89-
return childRenderer.renderChild(childWorkflow, props, onOutput = { output ->
88+
return logMethod("onRenderChild", session, "workflow" to childWorkflow, "props" to props) {
89+
childRenderer.renderChild(childWorkflow, props, onOutput = { output ->
9090
logMethod("onOutput", session, "output" to output) {
9191
onOutput?.invoke(output)
9292
}

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/compose/ComposeWorkflowChildNode.kt

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import androidx.compose.runtime.rememberUpdatedState
1313
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
1414
import androidx.compose.runtime.saveable.SaveableStateRegistry
1515
import androidx.compose.runtime.setValue
16-
import androidx.compose.runtime.snapshots.Snapshot
1716
import com.squareup.workflow1.ActionApplied
1817
import com.squareup.workflow1.ActionProcessingResult
1918
import com.squareup.workflow1.NoopWorkflowInterceptor
@@ -32,6 +31,7 @@ import com.squareup.workflow1.compose.WorkflowComposableRenderer
3231
import com.squareup.workflow1.identifier
3332
import com.squareup.workflow1.internal.IdCounter
3433
import com.squareup.workflow1.internal.WorkflowNodeId
34+
import com.squareup.workflow1.internal.compose.coroutines.requireSend
3535
import com.squareup.workflow1.internal.createId
3636
import com.squareup.workflow1.workflowSessionToString
3737
import kotlinx.coroutines.CoroutineName
@@ -42,6 +42,11 @@ import kotlinx.coroutines.channels.Channel
4242
import kotlinx.coroutines.selects.SelectBuilder
4343
import kotlin.coroutines.CoroutineContext
4444

45+
private const val OUTPUT_QUEUE_LIMIT = 1_000
46+
47+
/**
48+
* Representation and implementation of a single [ComposeWorkflow].
49+
*/
4550
@OptIn(WorkflowExperimentalApi::class)
4651
internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
4752
override val id: WorkflowNodeId,
@@ -74,7 +79,7 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
7479
private var lastProps by mutableStateOf(initialProps)
7580
private val saveableStateRegistry: SaveableStateRegistry
7681
private var snapshotCache = snapshot?.childTreeSnapshots
77-
private val nodesToSnapshot = mutableVectorOf<ComposeChildNode<*, *, *>>()
82+
private val childNodes = mutableVectorOf<ComposeChildNode<*, *, *>>()
7883

7984
private val outputsChannel = Channel<OutputT>(capacity = OUTPUT_QUEUE_LIMIT)
8085

@@ -94,11 +99,7 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
9499
* Function invoked when [onNextAction] receives an output from [outputsChannel].
95100
*/
96101
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")
102103

103104
val applied = ActionApplied(
104105
output = WorkflowOutput(output),
@@ -109,11 +110,15 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
109110
)
110111

111112
// 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+
}
113117
}
114118

115119
init {
116120
interceptor.onSessionStarted(workflowScope = this, session = this)
121+
117122
val workflowSnapshot = snapshot?.workflowSnapshot
118123
var restoredRegistry: SaveableStateRegistry? = null
119124
// Don't care about this return value, our state is separate.
@@ -140,6 +145,9 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
140145
workflow: Workflow<PropsT, OutputT, RenderingT>,
141146
props: PropsT
142147
): 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")
143151
workflow as ComposeWorkflow
144152
return withCompositionLocals(
145153
LocalSaveableStateRegistry provides saveableStateRegistry,
@@ -217,7 +225,13 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
217225

218226
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
219227
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)
221235
with(selector) {
222236
outputsChannel.onReceive(processOutputFromChannel)
223237
}
@@ -285,15 +299,15 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
285299
}
286300

287301
private fun addChildNode(childNode: ComposeChildNode<*, *, *>) {
288-
nodesToSnapshot += childNode
302+
childNodes += childNode
289303
}
290304

291305
private fun removeChildNode(childNode: ComposeChildNode<*, *, *>) {
292-
nodesToSnapshot -= childNode
306+
childNodes -= childNode
293307
}
294308

295309
private fun createChildSnapshots(): Map<WorkflowNodeId, TreeSnapshot> = buildMap {
296-
nodesToSnapshot.forEach { child ->
310+
childNodes.forEach { child ->
297311
put(child.id, child.snapshot())
298312
}
299313
}
@@ -318,19 +332,20 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
318332
appliedActionFromChild: ActionApplied<ChildOutputT>,
319333
onOutput: ((ChildOutputT) -> Unit)?
320334
): ActionProcessingResult {
335+
log("handling child output: $appliedActionFromChild")
321336
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+
// }
334349

335350
// The child DID emit an output, so we need to call our handler, which will zero or
336351
// more of two things: (1) change our state, (2) emit an output.
@@ -340,18 +355,29 @@ internal class ComposeWorkflowChildNode<PropsT, OutputT, RenderingT>(
340355
// directly. But only for the first call to emitOutput – subsequent calls will need to
341356
// be handled as usual.
342357
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)
347358

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)
349372
onEmitOutputOverride = null
350373
}
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+
}
355381

356382
// If maybeParentResult is not null then onOutput called emitOutput.
357383
return maybeParentResult ?: appliedActionFromChild.withOutput(null)

0 commit comments

Comments
 (0)