1
1
package com.squareup.workflow1.internal
2
2
3
3
import androidx.compose.runtime.Composition
4
+ import androidx.compose.runtime.CompositionLocalProvider
4
5
import androidx.compose.runtime.MonotonicFrameClock
5
6
import androidx.compose.runtime.Recomposer
6
7
import androidx.compose.runtime.SideEffect
7
8
import androidx.compose.runtime.getValue
8
9
import androidx.compose.runtime.mutableStateOf
10
+ import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
11
+ import androidx.compose.runtime.saveable.SaveableStateRegistry
9
12
import androidx.compose.runtime.setValue
10
13
import androidx.compose.runtime.snapshots.Snapshot
11
14
import com.squareup.workflow1.ActionApplied
12
15
import com.squareup.workflow1.ActionProcessingResult
13
- import com.squareup.workflow1.BaseRenderContext
14
16
import com.squareup.workflow1.NoopWorkflowInterceptor
15
17
import com.squareup.workflow1.NullableInitBox
16
18
import com.squareup.workflow1.RuntimeConfig
17
19
import com.squareup.workflow1.RuntimeConfigOptions
18
- import com.squareup.workflow1.Sink
19
20
import com.squareup.workflow1.TreeSnapshot
20
21
import com.squareup.workflow1.Workflow
21
- import com.squareup.workflow1.WorkflowAction
22
22
import com.squareup.workflow1.WorkflowExperimentalApi
23
23
import com.squareup.workflow1.WorkflowInterceptor
24
24
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
@@ -27,7 +27,6 @@ import com.squareup.workflow1.WorkflowTracer
27
27
import com.squareup.workflow1.compose.ComposeWorkflow
28
28
import kotlinx.coroutines.CancellableContinuation
29
29
import kotlinx.coroutines.CancellationException
30
- import kotlinx.coroutines.CoroutineScope
31
30
import kotlinx.coroutines.CoroutineStart.ATOMIC
32
31
import kotlinx.coroutines.DelicateCoroutinesApi
33
32
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -38,7 +37,6 @@ import kotlinx.coroutines.launch
38
37
import kotlinx.coroutines.selects.SelectBuilder
39
38
import kotlinx.coroutines.suspendCancellableCoroutine
40
39
import kotlin.coroutines.CoroutineContext
41
- import kotlin.reflect.KType
42
40
43
41
private const val OUTPUT_QUEUE_LIMIT = 1_000
44
42
@@ -47,6 +45,7 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
47
45
id : WorkflowNodeId ,
48
46
workflow : ComposeWorkflow <PropsT , OutputT , RenderingT >,
49
47
initialProps : PropsT ,
48
+ snapshot : TreeSnapshot ? ,
50
49
baseContext : CoroutineContext ,
51
50
// Providing default value so we don't need to specify in test.
52
51
runtimeConfig : RuntimeConfig = RuntimeConfigOptions .DEFAULT_CONFIG ,
@@ -86,6 +85,7 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
86
85
private var frameTimeCounter = 0L
87
86
private val outputsChannel = Channel <OutputT >(capacity = OUTPUT_QUEUE_LIMIT )
88
87
private val frameRequestChannel = Channel <FrameRequest <* >>(capacity = 1 )
88
+ private val saveableStateRegistry: SaveableStateRegistry
89
89
90
90
/* *
91
91
* This is the lambda passed to every invocation of the [ComposeWorkflow.produceRendering] method.
@@ -190,13 +190,22 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
190
190
init {
191
191
interceptor.onSessionStarted(workflowScope = this , session = this )
192
192
// Don't care about this return value, our state is separate.
193
+ val workflowSnapshot = snapshot?.workflowSnapshot
194
+ var restoredRegistry: SaveableStateRegistry ? = null
193
195
interceptor.onInitialState(
194
196
props = initialProps,
195
- snapshot = null , // TODO
197
+ snapshot = workflowSnapshot,
196
198
workflowScope = this ,
197
199
session = this ,
198
- proceed = { _, _, _ -> ComposeWorkflowState }
200
+ proceed = { innerProps, innerSnapshot, _ ->
201
+ lastProps = innerProps
202
+ restoredRegistry = restoreSaveableStateRegistryFromSnapshot(innerSnapshot)
203
+ ComposeWorkflowState
204
+ }
199
205
)
206
+ // Can't assign directly in proceed because the compiler can't guarantee it's ran during the
207
+ // initialization.
208
+ saveableStateRegistry = restoredRegistry ? : restoreSaveableStateRegistryFromSnapshot(null )
200
209
201
210
// By not calling setContent directly every time, we ensure that if neither the workflow
202
211
// instance nor input changed, we don't recompose.
@@ -208,23 +217,28 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
208
217
composition.setContent {
209
218
@Suppress(" NAME_SHADOWING" )
210
219
val workflow = cachedComposeWorkflow
220
+ // This composable will run synchronously when setContent is called, but we don't want to
221
+ // actually render the workflow since we're not in a render pass. The cached property will
222
+ // only be null until the first render pass, then never null again.
211
223
if (workflow != null ) {
212
- val rendering = interceptor.onRenderComposeWorkflow(
213
- renderProps = lastProps,
214
- emitOutput = sendOutputToChannel,
215
- proceed = { innerProps, innerEmitOutput ->
216
- workflow.produceRendering(
217
- props = innerProps,
218
- emitOutput = innerEmitOutput
219
- )
220
- },
221
- session = this
222
- )
223
-
224
- // lastRendering isn't snapshot state, so wait until the composition is applied to update
225
- // it.
226
- SideEffect {
227
- lastRendering = NullableInitBox (rendering)
224
+ CompositionLocalProvider (LocalSaveableStateRegistry provides saveableStateRegistry) {
225
+ val rendering = interceptor.onRenderComposeWorkflow(
226
+ renderProps = lastProps,
227
+ emitOutput = sendOutputToChannel,
228
+ proceed = { innerProps, innerEmitOutput ->
229
+ workflow.produceRendering(
230
+ props = innerProps,
231
+ emitOutput = innerEmitOutput
232
+ )
233
+ },
234
+ session = this
235
+ )
236
+
237
+ // lastRendering isn't snapshot state, so wait until the composition is applied to update
238
+ // it.
239
+ SideEffect {
240
+ lastRendering = NullableInitBox (rendering)
241
+ }
228
242
}
229
243
}
230
244
}
@@ -278,18 +292,19 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
278
292
}
279
293
}
280
294
281
- override fun snapshot (): TreeSnapshot {
282
- return interceptor.onSnapshotStateWithChildren(
283
- session = this ,
284
- proceed = {
285
- // Compose workflows do not support the onSnapshotState interceptor since they don't
286
- // distinguish between snapshot state objects for themselves and their child
287
- // ComposeWorkflows.
288
- // TODO Support snapshots from rememberSaveable.
289
- TreeSnapshot (workflowSnapshot = null , childTreeSnapshots = ::emptyMap)
290
- }
291
- )
292
- }
295
+ override fun snapshot (): TreeSnapshot = interceptor.onSnapshotStateWithChildren(
296
+ session = this ,
297
+ proceed = {
298
+ val workflowSnapshot = interceptor.onSnapshotState(
299
+ state = ComposeWorkflowState ,
300
+ session = this ,
301
+ proceed = {
302
+ saveSaveableStateRegistryToSnapshot(saveableStateRegistry)
303
+ }
304
+ )
305
+ TreeSnapshot (workflowSnapshot = workflowSnapshot, childTreeSnapshots = ::emptyMap)
306
+ }
307
+ )
293
308
294
309
@OptIn(ExperimentalCoroutinesApi ::class , DelicateCoroutinesApi ::class )
295
310
override fun onNextAction (selector : SelectBuilder <ActionProcessingResult >): Boolean {
@@ -356,38 +371,6 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
356
371
*/
357
372
private object ComposeWorkflowState
358
373
359
- private class ComposeRenderContext <PropsT , OutputT >(
360
- override val runtimeConfig : RuntimeConfig ,
361
- override val actionSink : Sink <WorkflowAction <PropsT , ComposeWorkflowState , OutputT >>,
362
- override val workflowTracer : WorkflowTracer ? ,
363
- ) : BaseRenderContext<PropsT, ComposeWorkflowState, OutputT> {
364
-
365
- override fun runningSideEffect (
366
- key : String ,
367
- sideEffect : suspend CoroutineScope .() -> Unit
368
- ) {
369
- throw UnsupportedOperationException (" runningSideEffect not supported in ComposeWorkflows" )
370
- }
371
-
372
- override fun <ResultT > remember (
373
- key : String ,
374
- resultType : KType ,
375
- vararg inputs : Any? ,
376
- calculation : () -> ResultT
377
- ): ResultT {
378
- throw UnsupportedOperationException (" remember not supported in ComposeWorkflows" )
379
- }
380
-
381
- override fun <ChildPropsT , ChildOutputT , ChildRenderingT > renderChild (
382
- child : Workflow <ChildPropsT , ChildOutputT , ChildRenderingT >,
383
- props : ChildPropsT ,
384
- key : String ,
385
- handler : (ChildOutputT ) -> WorkflowAction <PropsT , ComposeWorkflowState , OutputT >
386
- ): ChildRenderingT {
387
- throw UnsupportedOperationException (" renderChild not supported in ComposeWorkflows" )
388
- }
389
- }
390
-
391
374
private data class FrameRequest <R >(
392
375
private val onFrame : (frameTimeNanos: Long ) -> R ,
393
376
private val continuation : CancellableContinuation <R >
0 commit comments