Skip to content

Commit 4e4029a

Browse files
Basic support for rememberSaveable.
1 parent 17958c2 commit 4e4029a

File tree

5 files changed

+271
-68
lines changed

5 files changed

+271
-68
lines changed

samples/hello-compose-workflow/src/main/java/com/squareup/sample/hellocomposeworkflow/HelloComposeWorkflow.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.getValue
55
import androidx.compose.runtime.mutableStateOf
66
import androidx.compose.runtime.remember
7+
import androidx.compose.runtime.saveable.Saver
8+
import androidx.compose.runtime.saveable.SaverScope
9+
import androidx.compose.runtime.saveable.rememberSaveable
710
import androidx.compose.runtime.setValue
811
import com.squareup.sample.hellocomposeworkflow.HelloComposeWorkflow.State.Goodbye
912
import com.squareup.sample.hellocomposeworkflow.HelloComposeWorkflow.State.Hello
@@ -18,12 +21,17 @@ object HelloComposeWorkflow : ComposeWorkflow<Unit, Nothing, HelloRendering>() {
1821
Goodbye
1922
}
2023

24+
object StateSaver : Saver<State, Int> {
25+
override fun restore(value: Int) = State.entries[value]
26+
override fun SaverScope.save(value: State) = value.ordinal
27+
}
28+
2129
@Composable
2230
override fun produceRendering(
2331
props: Unit,
2432
emitOutput: (Nothing) -> Unit
2533
): HelloRendering {
26-
var state by remember { mutableStateOf(Hello) }
34+
var state by rememberSaveable(stateSaver = StateSaver) { mutableStateOf(Hello) }
2735
val compositions = remember { AtomicInteger(0) }
2836
println("OMG recomposing state=$state (count=${compositions.incrementAndGet()})")
2937

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Snapshot.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ public fun BufferedSink.writeFloat(float: Float): BufferedSink = writeInt(float.
9797

9898
public fun BufferedSource.readFloat(): Float = Float.fromBits(readInt())
9999

100+
public fun BufferedSink.writeDouble(double: Double): BufferedSink = writeLong(double.toRawBits())
101+
102+
public fun BufferedSource.readDouble(): Double = Double.fromBits(readLong())
103+
100104
public fun BufferedSink.writeUtf8WithLength(str: String): BufferedSink {
101105
return writeByteStringWithLength(str.encodeUtf8())
102106
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ internal fun <PropsT, OutputT, RenderingT> createWorkflowNode(
4040
id = id,
4141
workflow = workflow as ComposeWorkflow,
4242
initialProps = initialProps,
43+
snapshot = snapshot,
4344
baseContext = baseContext,
4445
runtimeConfig = runtimeConfig,
4546
workflowTracer = workflowTracer,

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

Lines changed: 50 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
package com.squareup.workflow1.internal
22

33
import androidx.compose.runtime.Composition
4+
import androidx.compose.runtime.CompositionLocalProvider
45
import androidx.compose.runtime.MonotonicFrameClock
56
import androidx.compose.runtime.Recomposer
67
import androidx.compose.runtime.SideEffect
78
import androidx.compose.runtime.getValue
89
import androidx.compose.runtime.mutableStateOf
10+
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
11+
import androidx.compose.runtime.saveable.SaveableStateRegistry
912
import androidx.compose.runtime.setValue
1013
import androidx.compose.runtime.snapshots.Snapshot
1114
import com.squareup.workflow1.ActionApplied
1215
import com.squareup.workflow1.ActionProcessingResult
13-
import com.squareup.workflow1.BaseRenderContext
1416
import com.squareup.workflow1.NoopWorkflowInterceptor
1517
import com.squareup.workflow1.NullableInitBox
1618
import com.squareup.workflow1.RuntimeConfig
1719
import com.squareup.workflow1.RuntimeConfigOptions
18-
import com.squareup.workflow1.Sink
1920
import com.squareup.workflow1.TreeSnapshot
2021
import com.squareup.workflow1.Workflow
21-
import com.squareup.workflow1.WorkflowAction
2222
import com.squareup.workflow1.WorkflowExperimentalApi
2323
import com.squareup.workflow1.WorkflowInterceptor
2424
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
@@ -27,7 +27,6 @@ import com.squareup.workflow1.WorkflowTracer
2727
import com.squareup.workflow1.compose.ComposeWorkflow
2828
import kotlinx.coroutines.CancellableContinuation
2929
import kotlinx.coroutines.CancellationException
30-
import kotlinx.coroutines.CoroutineScope
3130
import kotlinx.coroutines.CoroutineStart.ATOMIC
3231
import kotlinx.coroutines.DelicateCoroutinesApi
3332
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -38,7 +37,6 @@ import kotlinx.coroutines.launch
3837
import kotlinx.coroutines.selects.SelectBuilder
3938
import kotlinx.coroutines.suspendCancellableCoroutine
4039
import kotlin.coroutines.CoroutineContext
41-
import kotlin.reflect.KType
4240

4341
private const val OUTPUT_QUEUE_LIMIT = 1_000
4442

@@ -47,6 +45,7 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
4745
id: WorkflowNodeId,
4846
workflow: ComposeWorkflow<PropsT, OutputT, RenderingT>,
4947
initialProps: PropsT,
48+
snapshot: TreeSnapshot?,
5049
baseContext: CoroutineContext,
5150
// Providing default value so we don't need to specify in test.
5251
runtimeConfig: RuntimeConfig = RuntimeConfigOptions.DEFAULT_CONFIG,
@@ -86,6 +85,7 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
8685
private var frameTimeCounter = 0L
8786
private val outputsChannel = Channel<OutputT>(capacity = OUTPUT_QUEUE_LIMIT)
8887
private val frameRequestChannel = Channel<FrameRequest<*>>(capacity = 1)
88+
private val saveableStateRegistry: SaveableStateRegistry
8989

9090
/**
9191
* This is the lambda passed to every invocation of the [ComposeWorkflow.produceRendering] method.
@@ -190,13 +190,22 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
190190
init {
191191
interceptor.onSessionStarted(workflowScope = this, session = this)
192192
// Don't care about this return value, our state is separate.
193+
val workflowSnapshot = snapshot?.workflowSnapshot
194+
var restoredRegistry: SaveableStateRegistry? = null
193195
interceptor.onInitialState(
194196
props = initialProps,
195-
snapshot = null, // TODO
197+
snapshot = workflowSnapshot,
196198
workflowScope = this,
197199
session = this,
198-
proceed = { _, _, _ -> ComposeWorkflowState }
200+
proceed = { innerProps, innerSnapshot, _ ->
201+
lastProps = innerProps
202+
restoredRegistry = restoreSaveableStateRegistryFromSnapshot(innerSnapshot)
203+
ComposeWorkflowState
204+
}
199205
)
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)
200209

201210
// By not calling setContent directly every time, we ensure that if neither the workflow
202211
// instance nor input changed, we don't recompose.
@@ -208,23 +217,28 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
208217
composition.setContent {
209218
@Suppress("NAME_SHADOWING")
210219
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.
211223
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+
}
228242
}
229243
}
230244
}
@@ -278,18 +292,19 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
278292
}
279293
}
280294

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+
)
293308

294309
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
295310
override fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
@@ -356,38 +371,6 @@ internal class ComposeWorkflowNode<PropsT, OutputT, RenderingT>(
356371
*/
357372
private object ComposeWorkflowState
358373

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-
391374
private data class FrameRequest<R>(
392375
private val onFrame: (frameTimeNanos: Long) -> R,
393376
private val continuation: CancellableContinuation<R>

0 commit comments

Comments
 (0)