@@ -6,7 +6,6 @@ import com.squareup.workflow1.internal.compose.coroutines.PreemptingDispatcher
6
6
import com.squareup.workflow1.internal.compose.coroutines.requireSend
7
7
import kotlinx.coroutines.CancellableContinuation
8
8
import kotlinx.coroutines.CoroutineDispatcher
9
- import kotlinx.coroutines.CoroutineScope
10
9
import kotlinx.coroutines.Dispatchers
11
10
import kotlinx.coroutines.ExperimentalCoroutinesApi
12
11
import kotlinx.coroutines.channels.Channel
@@ -39,43 +38,48 @@ import kotlin.coroutines.Continuation
39
38
* recompose loop, and by advancing it any time the recomposer reports pending work but hasn't
40
39
* requested a frame yet.
41
40
*/
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
54
74
)
55
75
)
56
76
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 {
58
81
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 )
79
83
80
84
/* *
81
85
* Returns true if the recomposer is ready to recompose. When true, the next call to
@@ -84,7 +88,7 @@ internal class RecomposerDriver(
84
88
* Use [onAwaitFrameAvailable] to wait for this to be true.
85
89
*/
86
90
@OptIn(ExperimentalCoroutinesApi ::class )
87
- val needsRecompose: Boolean
91
+ override val needsRecompose: Boolean
88
92
get() {
89
93
val wasEmpty = frameRequestChannel.isEmpty
90
94
if (wasEmpty && recomposer.hasPendingWork) {
@@ -99,11 +103,12 @@ internal class RecomposerDriver(
99
103
}
100
104
}
101
105
102
- suspend fun runRecomposeAndApplyChanges () {
106
+ override suspend fun runRecomposeAndApplyChanges () {
103
107
// Note: This context is _only_ used for the actual recompose loop. Everything inside the
104
108
// composition (rememberCoroutineScope, LaunchedEffects, etc) will NOT see these, and will see
105
109
// only whatever context was passed into the Recomposer's constructor (plus the stuff it adds
106
110
// to that context itself, like the BroadcastFrameClock).
111
+ val frameClock = this
107
112
withContext(dispatcher + frameClock) {
108
113
recomposer.runRecomposeAndApplyChanges()
109
114
}
@@ -114,7 +119,7 @@ internal class RecomposerDriver(
114
119
* recomposition with the given frame time and returns true. Returns false if there is no work to
115
120
* do.
116
121
*/
117
- fun tryPerformRecompose (frameTimeNanos : Long ): Boolean {
122
+ override fun tryPerformRecompose (frameTimeNanos : Long ): Boolean {
118
123
tryGetFrameRequest()?.let { frameRequest ->
119
124
frameRequest.execute(frameTimeNanos)
120
125
return true
@@ -125,7 +130,7 @@ internal class RecomposerDriver(
125
130
/* *
126
131
* Registers with selector to resume when [needsRecompose] becomes true.
127
132
*/
128
- fun <R > onAwaitFrameAvailable (
133
+ override fun <R > onAwaitFrameAvailable (
129
134
selector : SelectBuilder <R >,
130
135
block : suspend () -> R
131
136
) {
@@ -140,6 +145,25 @@ internal class RecomposerDriver(
140
145
}
141
146
}
142
147
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
+
143
167
/* *
144
168
* Returns a [Continuation] representing the next frame request if the recomposer has work to do,
145
169
* otherwise returns null. This method is best-effort: It tries to poke the recomposer to request
0 commit comments