Skip to content

Commit 303a251

Browse files
Refactor RecomposerDriver a bit.
1 parent c3b70ae commit 303a251

File tree

2 files changed

+63
-42
lines changed

2 files changed

+63
-42
lines changed

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,7 @@ internal class ComposeWorkflowNodeAdapter<PropsT, OutputT, RenderingT>(
7272

7373
private val recomposer: Recomposer = Recomposer(coroutineContext)
7474

75-
// TODO we could create a PreemptingDispatcher and stash it in the coroutine context (not as a
76-
// dispatcher, but inside a private holder) so we can reuse it for all compose runtimes. But it
77-
// wouldn't really save that much and might not be worth it.
78-
private val recomposerDriver = RecomposerDriver(recomposer = recomposer, scope = this)
75+
private val recomposerDriver = RecomposerDriver(recomposer)
7976
private val composition: Composition = Composition(UnitApplier, recomposer)
8077

8178
private var frameTimeCounter = 0L

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

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.squareup.workflow1.internal.compose.coroutines.PreemptingDispatcher
66
import com.squareup.workflow1.internal.compose.coroutines.requireSend
77
import kotlinx.coroutines.CancellableContinuation
88
import kotlinx.coroutines.CoroutineDispatcher
9-
import kotlinx.coroutines.CoroutineScope
109
import kotlinx.coroutines.Dispatchers
1110
import kotlinx.coroutines.ExperimentalCoroutinesApi
1211
import kotlinx.coroutines.channels.Channel
@@ -39,43 +38,48 @@ import kotlin.coroutines.Continuation
3938
* recompose loop, and by advancing it any time the recomposer reports pending work but hasn't
4039
* requested a frame yet.
4140
*/
42-
internal class RecomposerDriver(
43-
private val recomposer: Recomposer,
44-
private val dispatcher: PreemptingDispatcher,
45-
) {
46-
@OptIn(ExperimentalStdlibApi::class)
47-
constructor(
48-
recomposer: Recomposer,
49-
scope: CoroutineScope
50-
) : this(
51-
recomposer = recomposer,
52-
dispatcher = PreemptingDispatcher(
53-
scope.coroutineContext[CoroutineDispatcher] ?: Dispatchers.Default
41+
interface RecomposerDriver {
42+
/**
43+
* Returns true if the recomposer is ready to recompose. When true, the next call to
44+
* [tryPerformRecompose] will succeed.
45+
*
46+
* Use [onAwaitFrameAvailable] to wait for this to be true.
47+
*/
48+
val needsRecompose: Boolean
49+
50+
suspend fun runRecomposeAndApplyChanges()
51+
52+
/**
53+
* If the [Recomposer] is ready to recompose ([needsRecompose] is true), performs the
54+
* recomposition with the given frame time and returns true. Returns false if there is no work to
55+
* do.
56+
*/
57+
fun tryPerformRecompose(frameTimeNanos: Long): Boolean
58+
59+
/**
60+
* Registers with selector to resume when [needsRecompose] becomes true.
61+
*/
62+
fun <R> onAwaitFrameAvailable(
63+
selector: SelectBuilder<R>,
64+
block: suspend () -> R
65+
)
66+
}
67+
68+
@OptIn(ExperimentalStdlibApi::class)
69+
internal fun RecomposerDriver(recomposer: Recomposer): RecomposerDriver =
70+
RealRecomposerDriver(
71+
recomposer,
72+
PreemptingDispatcher(
73+
recomposer.effectCoroutineContext[CoroutineDispatcher] ?: Dispatchers.Default
5474
)
5575
)
5676

57-
private val frameRequestChannel = Channel<FrameRequest<*>>(capacity = 1)
77+
private class RealRecomposerDriver(
78+
private val recomposer: Recomposer,
79+
private val dispatcher: PreemptingDispatcher,
80+
) : RecomposerDriver, MonotonicFrameClock {
5881

59-
private val frameClock = object : MonotonicFrameClock {
60-
@OptIn(ExperimentalStdlibApi::class)
61-
override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
62-
log("compose workflow withFrameNanos (dispatcher=${currentCoroutineContext()[CoroutineDispatcher]})")
63-
log(RuntimeException().stackTraceToString())
64-
65-
return suspendCancellableCoroutine { continuation ->
66-
val frameRequest = FrameRequest(
67-
onFrame = onFrame,
68-
continuation = continuation
69-
)
70-
71-
// This will throw if a frame request is already enqueued. If currently in an action cascade
72-
// (i.e. handling a received output), then it will be picked up in the imminent re-render.
73-
// Otherwise, onNextAction will have registered a receiver for it that will trigger a render
74-
// pass.
75-
frameRequestChannel.requireSend(frameRequest)
76-
}
77-
}
78-
}
82+
private val frameRequestChannel = Channel<FrameRequest<*>>(capacity = 1)
7983

8084
/**
8185
* Returns true if the recomposer is ready to recompose. When true, the next call to
@@ -84,7 +88,7 @@ internal class RecomposerDriver(
8488
* Use [onAwaitFrameAvailable] to wait for this to be true.
8589
*/
8690
@OptIn(ExperimentalCoroutinesApi::class)
87-
val needsRecompose: Boolean
91+
override val needsRecompose: Boolean
8892
get() {
8993
val wasEmpty = frameRequestChannel.isEmpty
9094
if (wasEmpty && recomposer.hasPendingWork) {
@@ -99,11 +103,12 @@ internal class RecomposerDriver(
99103
}
100104
}
101105

102-
suspend fun runRecomposeAndApplyChanges() {
106+
override suspend fun runRecomposeAndApplyChanges() {
103107
// Note: This context is _only_ used for the actual recompose loop. Everything inside the
104108
// composition (rememberCoroutineScope, LaunchedEffects, etc) will NOT see these, and will see
105109
// only whatever context was passed into the Recomposer's constructor (plus the stuff it adds
106110
// to that context itself, like the BroadcastFrameClock).
111+
val frameClock = this
107112
withContext(dispatcher + frameClock) {
108113
recomposer.runRecomposeAndApplyChanges()
109114
}
@@ -114,7 +119,7 @@ internal class RecomposerDriver(
114119
* recomposition with the given frame time and returns true. Returns false if there is no work to
115120
* do.
116121
*/
117-
fun tryPerformRecompose(frameTimeNanos: Long): Boolean {
122+
override fun tryPerformRecompose(frameTimeNanos: Long): Boolean {
118123
tryGetFrameRequest()?.let { frameRequest ->
119124
frameRequest.execute(frameTimeNanos)
120125
return true
@@ -125,7 +130,7 @@ internal class RecomposerDriver(
125130
/**
126131
* Registers with selector to resume when [needsRecompose] becomes true.
127132
*/
128-
fun <R> onAwaitFrameAvailable(
133+
override fun <R> onAwaitFrameAvailable(
129134
selector: SelectBuilder<R>,
130135
block: suspend () -> R
131136
) {
@@ -140,6 +145,25 @@ internal class RecomposerDriver(
140145
}
141146
}
142147

148+
@OptIn(ExperimentalStdlibApi::class)
149+
override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
150+
log("compose workflow withFrameNanos (dispatcher=${currentCoroutineContext()[CoroutineDispatcher]})")
151+
log(RuntimeException().stackTraceToString())
152+
153+
return suspendCancellableCoroutine { continuation ->
154+
val frameRequest = FrameRequest(
155+
onFrame = onFrame,
156+
continuation = continuation
157+
)
158+
159+
// This will throw if a frame request is already enqueued. If currently in an action cascade
160+
// (i.e. handling a received output), then it will be picked up in the imminent re-render.
161+
// Otherwise, onNextAction will have registered a receiver for it that will trigger a render
162+
// pass.
163+
frameRequestChannel.requireSend(frameRequest)
164+
}
165+
}
166+
143167
/**
144168
* Returns a [Continuation] representing the next frame request if the recomposer has work to do,
145169
* otherwise returns null. This method is best-effort: It tries to poke the recomposer to request

0 commit comments

Comments
 (0)