1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:OptIn(InternalComposeApi::class)
18 
19 package androidx.compose.runtime
20 
21 import androidx.collection.emptyScatterSet
22 import androidx.collection.mutableIntListOf
23 import androidx.collection.mutableObjectListOf
24 import androidx.compose.runtime.internal.RememberEventDispatcher
25 import androidx.compose.runtime.platform.SynchronizedObject
26 import androidx.compose.runtime.platform.synchronized
27 
28 /**
29  * A [PausableComposition] is a sub-composition that can be composed incrementally as it supports
30  * being paused and resumed.
31  *
32  * Pausable sub-composition can be used between frames to prepare a sub-composition before it is
33  * required by the main composition. For example, this is used in lazy lists to prepare list items
34  * in between frames to that are likely to be scrolled in. The composition is paused when the start
35  * of the next frame is near allowing composition to be spread across multiple frames without
36  * delaying the production of the next frame.
37  *
38  * The result of the composition should not be used (e.g. the nodes should not added to a layout
39  * tree or placed in layout) until [PausedComposition.isComplete] is `true` and
40  * [PausedComposition.apply] has been called. The composition is incomplete and will not
41  * automatically recompose until after [PausedComposition.apply] is called.
42  *
43  * A [PausableComposition] is a [ReusableComposition] but [setPausableContent] should be used
44  * instead of [ReusableComposition.setContentWithReuse] to create a paused composition.
45  *
46  * If [Composition.setContent] or [ReusableComposition.setContentWithReuse] are used then the
47  * composition behaves as if it wasn't pausable. If there is a [PausedComposition] that has not yet
48  * been applied, an exception is thrown.
49  *
50  * @see Composition
51  * @see ReusableComposition
52  */
53 sealed interface PausableComposition : ReusableComposition {
54     /**
55      * Set the content of the composition. A [PausedComposition] that is currently paused. No
56      * composition is performed until [PausedComposition.resume] is called.
57      * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
58      * The composition should not be used until [PausedComposition.isComplete] is `true` and
59      * [PausedComposition.apply] has been called.
60      *
61      * @see Composition.setContent
62      * @see ReusableComposition.setContentWithReuse
63      */
setPausableContentnull64     fun setPausableContent(content: @Composable () -> Unit): PausedComposition
65 
66     /**
67      * Set the content of a reusable composition. A [PausedComposition] that is currently paused. No
68      * composition is performed until [PausedComposition.resume] is called.
69      * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
70      * The composition should not be used until [PausedComposition.isComplete] is `true` and
71      * [PausedComposition.apply] has been called.
72      *
73      * @see Composition.setContent
74      * @see ReusableComposition.setContentWithReuse
75      */
76     fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition
77 }
78 
79 /** The callback type used in [PausedComposition.resume]. */
80 fun interface ShouldPauseCallback {
81     /**
82      * Called to determine if a resumed [PausedComposition] should pause.
83      *
84      * @return Return `true` to indicate that the composition should pause. Otherwise the
85      *   composition will continue normally.
86      */
87     @Suppress("CallbackMethodName") fun shouldPause(): Boolean
88 }
89 
90 /**
91  * [PausedComposition] is the result of calling [PausableComposition.setContent] or
92  * [PausableComposition.setContentWithReuse]. It is used to drive the paused composition to
93  * completion. A [PausedComposition] should not be used until [isComplete] is `true` and [apply] has
94  * been called.
95  *
96  * A [PausedComposition] is created paused and will only compose the `content` parameter when
97  * [resume] is called the first time.
98  */
99 sealed interface PausedComposition {
100     /**
101      * Returns `true` when the [PausedComposition] is complete. [isComplete] matches the last value
102      * returned from [resume]. Once a [PausedComposition] is [isComplete] the [apply] method should
103      * be called. If the [apply] method is not called synchronously and immediately after [resume]
104      * returns `true` then this [isComplete] can return `false` as any state changes read by the
105      * paused composition while it is paused will cause the composition to require the paused
106      * composition to need to be resumed before it is used.
107      */
108     val isComplete: Boolean
109 
110     /**
111      * Resume the composition that has been paused. This method should be called until [resume]
112      * returns `true` or [isComplete] is `true` which has the same result as the last result of
113      * calling [resume]. The [shouldPause] parameter is a lambda that returns whether the
114      * composition should be paused. For example, in lazy lists this returns `false` until just
115      * prior to the next frame starting in which it returns `true`
116      *
117      * Calling [resume] after it returns `true` or when `isComplete` is true will throw an
118      * exception.
119      *
120      * @param shouldPause A lambda that is used to determine if the composition should be paused.
121      *   This lambda is called often so should be a very simple calculation. Returning `true` does
122      *   not guarantee the composition will pause, it should only be considered a request to pause
123      *   the composition. Not all composable functions are pausable and only pausable composition
124      *   functions will pause.
125      * @return `true` if the composition is complete and `false` if one or more calls to `resume`
126      *   are required to complete composition.
127      */
resumenull128     @Suppress("ExecutorRegistration") fun resume(shouldPause: ShouldPauseCallback): Boolean
129 
130     /**
131      * Apply the composition. This is the last step of a paused composition and is required to be
132      * called prior to the composition is usable.
133      *
134      * Calling [apply] should always be proceeded with a check of [isComplete] before it is called
135      * and potentially calling [resume] in a loop until [isComplete] returns `true`. This can happen
136      * if [resume] returned `true` but [apply] was not synchronously called immediately afterwords.
137      * Any state that was read that changed between when [resume] being called and [apply] being
138      * called may require the paused composition to be resumed before applied.
139      */
140     fun apply()
141 
142     /**
143      * Cancels the paused composition. This should only be used if the composition is going to be
144      * disposed and the entire composition is not going to be used.
145      */
146     fun cancel()
147 }
148 
149 /**
150  * Create a [PausableComposition]. A [PausableComposition] can create a [PausedComposition] which
151  * allows pausing and resuming the composition.
152  *
153  * @param applier The [Applier] instance to be used in the composition.
154  * @param parent The parent [CompositionContext].
155  * @see Applier
156  * @see CompositionContext
157  * @see PausableComposition
158  */
159 fun PausableComposition(applier: Applier<*>, parent: CompositionContext): PausableComposition =
160     CompositionImpl(parent, applier)
161 
162 internal enum class PausedCompositionState {
163     Invalid,
164     Cancelled,
165     InitialPending,
166     RecomposePending,
167     Recomposing,
168     ApplyPending,
169     Applied,
170 }
171 
172 internal class PausedCompositionImpl(
173     val composition: CompositionImpl,
174     val context: CompositionContext,
175     val composer: ComposerImpl,
176     abandonSet: MutableSet<RememberObserver>,
177     val content: @Composable () -> Unit,
178     val reusable: Boolean,
179     val applier: Applier<*>,
180     val lock: SynchronizedObject,
181 ) : PausedComposition {
182     private var state = PausedCompositionState.InitialPending
183     private var invalidScopes = emptyScatterSet<RecomposeScopeImpl>()
184     internal val rememberManager =
<lambda>null185         RememberEventDispatcher().apply { prepare(abandonSet, composer.errorContext) }
186     internal val pausableApplier = RecordingApplier(applier.current)
187     internal val isRecomposing
188         get() = state == PausedCompositionState.Recomposing
189 
190     override val isComplete: Boolean
191         get() = state >= PausedCompositionState.ApplyPending
192 
resumenull193     override fun resume(shouldPause: ShouldPauseCallback): Boolean {
194         try {
195             when (state) {
196                 PausedCompositionState.InitialPending -> {
197                     if (reusable) composer.startReuseFromRoot()
198                     try {
199                         invalidScopes =
200                             context.composeInitialPaused(composition, shouldPause, content)
201                     } finally {
202                         if (reusable) composer.endReuseFromRoot()
203                     }
204                     state = PausedCompositionState.RecomposePending
205                     if (invalidScopes.isEmpty()) markComplete()
206                 }
207                 PausedCompositionState.RecomposePending -> {
208                     state = PausedCompositionState.Recomposing
209                     try {
210                         invalidScopes =
211                             context.recomposePaused(composition, shouldPause, invalidScopes)
212                     } finally {
213                         state = PausedCompositionState.RecomposePending
214                     }
215                     if (invalidScopes.isEmpty()) markComplete()
216                 }
217                 PausedCompositionState.Recomposing -> {
218                     composeRuntimeError("Recursive call to resume()")
219                 }
220                 PausedCompositionState.ApplyPending ->
221                     error("Pausable composition is complete and apply() should be applied")
222                 PausedCompositionState.Applied -> error("The paused composition has been applied")
223                 PausedCompositionState.Cancelled ->
224                     error("The paused composition has been cancelled")
225                 PausedCompositionState.Invalid ->
226                     error("The paused composition is invalid because of a previous exception")
227             }
228         } catch (e: Exception) {
229             state = PausedCompositionState.Invalid
230             throw e
231         }
232         return isComplete
233     }
234 
applynull235     override fun apply() {
236         try {
237             when (state) {
238                 PausedCompositionState.InitialPending,
239                 PausedCompositionState.RecomposePending,
240                 PausedCompositionState.Recomposing ->
241                     error("The paused composition has not completed yet")
242                 PausedCompositionState.ApplyPending -> {
243                     applyChanges()
244                     state = PausedCompositionState.Applied
245                 }
246                 PausedCompositionState.Applied ->
247                     error("The paused composition has already been applied")
248                 PausedCompositionState.Cancelled ->
249                     error("The paused composition has been cancelled")
250                 PausedCompositionState.Invalid ->
251                     error("The paused composition is invalid because of a previous exception")
252             }
253         } catch (e: Exception) {
254             state = PausedCompositionState.Invalid
255             throw e
256         }
257     }
258 
cancelnull259     override fun cancel() {
260         state = PausedCompositionState.Cancelled
261         rememberManager.dispatchAbandons()
262         composition.pausedCompositionFinished()
263     }
264 
markIncompletenull265     internal fun markIncomplete() {
266         if (state == PausedCompositionState.ApplyPending)
267             state = PausedCompositionState.RecomposePending
268     }
269 
markCompletenull270     private fun markComplete() {
271         state = PausedCompositionState.ApplyPending
272     }
273 
applyChangesnull274     private fun applyChanges() {
275         synchronized(lock) {
276             @Suppress("UNCHECKED_CAST")
277             try {
278                 pausableApplier.playTo(applier as Applier<Any?>)
279                 rememberManager.dispatchRememberObservers()
280                 rememberManager.dispatchSideEffects()
281             } finally {
282                 rememberManager.dispatchAbandons()
283                 composition.pausedCompositionFinished()
284             }
285         }
286     }
287 }
288 
289 internal class RecordingApplier<N>(root: N) : Applier<N> {
290     private val operations = mutableIntListOf()
291     private val instances = mutableObjectListOf<Any?>()
292 
293     override var current: N = root
294 
downnull295     override fun down(node: N) {
296         operations.add(DOWN)
297         instances.add(node)
298     }
299 
upnull300     override fun up() {
301         operations.add(UP)
302     }
303 
removenull304     override fun remove(index: Int, count: Int) {
305         operations.add(REMOVE)
306         operations.add(index)
307         operations.add(count)
308     }
309 
movenull310     override fun move(from: Int, to: Int, count: Int) {
311         operations.add(MOVE)
312         operations.add(from)
313         operations.add(to)
314         operations.add(count)
315     }
316 
clearnull317     override fun clear() {
318         operations.add(CLEAR)
319     }
320 
insertBottomUpnull321     override fun insertBottomUp(index: Int, instance: N) {
322         operations.add(INSERT_BOTTOM_UP)
323         operations.add(index)
324         instances.add(instance)
325     }
326 
insertTopDownnull327     override fun insertTopDown(index: Int, instance: N) {
328         operations.add(INSERT_TOP_DOWN)
329         operations.add(index)
330         instances.add(instance)
331     }
332 
applynull333     override fun apply(block: N.(Any?) -> Unit, value: Any?) {
334         operations.add(APPLY)
335         instances.add(block)
336         instances.add(value)
337     }
338 
reusenull339     override fun reuse() {
340         operations.add(REUSE)
341     }
342 
playTonull343     fun playTo(applier: Applier<N>) {
344         var currentOperation = 0
345         var currentInstance = 0
346         val operations = operations
347         val size = operations.size
348         val instances = instances
349         applier.onBeginChanges()
350         try {
351             while (currentOperation < size) {
352                 val operation = operations[currentOperation++]
353                 when (operation) {
354                     UP -> {
355                         applier.up()
356                     }
357                     DOWN -> {
358                         @Suppress("UNCHECKED_CAST") val node = instances[currentInstance++] as N
359                         applier.down(node)
360                     }
361                     REMOVE -> {
362                         val index = operations[currentOperation++]
363                         val count = operations[currentOperation++]
364                         applier.remove(index, count)
365                     }
366                     MOVE -> {
367                         val from = operations[currentOperation++]
368                         val to = operations[currentOperation++]
369                         val count = operations[currentOperation++]
370                         applier.move(from, to, count)
371                     }
372                     CLEAR -> {
373                         applier.clear()
374                     }
375                     INSERT_TOP_DOWN -> {
376                         val index = operations[currentOperation++]
377 
378                         @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
379                         applier.insertTopDown(index, instance)
380                     }
381                     INSERT_BOTTOM_UP -> {
382                         val index = operations[currentOperation++]
383 
384                         @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
385                         applier.insertBottomUp(index, instance)
386                     }
387                     APPLY -> {
388                         @Suppress("UNCHECKED_CAST")
389                         val block = instances[currentInstance++] as Any?.(Any?) -> Unit
390                         val value = instances[currentInstance++]
391                         applier.apply(block, value)
392                     }
393                     REUSE -> {
394                         applier.reuse()
395                     }
396                 }
397             }
398             runtimeCheck(currentInstance == instances.size) { "Applier operation size mismatch" }
399             instances.clear()
400             operations.clear()
401         } finally {
402             applier.onEndChanges()
403         }
404     }
405 
406     // These commands need to be an integer, not just a enum value, as they are stored along side
407     // the commands integer parameters, so the values are explicitly set.
408     companion object {
409         const val UP = 0
410         const val DOWN = UP + 1
411         const val REMOVE = DOWN + 1
412         const val MOVE = REMOVE + 1
413         const val CLEAR = MOVE + 1
414         const val INSERT_BOTTOM_UP = CLEAR + 1
415         const val INSERT_TOP_DOWN = INSERT_BOTTOM_UP + 1
416         const val APPLY = INSERT_TOP_DOWN + 1
417         const val REUSE = APPLY + 1
418     }
419 }
420