Skip to content

Commit 6194650

Browse files
move renderComposable state into dedicated class
1 parent d24e37a commit 6194650

File tree

3 files changed

+107
-56
lines changed

3 files changed

+107
-56
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.squareup.workflow1
22

3+
import androidx.compose.runtime.Composable
34
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
45
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
56
import kotlinx.coroutines.CoroutineScope
@@ -259,6 +260,15 @@ public interface WorkflowInterceptor {
259260
handler: (CO) -> WorkflowAction<P, S, O>
260261
) -> CR
261262
): CR = proceed(child, childProps, key, handler)
263+
264+
public fun <CR> onRenderComposable(
265+
key: String,
266+
content: @Composable () -> CR,
267+
proceed: (
268+
key: String,
269+
content: @Composable () -> CR
270+
) -> CR
271+
): CR = proceed(key, content)
262272
}
263273
}
264274

@@ -384,6 +394,21 @@ private class InterceptedRenderContext<P, S, O>(
384394
}
385395
}
386396

397+
@OptIn(WorkflowExperimentalApi::class)
398+
override fun <ChildRenderingT> renderComposable(
399+
key: String,
400+
content: @Composable () -> ChildRenderingT
401+
): ChildRenderingT = interceptor.onRenderComposable(
402+
key = key,
403+
content = content,
404+
proceed = { iKey, iContent ->
405+
baseRenderContext.renderComposable(
406+
key = iKey,
407+
content = iContent
408+
)
409+
}
410+
)
411+
387412
/**
388413
* In a block with a CoroutineScope receiver, calls to `coroutineContext` bind
389414
* to `CoroutineScope.coroutineContext` instead of `suspend val coroutineContext`.

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

Lines changed: 8 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,11 @@
33
package com.squareup.workflow1.internal
44

55
import androidx.compose.runtime.Composable
6-
import androidx.compose.runtime.Composition
7-
import androidx.compose.runtime.CompositionLocalContext
8-
import androidx.compose.runtime.CompositionLocalProvider
9-
import androidx.compose.runtime.MonotonicFrameClock
10-
import androidx.compose.runtime.Recomposer
116
import androidx.compose.runtime.currentCompositeKeyHash
127
import androidx.compose.runtime.currentCompositionLocalContext
138
import androidx.compose.runtime.currentRecomposeScope
14-
import androidx.compose.runtime.mutableStateOf
159
import androidx.compose.runtime.remember
1610
import androidx.compose.runtime.rememberCoroutineScope
17-
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
18-
import androidx.compose.runtime.saveable.SaveableStateRegistry
1911
import com.squareup.workflow1.ActionApplied
2012
import com.squareup.workflow1.ActionProcessingResult
2113
import com.squareup.workflow1.NoopWorkflowInterceptor
@@ -27,16 +19,11 @@ import com.squareup.workflow1.WorkflowExperimentalApi
2719
import com.squareup.workflow1.WorkflowInterceptor
2820
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
2921
import com.squareup.workflow1.WorkflowTracer
30-
import com.squareup.workflow1.compose.LocalWorkflowCompositionHost
3122
import com.squareup.workflow1.compose.WorkflowCompositionHost
3223
import com.squareup.workflow1.identifier
3324
import com.squareup.workflow1.trace
34-
import kotlinx.coroutines.CoroutineScope
35-
import kotlinx.coroutines.CoroutineStart
36-
import kotlinx.coroutines.launch
3725
import kotlinx.coroutines.selects.SelectBuilder
3826
import kotlin.coroutines.CoroutineContext
39-
import kotlin.coroutines.EmptyCoroutineContext
4027

4128
/**
4229
* Responsible for tracking child workflows, starting them and tearing them down when necessary.
@@ -171,50 +158,15 @@ internal class SubtreeManager<PropsT, StateT, OutputT>(
171158
key: String,
172159
content: @Composable () -> ChildRenderingT
173160
): ChildRenderingT {
174-
val frameClock: MonotonicFrameClock // TODO
175-
val coroutineContext = EmptyCoroutineContext + frameClock
176-
val recomposer = Recomposer(coroutineContext)
177-
val composition = Composition(UnitApplier, recomposer)
178-
val saveableStateRegistry: SaveableStateRegistry // TODO
179-
val localsContext: CompositionLocalContext? // TODO
180-
181-
// TODO I think we need more than a simple UNDISPATCHED start to make this work – we have to
182-
// pump the dispatcher until the composition is finished.
183-
CoroutineScope(coroutineContext).launch(start = CoroutineStart.UNDISPATCHED) {
184-
try {
185-
recomposer.runRecomposeAndApplyChanges()
186-
} finally {
187-
composition.dispose()
188-
}
189-
}
190-
191-
val rendering = mutableStateOf<ChildRenderingT?>(null)
192-
val wrappedContent = @Composable {
193-
CompositionLocalProvider(
194-
LocalWorkflowCompositionHost provides this,
195-
LocalSaveableStateRegistry provides saveableStateRegistry,
196-
) {
197-
rendering.value = content()
198-
}
199-
}
200-
201-
composition.setContent {
202-
// Must provide the locals from the parent composition first so we can override the ones we
203-
// need. If it's null then there's no parent, but the CompositionLocalProvider API has no nice
204-
// way to pass nothing in this overload. I believe it's safe to actually call content through
205-
// two different code paths because whether there's a parent composition cannot change for an
206-
// existing workflow session – they can't move.
207-
if (localsContext == null) {
208-
wrappedContent()
209-
} else {
210-
CompositionLocalProvider(localsContext, wrappedContent)
211-
}
212-
}
213-
214-
// TODO prime the first frame to generate the initial rendering
161+
// TODO initialize, store, and start the node from an ActiveStagingList
162+
val node = WorkflowComposableNode<ChildRenderingT>(
163+
frameClock = TODO(),
164+
saveableStateRegistry = TODO(),
165+
localsContext = TODO("get from parent somehow")
166+
)
167+
node.start()
215168

216-
@Suppress("UNCHECKED_CAST")
217-
return rendering.value as ChildRenderingT
169+
node.render(content)
218170
}
219171

220172
@Composable
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.squareup.workflow1.internal
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.Composition
5+
import androidx.compose.runtime.CompositionLocalContext
6+
import androidx.compose.runtime.CompositionLocalProvider
7+
import androidx.compose.runtime.MonotonicFrameClock
8+
import androidx.compose.runtime.Recomposer
9+
import androidx.compose.runtime.mutableStateOf
10+
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
11+
import androidx.compose.runtime.saveable.SaveableStateRegistry
12+
import com.squareup.workflow1.WorkflowExperimentalApi
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.CoroutineStart
15+
import kotlinx.coroutines.launch
16+
import kotlin.coroutines.CoroutineContext
17+
import kotlin.coroutines.EmptyCoroutineContext
18+
19+
@OptIn(WorkflowExperimentalApi::class)
20+
internal class WorkflowComposableNode<RenderingT>(
21+
private val frameClock: MonotonicFrameClock, // TODO
22+
coroutineContext: CoroutineContext = EmptyCoroutineContext,
23+
private val saveableStateRegistry: SaveableStateRegistry, // TODO
24+
private val localsContext: CompositionLocalContext?, // TODO
25+
) {
26+
private val coroutineContext = coroutineContext + frameClock
27+
private val recomposer: Recomposer = Recomposer(coroutineContext)
28+
private val composition: Composition = Composition(UnitApplier, recomposer)
29+
private val rendering = mutableStateOf<RenderingT?>(null)
30+
31+
fun start() {
32+
// TODO I think we need more than a simple UNDISPATCHED start to make this work – we have to
33+
// pump the dispatcher until the composition is finished.
34+
CoroutineScope(coroutineContext).launch(start = CoroutineStart.UNDISPATCHED) {
35+
try {
36+
recomposer.runRecomposeAndApplyChanges()
37+
} finally {
38+
composition.dispose()
39+
}
40+
}
41+
}
42+
43+
fun render(content: @Composable () -> RenderingT): RenderingT {
44+
composition.setContent {
45+
// Must provide the locals from the parent composition first so we can override the ones we
46+
// need. If it's null then there's no parent, but the CompositionLocalProvider API has no nice
47+
// way to pass nothing in this overload. I believe it's safe to actually call content through
48+
// two different code paths because whether there's a parent composition cannot change for an
49+
// existing workflow session – they can't move.
50+
if (localsContext == null) {
51+
LocalsProvider(content)
52+
} else {
53+
CompositionLocalProvider(localsContext) {
54+
LocalsProvider(content)
55+
}
56+
}
57+
}
58+
59+
// TODO prime the first frame to generate the initial rendering
60+
61+
@Suppress("UNCHECKED_CAST")
62+
return rendering.value as RenderingT
63+
}
64+
65+
@Composable
66+
private inline fun LocalsProvider(crossinline content: @Composable () -> RenderingT) {
67+
CompositionLocalProvider(
68+
// LocalWorkflowCompositionHost provides this,
69+
LocalSaveableStateRegistry provides saveableStateRegistry,
70+
) {
71+
rendering.value = content()
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)