@@ -26,7 +26,6 @@ import com.squareup.workflow1.internal.UnitApplier
26
26
import com.squareup.workflow1.internal.WorkflowNodeId
27
27
import kotlinx.coroutines.CoroutineStart.ATOMIC
28
28
import kotlinx.coroutines.ExperimentalCoroutinesApi
29
- import kotlinx.coroutines.ensureActive
30
29
import kotlinx.coroutines.launch
31
30
import kotlinx.coroutines.selects.SelectBuilder
32
31
import kotlin.coroutines.CoroutineContext
@@ -75,8 +74,6 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
75
74
private val recomposerDriver = RecomposerDriver (recomposer)
76
75
private val composition: Composition = Composition (UnitApplier , recomposer)
77
76
78
- private var frameTimeCounter = 0L
79
-
80
77
private var cachedComposeWorkflow: ComposeWorkflow <PropsT , OutputT , RenderingT > by
81
78
mutableStateOf(workflow)
82
79
private var lastProps by mutableStateOf(initialProps)
@@ -94,8 +91,6 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
94
91
* not called.
95
92
*/
96
93
private val processFrameRequestFromChannel: () -> ActionProcessingResult = {
97
- log(" frame request received from channel" )
98
-
99
94
// A pure frame request means compose state was updated that the composition read, but
100
95
// emitOutput was not called, so we don't have any outputs to report.
101
96
val applied = ActionApplied <OutputT >(
@@ -104,7 +99,7 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
104
99
)
105
100
106
101
// Propagate the action up the workflow tree.
107
- log(" sending no output to parent: $applied " )
102
+ log(" frame request received from channel, sending no output to parent: $applied " )
108
103
emitAppliedActionToParent(applied)
109
104
}
110
105
@@ -119,6 +114,8 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
119
114
// We also need to set the composition content before calling startComposition so it doesn't
120
115
// need to suspend to wait for it.
121
116
composition.setContent {
117
+ // childNode isn't snapshot state but that's fine, since when the recomposer is started it
118
+ // will always recompose, childNode will be non-null by then, and it will never change again.
122
119
val childNode = this .childNode
123
120
if (childNode != null ) {
124
121
val rendering = childNode.produceRendering(
@@ -180,40 +177,43 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
180
177
workflow : Workflow <PropsT , OutputT , RenderingT >,
181
178
input : PropsT
182
179
): RenderingT {
183
- log(" render setting props and workflow states" )
184
180
this .cachedComposeWorkflow = workflow as ComposeWorkflow
185
181
this .lastProps = input
186
182
187
183
// Ensure that recomposer has a chance to process any state changes from the action cascade that
188
184
// triggered this render before we check for a frame.
189
185
log(" render sending apply notifications again needsRecompose=${recomposerDriver.needsRecompose} " )
186
+ // TODO Consider pulling this up into the workflow runtime loop, since we only need to run it
187
+ // once before the entire tree renders, not at every level. In fact, if this is only here to
188
+ // ensure cachedComposeWorkflow and lastProps are seen, that will only work if this
189
+ // ComposeWorkflow is not nested below another traditional and compose workflow, since anything
190
+ // rendering under the first CW will be in a snapshot.
190
191
Snapshot .sendApplyNotifications()
191
192
log(" sent apply notifications, needsRecompose=${recomposerDriver.needsRecompose} " )
192
193
193
194
val initialRender = ! lastRendering.isInitialized
194
195
if (initialRender) {
195
- // Initial render kicks off the render loop. This should always synchronously request a frame.
196
+ // Initial render kicks off the render loop. This should synchronously request a frame.
196
197
startComposition()
197
198
}
198
199
199
200
// Synchronously recompose any invalidated composables, if any, and update lastRendering.
200
201
// It is very likely that trySendFrame will fail: any time the workflow runtime is doing a
201
202
// render pass and no state read by our composition changed, there shouldn't be a frame request.
202
- log(" renderFrame with time $frameTimeCounter " )
203
- val frameSent = recomposerDriver.tryPerformRecompose(frameTimeCounter)
204
- if (frameSent) {
205
- log(" renderFrame finished executing frame with time $frameTimeCounter " )
206
- frameTimeCounter++
203
+ // Hard-code unchanging frame time since there's no actual frame time and workflow code
204
+ // shouldn't rely on this value.
205
+ log(" renderFrame" )
206
+ val recomposed = recomposerDriver.tryPerformRecompose(frameTimeNanos = 0L )
207
+ if (recomposed) {
208
+ log(" renderFrame finished executing frame" )
207
209
} else {
208
210
log(" no frame request at time of render!" )
209
211
if (initialRender) {
210
212
error(" Expected initial composition to synchronously request initial frame." )
211
213
}
212
214
}
213
215
214
- return lastRendering.getOrThrow().also {
215
- log(" render returning value: $it " )
216
- }
216
+ return lastRendering.getOrThrow()
217
217
}
218
218
219
219
override fun snapshot (): TreeSnapshot = childNode!! .snapshot()
@@ -237,14 +237,11 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
237
237
238
238
@OptIn(ExperimentalCoroutinesApi ::class )
239
239
private fun startComposition () {
240
+ // Launch as atomic to ensure the composition is always disposed, even if our job is cancelled
241
+ // before this coroutine has a chance to start running.
240
242
launch(start = ATOMIC ) {
241
243
try {
242
- log(" runRecomposeAndApplyChanges" )
243
244
recomposerDriver.runRecomposeAndApplyChanges()
244
- } catch (e: Throwable ) {
245
- log(" compose runtime threw: $e \n " + e.stackTraceToString())
246
- ensureActive()
247
- throw e
248
245
} finally {
249
246
composition.dispose()
250
247
}
0 commit comments