@@ -26,7 +26,6 @@ import com.squareup.workflow1.internal.UnitApplier
2626import com.squareup.workflow1.internal.WorkflowNodeId
2727import kotlinx.coroutines.CoroutineStart.ATOMIC
2828import kotlinx.coroutines.ExperimentalCoroutinesApi
29- import kotlinx.coroutines.ensureActive
3029import kotlinx.coroutines.launch
3130import kotlinx.coroutines.selects.SelectBuilder
3231import kotlin.coroutines.CoroutineContext
@@ -75,8 +74,6 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
7574 private val recomposerDriver = RecomposerDriver (recomposer)
7675 private val composition: Composition = Composition (UnitApplier , recomposer)
7776
78- private var frameTimeCounter = 0L
79-
8077 private var cachedComposeWorkflow: ComposeWorkflow <PropsT , OutputT , RenderingT > by
8178 mutableStateOf(workflow)
8279 private var lastProps by mutableStateOf(initialProps)
@@ -94,8 +91,6 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
9491 * not called.
9592 */
9693 private val processFrameRequestFromChannel: () -> ActionProcessingResult = {
97- log(" frame request received from channel" )
98-
9994 // A pure frame request means compose state was updated that the composition read, but
10095 // emitOutput was not called, so we don't have any outputs to report.
10196 val applied = ActionApplied <OutputT >(
@@ -104,7 +99,7 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
10499 )
105100
106101 // 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 " )
108103 emitAppliedActionToParent(applied)
109104 }
110105
@@ -119,6 +114,8 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
119114 // We also need to set the composition content before calling startComposition so it doesn't
120115 // need to suspend to wait for it.
121116 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.
122119 val childNode = this .childNode
123120 if (childNode != null ) {
124121 val rendering = childNode.produceRendering(
@@ -180,40 +177,43 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
180177 workflow : Workflow <PropsT , OutputT , RenderingT >,
181178 input : PropsT
182179 ): RenderingT {
183- log(" render setting props and workflow states" )
184180 this .cachedComposeWorkflow = workflow as ComposeWorkflow
185181 this .lastProps = input
186182
187183 // Ensure that recomposer has a chance to process any state changes from the action cascade that
188184 // triggered this render before we check for a frame.
189185 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.
190191 Snapshot .sendApplyNotifications()
191192 log(" sent apply notifications, needsRecompose=${recomposerDriver.needsRecompose} " )
192193
193194 val initialRender = ! lastRendering.isInitialized
194195 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.
196197 startComposition()
197198 }
198199
199200 // Synchronously recompose any invalidated composables, if any, and update lastRendering.
200201 // It is very likely that trySendFrame will fail: any time the workflow runtime is doing a
201202 // 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" )
207209 } else {
208210 log(" no frame request at time of render!" )
209211 if (initialRender) {
210212 error(" Expected initial composition to synchronously request initial frame." )
211213 }
212214 }
213215
214- return lastRendering.getOrThrow().also {
215- log(" render returning value: $it " )
216- }
216+ return lastRendering.getOrThrow()
217217 }
218218
219219 override fun snapshot (): TreeSnapshot = childNode!! .snapshot()
@@ -237,14 +237,11 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
237237
238238 @OptIn(ExperimentalCoroutinesApi ::class )
239239 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.
240242 launch(start = ATOMIC ) {
241243 try {
242- log(" runRecomposeAndApplyChanges" )
243244 recomposerDriver.runRecomposeAndApplyChanges()
244- } catch (e: Throwable ) {
245- log(" compose runtime threw: $e \n " + e.stackTraceToString())
246- ensureActive()
247- throw e
248245 } finally {
249246 composition.dispose()
250247 }
0 commit comments