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