1 /*
<lambda>null2 * Copyright 2019 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.MutableScatterSet
22 import androidx.compose.runtime.changelist.ChangeList
23 import androidx.compose.runtime.collection.ScopeMap
24 import androidx.compose.runtime.collection.fastForEach
25 import androidx.compose.runtime.internal.AtomicReference
26 import androidx.compose.runtime.internal.RememberEventDispatcher
27 import androidx.compose.runtime.internal.trace
28 import androidx.compose.runtime.platform.makeSynchronizedObject
29 import androidx.compose.runtime.platform.synchronized
30 import androidx.compose.runtime.snapshots.ReaderKind
31 import androidx.compose.runtime.snapshots.StateObjectImpl
32 import androidx.compose.runtime.snapshots.fastAll
33 import androidx.compose.runtime.snapshots.fastAny
34 import androidx.compose.runtime.snapshots.fastForEach
35 import androidx.compose.runtime.tooling.CompositionObserver
36 import androidx.compose.runtime.tooling.CompositionObserverHandle
37 import kotlin.coroutines.CoroutineContext
38 import kotlin.coroutines.EmptyCoroutineContext
39
40 /**
41 * A composition object is usually constructed for you, and returned from an API that is used to
42 * initially compose a UI. For instance, [setContent] returns a Composition.
43 *
44 * The [dispose] method should be used when you would like to dispose of the UI and the Composition.
45 */
46 interface Composition {
47 /**
48 * Returns true if any pending invalidations have been scheduled. An invalidation is schedule if
49 * [RecomposeScope.invalidate] has been called on any composition scopes create for the
50 * composition.
51 *
52 * Modifying [MutableState.value] of a value produced by [mutableStateOf] will automatically
53 * call [RecomposeScope.invalidate] for any scope that read [State.value] of the mutable state
54 * instance during composition.
55 *
56 * @see RecomposeScope
57 * @see mutableStateOf
58 */
59 val hasInvalidations: Boolean
60
61 /** True if [dispose] has been called. */
62 val isDisposed: Boolean
63
64 /**
65 * Clear the hierarchy that was created from the composition and release resources allocated for
66 * composition. After calling [dispose] the composition will no longer be recomposed and calling
67 * [setContent] will throw an [IllegalStateException]. Calling [dispose] is idempotent, all
68 * calls after the first are a no-op.
69 */
70 fun dispose()
71
72 /**
73 * Update the composition with the content described by the [content] composable. After this has
74 * been called the changes to produce the initial composition has been calculated and applied to
75 * the composition.
76 *
77 * Will throw an [IllegalStateException] if the composition has been disposed.
78 *
79 * @param content A composable function that describes the content of the composition.
80 * @exception IllegalStateException thrown in the composition has been [dispose]d.
81 */
82 fun setContent(content: @Composable () -> Unit)
83 }
84
85 /**
86 * A [ReusableComposition] is a [Composition] that can be reused for different composable content.
87 *
88 * This interface is used by components that have to synchronize lifecycle of parent and child
89 * compositions and efficiently reuse the nodes emitted by [ReusableComposeNode].
90 */
91 sealed interface ReusableComposition : Composition {
92 /**
93 * Update the composition with the content described by the [content] composable. After this has
94 * been called the changes to produce the initial composition has been calculated and applied to
95 * the composition.
96 *
97 * This method forces this composition into "reusing" state before setting content. In reusing
98 * state, all remembered content is discarded, and nodes emitted by [ReusableComposeNode] are
99 * re-used for the new content. The nodes are only reused if the group structure containing the
100 * node matches new content.
101 *
102 * Will throw an [IllegalStateException] if the composition has been disposed.
103 *
104 * @param content A composable function that describes the content of the composition.
105 * @exception IllegalStateException thrown in the composition has been [dispose]d.
106 */
setContentWithReusenull107 fun setContentWithReuse(content: @Composable () -> Unit)
108
109 /**
110 * Deactivate all observation scopes in composition and remove all remembered slots while
111 * preserving nodes in place. The composition can be re-activated by calling [setContent] with a
112 * new content.
113 */
114 fun deactivate()
115 }
116
117 /**
118 * A key to locate a service using the [CompositionServices] interface optionally implemented by
119 * implementations of [Composition].
120 */
121 interface CompositionServiceKey<T>
122
123 /**
124 * Allows finding composition services from the runtime. The services requested through this
125 * interface are internal to the runtime and cannot be provided directly.
126 *
127 * The [CompositionServices] interface is used by the runtime to provide optional and/or
128 * experimental services through public extension functions.
129 *
130 * Implementation of [Composition] that delegate to another [Composition] instance should implement
131 * this interface and delegate calls to [getCompositionService] to the original [Composition].
132 */
133 interface CompositionServices {
134 /** Find a service of class [T]. */
135 fun <T> getCompositionService(key: CompositionServiceKey<T>): T?
136 }
137
138 /**
139 * Find a Composition service.
140 *
141 * Find services that implement optional and/or experimental services provided through public or
142 * experimental extension functions.
143 */
getCompositionServicenull144 internal fun <T> Composition.getCompositionService(key: CompositionServiceKey<T>) =
145 (this as? CompositionServices)?.getCompositionService(key)
146
147 /**
148 * A controlled composition is a [Composition] that can be directly controlled by the caller.
149 *
150 * This is the interface used by the [Recomposer] to control how and when a composition is
151 * invalidated and subsequently recomposed.
152 *
153 * Normally a composition is controlled by the [Recomposer] but it is often more efficient for tests
154 * to take direct control over a composition by calling [ControlledComposition] instead of
155 * [Composition].
156 *
157 * @see ControlledComposition
158 */
159 sealed interface ControlledComposition : Composition {
160 /**
161 * True if the composition is actively compositing such as when actively in a call to
162 * [composeContent] or [recompose].
163 */
164 val isComposing: Boolean
165
166 /**
167 * True after [composeContent] or [recompose] has been called and [applyChanges] is expected as
168 * the next call. An exception will be throw in [composeContent] or [recompose] is called while
169 * there are pending from the previous composition pending to be applied.
170 */
171 val hasPendingChanges: Boolean
172
173 /**
174 * Called by the parent composition in response to calling [setContent]. After this method the
175 * changes should be calculated but not yet applied. DO NOT call this method directly if this is
176 * interface is controlled by a [Recomposer], either use [setContent] or
177 * [Recomposer.composeInitial] instead.
178 *
179 * @param content A composable function that describes the tree.
180 */
181 fun composeContent(content: @Composable () -> Unit)
182
183 /**
184 * Record the values that were modified after the last call to [recompose] or from the initial
185 * call to [composeContent]. This should be called before [recompose] is called to record which
186 * parts of the composition need to be recomposed.
187 *
188 * @param values the set of values that have changed since the last composition.
189 */
190 fun recordModificationsOf(values: Set<Any>)
191
192 /**
193 * Returns true if any of the object instances in [values] is observed by this composition. This
194 * allows detecting if values changed by a previous composition will potentially affect this
195 * composition.
196 */
197 fun observesAnyOf(values: Set<Any>): Boolean
198
199 /**
200 * Execute [block] with [isComposing] set temporarily to `true`. This allows treating
201 * invalidations reported during [prepareCompose] as if they happened while composing to avoid
202 * double invalidations when propagating changes from a parent composition while before
203 * composing the child composition.
204 */
205 fun prepareCompose(block: () -> Unit)
206
207 /**
208 * Record that [value] has been read. This is used primarily by the [Recomposer] to inform the
209 * composer when the a [MutableState] instance has been read implying it should be observed for
210 * changes.
211 *
212 * @param value the instance from which a property was read
213 */
214 fun recordReadOf(value: Any)
215
216 /**
217 * Record that [value] has been modified. This is used primarily by the [Recomposer] to inform
218 * the composer when the a [MutableState] instance been change by a composable function.
219 */
220 fun recordWriteOf(value: Any)
221
222 /**
223 * Recompose the composition to calculate any changes necessary to the composition state and the
224 * tree maintained by the applier. No changes have been made yet. Changes calculated will be
225 * applied when [applyChanges] is called.
226 *
227 * @return returns `true` if any changes are pending and [applyChanges] should be called.
228 */
229 fun recompose(): Boolean
230
231 /**
232 * Insert the given list of movable content with their paired state in potentially a different
233 * composition. If the second part of the pair is null then the movable content should be
234 * inserted as new. If second part of the pair has a value then the state should be moved into
235 * the referenced location and then recomposed there.
236 */
237 @InternalComposeApi
238 fun insertMovableContent(
239 references: List<Pair<MovableContentStateReference, MovableContentStateReference?>>
240 )
241
242 /** Dispose the value state that is no longer needed. */
243 @InternalComposeApi fun disposeUnusedMovableContent(state: MovableContentState)
244
245 /**
246 * Apply the changes calculated during [setContent] or [recompose]. If an exception is thrown by
247 * [applyChanges] the composition is irreparably damaged and should be [dispose]d.
248 */
249 fun applyChanges()
250
251 /**
252 * Apply change that must occur after the main bulk of changes have been applied. Late changes
253 * are the result of inserting movable content and it must be performed after [applyChanges]
254 * because, for content that have moved must be inserted only after it has been removed from the
255 * previous location. All deletes must be executed before inserts. To ensure this, all deletes
256 * are performed in [applyChanges] and all inserts are performed in [applyLateChanges].
257 */
258 fun applyLateChanges()
259
260 /**
261 * Call when all changes, including late changes, have been applied. This signals to the
262 * composition that any transitory composition state can now be discarded. This is advisory only
263 * and a controlled composition will execute correctly when this is not called.
264 */
265 fun changesApplied()
266
267 /**
268 * Abandon current changes and reset composition state. Called when recomposer cannot proceed
269 * with current recomposition loop and needs to reset composition.
270 */
271 fun abandonChanges()
272
273 /**
274 * Invalidate all invalidation scopes. This is called, for example, by [Recomposer] when the
275 * Recomposer becomes active after a previous period of inactivity, potentially missing more
276 * granular invalidations.
277 */
278 fun invalidateAll()
279
280 /**
281 * Throws an exception if the internal state of the composer has been corrupted and is no longer
282 * consistent. Used in testing the composer itself.
283 */
284 @InternalComposeApi fun verifyConsistent()
285
286 /**
287 * Temporarily delegate all invalidations sent to this composition to the [to] composition. This
288 * is used when movable content moves between compositions. The recompose scopes are not
289 * redirected until after the move occurs during [applyChanges] and [applyLateChanges]. This is
290 * used to compose as if the scopes have already been changed.
291 */
292 fun <R> delegateInvalidations(to: ControlledComposition?, groupIndex: Int, block: () -> R): R
293
294 /**
295 * Sets the [shouldPause] callback allowing a composition to be pausable if it is not `null`.
296 * Setting the callback to `null` disables pausing.
297 *
298 * @return the previous value of the callback which will be restored once the callback is no
299 * longer needed.
300 * @see PausableComposition
301 */
302 @Suppress("ExecutorRegistration")
303 fun getAndSetShouldPauseCallback(shouldPause: ShouldPauseCallback?): ShouldPauseCallback?
304 }
305
306 /** Utility function to set and restore a should pause callback. */
pausablenull307 internal inline fun <R> ControlledComposition.pausable(
308 shouldPause: ShouldPauseCallback,
309 block: () -> R
310 ): R {
311 val previous = getAndSetShouldPauseCallback(shouldPause)
312 return try {
313 block()
314 } finally {
315 getAndSetShouldPauseCallback(previous)
316 }
317 }
318
319 /**
320 * The [CoroutineContext] that should be used to perform concurrent recompositions of this
321 * [ControlledComposition] when used in an environment supporting concurrent composition.
322 *
323 * See [Recomposer.runRecomposeConcurrentlyAndApplyChanges] as an example of configuring such an
324 * environment.
325 */
326 // Implementation note: as/if this method graduates it should become a real method of
327 // ControlledComposition with a default implementation.
328 @ExperimentalComposeApi
329 val ControlledComposition.recomposeCoroutineContext: CoroutineContext
330 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
331 @ExperimentalComposeApi
332 get() = (this as? CompositionImpl)?.recomposeContext ?: EmptyCoroutineContext
333
334 /**
335 * This method is the way to initiate a composition. [parent] [CompositionContext] can be
336 * * provided to make the composition behave as a sub-composition of the parent. If composition does
337 * * not have a parent, [Recomposer] instance should be provided.
338 *
339 * It is important to call [Composition.dispose] when composition is no longer needed in order to
340 * release resources.
341 *
342 * @sample androidx.compose.runtime.samples.CustomTreeComposition
343 * @param applier The [Applier] instance to be used in the composition.
344 * @param parent The parent [CompositionContext].
345 * @see Applier
346 * @see Composition
347 * @see Recomposer
348 */
Compositionnull349 fun Composition(applier: Applier<*>, parent: CompositionContext): Composition =
350 CompositionImpl(parent, applier)
351
352 /**
353 * This method is the way to initiate a reusable composition. [parent] [CompositionContext] can be
354 * provided to make the composition behave as a sub-composition of the parent. If composition does
355 * not have a parent, [Recomposer] instance should be provided.
356 *
357 * It is important to call [Composition.dispose] when composition is no longer needed in order to
358 * release resources.
359 *
360 * @param applier The [Applier] instance to be used in the composition.
361 * @param parent The parent [CompositionContext].
362 * @see Applier
363 * @see ReusableComposition
364 * @see rememberCompositionContext
365 */
366 fun ReusableComposition(applier: Applier<*>, parent: CompositionContext): ReusableComposition =
367 CompositionImpl(parent, applier)
368
369 /**
370 * This method is a way to initiate a composition. Optionally, a [parent] [CompositionContext] can
371 * be provided to make the composition behave as a sub-composition of the parent or a [Recomposer]
372 * can be provided.
373 *
374 * A controlled composition allows direct control of the composition instead of it being controlled
375 * by the [Recomposer] passed ot the root composition.
376 *
377 * It is important to call [Composition.dispose] this composer is no longer needed in order to
378 * release resources.
379 *
380 * @sample androidx.compose.runtime.samples.CustomTreeComposition
381 * @param applier The [Applier] instance to be used in the composition.
382 * @param parent The parent [CompositionContext].
383 * @see Applier
384 * @see Composition
385 * @see Recomposer
386 */
387 @TestOnly
388 fun ControlledComposition(applier: Applier<*>, parent: CompositionContext): ControlledComposition =
389 CompositionImpl(parent, applier)
390
391 /**
392 * Create a [Composition] using [applier] to manage the composition, as a child of [parent].
393 *
394 * When used in a configuration that supports concurrent recomposition, hint to the environment that
395 * [recomposeCoroutineContext] should be used to perform recomposition. Recompositions will be
396 * launched into the
397 */
398 @ExperimentalComposeApi
399 fun Composition(
400 applier: Applier<*>,
401 parent: CompositionContext,
402 recomposeCoroutineContext: CoroutineContext
403 ): Composition = CompositionImpl(parent, applier, recomposeContext = recomposeCoroutineContext)
404
405 @TestOnly
406 @ExperimentalComposeApi
407 fun ControlledComposition(
408 applier: Applier<*>,
409 parent: CompositionContext,
410 recomposeCoroutineContext: CoroutineContext
411 ): ControlledComposition =
412 CompositionImpl(parent, applier, recomposeContext = recomposeCoroutineContext)
413
414 private val PendingApplyNoModifications = Any()
415
416 internal val CompositionImplServiceKey = object : CompositionServiceKey<CompositionImpl> {}
417
418 /**
419 * The implementation of the [Composition] interface.
420 *
421 * @param parent An optional reference to the parent composition.
422 * @param applier The applier to use to manage the tree built by the composer.
423 * @param recomposeContext The coroutine context to use to recompose this composition. If left
424 * `null` the controlling recomposer's default context is used.
425 */
426 @OptIn(ExperimentalComposeRuntimeApi::class)
427 internal class CompositionImpl(
428 /**
429 * The parent composition from [rememberCompositionContext], for sub-compositions, or the an
430 * instance of [Recomposer] for root compositions.
431 */
432 @get:TestOnly val parent: CompositionContext,
433
434 /** The applier to use to update the tree managed by the composition. */
435 private val applier: Applier<*>,
436 recomposeContext: CoroutineContext? = null
437 ) :
438 ControlledComposition,
439 ReusableComposition,
440 RecomposeScopeOwner,
441 CompositionServices,
442 PausableComposition {
443
444 /**
445 * `null` if a composition isn't pending to apply. `Set<Any>` or `Array<Set<Any>>` if there are
446 * modifications to record [PendingApplyNoModifications] if a composition is pending to apply,
447 * no modifications. any set contents will be sent to [recordModificationsOf] after applying
448 * changes before releasing [lock]
449 */
450 private val pendingModifications = AtomicReference<Any?>(null)
451
452 // Held when making changes to self or composer
453 private val lock = makeSynchronizedObject()
454
455 /**
456 * A set of remember observers that were potentially abandoned between [composeContent] or
457 * [recompose] and [applyChanges]. When inserting new content any newly remembered
458 * [RememberObserver]s are added to this set and then removed as [RememberObserver.onRemembered]
459 * is dispatched. If any are left in this when exiting [applyChanges] they have been abandoned
460 * and are sent an [RememberObserver.onAbandoned] notification.
461 */
462 @Suppress("AsCollectionCall") // Requires iterator API when dispatching abandons
463 private val abandonSet = MutableScatterSet<RememberObserver>().asMutableSet()
464
465 /** The slot table is used to store the composition information required for recomposition. */
466 @Suppress("MemberVisibilityCanBePrivate") // published as internal
467 internal val slotTable =
<lambda>null468 SlotTable().also {
469 if (parent.collectingCallByInformation) it.collectCalledByInformation()
470 if (parent.collectingSourceInformation) it.collectSourceInformation()
471 }
472
473 /**
474 * A map of observable objects to the [RecomposeScope]s that observe the object. If the key
475 * object is modified the associated scopes should be invalidated.
476 */
477 private val observations = ScopeMap<Any, RecomposeScopeImpl>()
478
479 /** Used for testing. Returns the objects that are observed */
480 internal val observedObjects
481 @TestOnly @Suppress("AsCollectionCall") get() = observations.map.asMap().keys
482
483 /**
484 * A set of scopes that were invalidated by a call from [recordModificationsOf]. This set is
485 * only used in [addPendingInvalidationsLocked], and is reused between invocations.
486 */
487 private val invalidatedScopes = MutableScatterSet<RecomposeScopeImpl>()
488
489 /**
490 * A set of scopes that were invalidated conditionally (that is they were invalidated by a
491 * [derivedStateOf] object) by a call from [recordModificationsOf]. They need to be held in the
492 * [observations] map until invalidations are drained for composition as a later call to
493 * [recordModificationsOf] might later cause them to be unconditionally invalidated.
494 */
495 private val conditionallyInvalidatedScopes = MutableScatterSet<RecomposeScopeImpl>()
496
497 /** A map of object read during derived states to the corresponding derived state. */
498 private val derivedStates = ScopeMap<Any, DerivedState<*>>()
499
500 /** Used for testing. Returns dependencies of derived states that are currently observed. */
501 internal val derivedStateDependencies
502 @TestOnly @Suppress("AsCollectionCall") get() = derivedStates.map.asMap().keys
503
504 /** Used for testing. Returns the conditional scopes being tracked by the composer */
505 internal val conditionalScopes: List<RecomposeScopeImpl>
506 @TestOnly
507 @Suppress("AsCollectionCall")
508 get() = conditionallyInvalidatedScopes.asSet().toList()
509
510 /**
511 * A list of changes calculated by [Composer] to be applied to the [Applier] and the [SlotTable]
512 * to reflect the result of composition. This is a list of lambdas that need to be invoked in
513 * order to produce the desired effects.
514 */
515 private val changes = ChangeList()
516
517 /**
518 * A list of changes calculated by [Composer] to be applied after all other compositions have
519 * had [applyChanges] called. These changes move [MovableContent] state from one composition to
520 * another and must be applied after [applyChanges] because [applyChanges] copies and removes
521 * the state out of the previous composition so it can be inserted into the new location. As
522 * inserts might be earlier in the composition than the position it is deleted, this move must
523 * be done in two phases.
524 */
525 private val lateChanges = ChangeList()
526
527 /**
528 * When an observable object is modified during composition any recompose scopes that are
529 * observing that object are invalidated immediately. Since they have already been processed
530 * there is no need to process them again, so this set maintains a set of the recompose scopes
531 * that were already dismissed by composition and should be ignored in the next call to
532 * [recordModificationsOf].
533 */
534 private val observationsProcessed = ScopeMap<Any, RecomposeScopeImpl>()
535
536 /**
537 * A map of the invalid [RecomposeScope]s. If this map is non-empty the current state of the
538 * composition does not reflect the current state of the objects it observes and should be
539 * recomposed by calling [recompose]. Tbe value is a map of values that invalidated the scope.
540 * The scope is checked with these instances to ensure the value has changed. This is used to
541 * only invalidate the scope if a [derivedStateOf] object changes.
542 */
543 private var invalidations = ScopeMap<RecomposeScopeImpl, Any>()
544
545 /**
546 * As [RecomposeScope]s are removed the corresponding entries in the observations set must be
547 * removed as well. This process is expensive so should only be done if it is certain the
548 * [observations] set contains [RecomposeScope] that is no longer needed. [pendingInvalidScopes]
549 * is set to true whenever a [RecomposeScope] is removed from the [slotTable].
550 */
551 @Suppress("MemberVisibilityCanBePrivate") // published as internal
552 internal var pendingInvalidScopes = false
553
554 /**
555 * If the [shouldPause] callback is set the composition is pausable and should pause whenever
556 * the [shouldPause] callback returns `true`.
557 */
558 private var shouldPause: ShouldPauseCallback? = null
559
560 private var pendingPausedComposition: PausedCompositionImpl? = null
561
562 private var invalidationDelegate: CompositionImpl? = null
563
564 private var invalidationDelegateGroup: Int = 0
565
566 internal val observerHolder = CompositionObserverHolder()
567
568 private val rememberManager = RememberEventDispatcher()
569
570 /** The [Composer] to use to create and update the tree managed by this composition. */
571 internal val composer: ComposerImpl =
572 ComposerImpl(
573 applier = applier,
574 parentContext = parent,
575 slotTable = slotTable,
576 abandonSet = abandonSet,
577 changes = changes,
578 lateChanges = lateChanges,
579 composition = this
580 )
<lambda>null581 .also { parent.registerComposer(it) }
582
583 /** The [CoroutineContext] override, if there is one, for this composition. */
584 private val _recomposeContext: CoroutineContext? = recomposeContext
585
586 /** the [CoroutineContext] to use to [recompose] this composition. */
587 val recomposeContext: CoroutineContext
588 get() = _recomposeContext ?: parent.recomposeCoroutineContext
589
590 /** Return true if this is a root (non-sub-) composition. */
591 val isRoot: Boolean = parent is Recomposer
592
593 /** True if [dispose] has been called. */
594 private var disposed = false
595
596 /** True if a sub-composition of this composition is current composing. */
597 private val areChildrenComposing
598 get() = composer.areChildrenComposing
599
600 /**
601 * The [Composable] function used to define the tree managed by this composition. This is set by
602 * [setContent].
603 */
604 var composable: @Composable () -> Unit = {}
605
606 override val isComposing: Boolean
607 get() = composer.isComposing
608
609 override val isDisposed: Boolean
610 get() = disposed
611
612 override val hasPendingChanges: Boolean
<lambda>null613 get() = synchronized(lock) { composer.hasPendingChanges }
614
615 override fun setContent(content: @Composable () -> Unit) {
<lambda>null616 checkPrecondition(pendingPausedComposition == null) {
617 "A pausable composition is in progress"
618 }
619 composeInitial(content)
620 }
621
622 override fun setContentWithReuse(content: @Composable () -> Unit) {
<lambda>null623 checkPrecondition(pendingPausedComposition == null) {
624 "A pausable composition is in progress"
625 }
626 composer.startReuseFromRoot()
627
628 composeInitial(content)
629
630 composer.endReuseFromRoot()
631 }
632
633 override fun setPausableContent(content: @Composable () -> Unit): PausedComposition {
<lambda>null634 checkPrecondition(!disposed) { "The composition is disposed" }
<lambda>null635 checkPrecondition(pendingPausedComposition == null) {
636 "A pausable composition is in progress"
637 }
638 val pausedComposition =
639 PausedCompositionImpl(
640 composition = this,
641 context = parent,
642 composer = composer,
643 content = content,
644 reusable = false,
645 abandonSet = abandonSet,
646 applier = applier,
647 lock = lock,
648 )
649 pendingPausedComposition = pausedComposition
650 return pausedComposition
651 }
652
653 override fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition {
<lambda>null654 checkPrecondition(!disposed) { "The composition is disposed" }
<lambda>null655 checkPrecondition(pendingPausedComposition == null) {
656 "A pausable composition is in progress"
657 }
658 val pausedComposition =
659 PausedCompositionImpl(
660 composition = this,
661 context = parent,
662 composer = composer,
663 content = content,
664 reusable = true,
665 abandonSet = abandonSet,
666 applier = applier,
667 lock = lock,
668 )
669 pendingPausedComposition = pausedComposition
670 return pausedComposition
671 }
672
pausedCompositionFinishednull673 internal fun pausedCompositionFinished() {
674 pendingPausedComposition = null
675 }
676
composeInitialnull677 private fun composeInitial(content: @Composable () -> Unit) {
678 checkPrecondition(!disposed) { "The composition is disposed" }
679 this.composable = content
680 parent.composeInitial(this, composable)
681 }
682
683 @OptIn(ExperimentalComposeRuntimeApi::class)
observenull684 internal fun observe(observer: CompositionObserver): CompositionObserverHandle {
685 synchronized(lock) {
686 observerHolder.observer = observer
687 observerHolder.root = true
688 }
689 return object : CompositionObserverHandle {
690 override fun dispose() {
691 synchronized(lock) {
692 if (observerHolder.observer == observer) {
693 observerHolder.observer = null
694 observerHolder.root = false
695 }
696 }
697 }
698 }
699 }
700
invalidateGroupsWithKeynull701 fun invalidateGroupsWithKey(key: Int) {
702 val scopesToInvalidate = synchronized(lock) { slotTable.invalidateGroupsWithKey(key) }
703 // Calls to invalidate must be performed without the lock as the they may cause the
704 // recomposer to take its lock to respond to the invalidation and that takes the locks
705 // in the opposite order of composition so if composition begins in another thread taking
706 // the recomposer lock with the composer lock held will deadlock.
707 val forceComposition =
708 scopesToInvalidate == null ||
709 scopesToInvalidate.fastAny {
710 it.invalidateForResult(null) == InvalidationResult.IGNORED
711 }
712 if (forceComposition && composer.forceRecomposeScopes()) {
713 parent.invalidate(this)
714 }
715 }
716
717 @Suppress("UNCHECKED_CAST")
drainPendingModificationsForCompositionLockednull718 private fun drainPendingModificationsForCompositionLocked() {
719 // Recording modifications may race for lock. If there are pending modifications
720 // and we won the lock race, drain them before composing.
721 when (val toRecord = pendingModifications.getAndSet(PendingApplyNoModifications)) {
722 null -> {
723 // Do nothing, just start composing.
724 }
725 PendingApplyNoModifications -> {
726 composeRuntimeError("pending composition has not been applied")
727 }
728 is Set<*> -> {
729 addPendingInvalidationsLocked(toRecord as Set<Any>, forgetConditionalScopes = true)
730 }
731 is Array<*> ->
732 for (changed in toRecord as Array<Set<Any>>) {
733 addPendingInvalidationsLocked(changed, forgetConditionalScopes = true)
734 }
735 else -> composeRuntimeError("corrupt pendingModifications drain: $pendingModifications")
736 }
737 }
738
739 @Suppress("UNCHECKED_CAST")
drainPendingModificationsLockednull740 private fun drainPendingModificationsLocked() {
741 when (val toRecord = pendingModifications.getAndSet(null)) {
742 PendingApplyNoModifications -> {
743 // No work to do
744 }
745 is Set<*> -> {
746 addPendingInvalidationsLocked(toRecord as Set<Any>, forgetConditionalScopes = false)
747 }
748 is Array<*> ->
749 for (changed in toRecord as Array<Set<Any>>) {
750 addPendingInvalidationsLocked(changed, forgetConditionalScopes = false)
751 }
752 null ->
753 composeRuntimeError(
754 "calling recordModificationsOf and applyChanges concurrently is not supported"
755 )
756 else -> composeRuntimeError("corrupt pendingModifications drain: $pendingModifications")
757 }
758 }
759
760 // Drain the modification out of the normal recordModificationsOf(), composition() cycle.
761 // This avoids the checks to make sure the two calls are called in order.
762 @Suppress("UNCHECKED_CAST")
drainPendingModificationsOutOfBandLockednull763 private fun drainPendingModificationsOutOfBandLocked() {
764 when (val toRecord = pendingModifications.getAndSet(emptySet<Any>())) {
765 PendingApplyNoModifications,
766 null -> {
767 // No work to do
768 }
769 is Set<*> -> {
770 addPendingInvalidationsLocked(toRecord as Set<Any>, forgetConditionalScopes = false)
771 }
772 is Array<*> ->
773 for (changed in toRecord as Array<Set<Any>>) {
774 addPendingInvalidationsLocked(changed, forgetConditionalScopes = false)
775 }
776 else -> composeRuntimeError("corrupt pendingModifications drain: $pendingModifications")
777 }
778 }
779
780 override fun composeContent(content: @Composable () -> Unit) {
781 // TODO: This should raise a signal to any currently running recompose calls
782 // to halt and return
<lambda>null783 guardChanges {
784 synchronized(lock) {
785 drainPendingModificationsForCompositionLocked()
786 guardInvalidationsLocked { invalidations ->
787 val observer = observer()
788 if (observer != null) {
789 @Suppress("UNCHECKED_CAST")
790 observer.onBeginComposition(
791 this,
792 invalidations.asMap() as Map<RecomposeScope, Set<Any>>
793 )
794 }
795 composer.composeContent(invalidations, content, shouldPause)
796 observer?.onEndComposition(this)
797 }
798 }
799 }
800 }
801
updateMovingInvalidationsnull802 internal fun updateMovingInvalidations() {
803 synchronized(lock) {
804 drainPendingModificationsOutOfBandLocked()
805 guardInvalidationsLocked { invalidations ->
806 composer.updateComposerInvalidations(invalidations)
807 }
808 }
809 }
810
disposenull811 override fun dispose() {
812 synchronized(lock) {
813 checkPrecondition(!composer.isComposing) {
814 "Composition is disposed while composing. If dispose is triggered by a call in " +
815 "@Composable function, consider wrapping it with SideEffect block."
816 }
817 if (!disposed) {
818 disposed = true
819 composable = {}
820
821 // Changes are deferred if the composition contains movable content that needs
822 // to be released. NOTE: Applying these changes leaves the slot table in
823 // potentially invalid state. The routine use to produce this change list reuses
824 // code that extracts movable content from groups that are being deleted. This code
825 // does not bother to correctly maintain the node counts of a group nested groups
826 // that are going to be removed anyway so the node counts of the groups affected
827 // are might be incorrect after the changes have been applied.
828 val deferredChanges = composer.deferredChanges
829 if (deferredChanges != null) {
830 applyChangesInLocked(deferredChanges)
831 }
832
833 // Dispatch all the `onForgotten` events for object that are no longer part of a
834 // composition because this composition is being discarded. It is important that
835 // this is done after applying deferred changes above to avoid sending `
836 // onForgotten` notification to objects that are still part of movable content that
837 // will be moved to a new location.
838 val nonEmptySlotTable = slotTable.groupsSize > 0
839 if (nonEmptySlotTable || abandonSet.isNotEmpty()) {
840 rememberManager.use(abandonSet, composer.errorContext) {
841 if (nonEmptySlotTable) {
842 applier.onBeginChanges()
843 slotTable.write { writer -> writer.removeCurrentGroup(rememberManager) }
844 applier.clear()
845 applier.onEndChanges()
846 dispatchRememberObservers()
847 }
848 dispatchAbandons()
849 }
850 }
851 composer.dispose()
852 }
853 }
854 parent.unregisterComposition(this)
855 }
856
857 override val hasInvalidations
<lambda>null858 get() = synchronized(lock) { invalidations.size > 0 }
859
860 /**
861 * To bootstrap multithreading handling, recording modifications is now deferred between
862 * recomposition with changes to apply and the application of those changes.
863 * [pendingModifications] will contain a queue of changes to apply once all current changes have
864 * been successfully processed. Draining this queue is the responsibility of [recompose] if it
865 * would return `false` (changes do not need to be applied) or [applyChanges].
866 */
867 @Suppress("UNCHECKED_CAST")
recordModificationsOfnull868 override fun recordModificationsOf(values: Set<Any>) {
869 while (true) {
870 val old = pendingModifications.get()
871 val new: Any =
872 when (old) {
873 null,
874 PendingApplyNoModifications -> values
875 is Set<*> -> arrayOf(old, values)
876 is Array<*> -> (old as Array<Set<Any>>) + values
877 else -> error("corrupt pendingModifications: $pendingModifications")
878 }
879 if (pendingModifications.compareAndSet(old, new)) {
880 if (old == null) {
881 synchronized(lock) { drainPendingModificationsLocked() }
882 }
883 break
884 }
885 }
886 }
887
observesAnyOfnull888 override fun observesAnyOf(values: Set<Any>): Boolean {
889 values.fastForEach { value ->
890 if (value in observations || value in derivedStates) return true
891 }
892 return false
893 }
894
prepareComposenull895 override fun prepareCompose(block: () -> Unit) = composer.prepareCompose(block)
896
897 private fun addPendingInvalidationsLocked(value: Any, forgetConditionalScopes: Boolean) {
898 observations.forEachScopeOf(value) { scope ->
899 if (
900 !observationsProcessed.remove(value, scope) &&
901 scope.invalidateForResult(value) != InvalidationResult.IGNORED
902 ) {
903 if (scope.isConditional && !forgetConditionalScopes) {
904 conditionallyInvalidatedScopes.add(scope)
905 } else {
906 invalidatedScopes.add(scope)
907 }
908 }
909 }
910 }
911
addPendingInvalidationsLockednull912 private fun addPendingInvalidationsLocked(values: Set<Any>, forgetConditionalScopes: Boolean) {
913 values.fastForEach { value ->
914 if (value is RecomposeScopeImpl) {
915 value.invalidateForResult(null)
916 } else {
917 addPendingInvalidationsLocked(value, forgetConditionalScopes)
918 derivedStates.forEachScopeOf(value) {
919 addPendingInvalidationsLocked(it, forgetConditionalScopes)
920 }
921 }
922 }
923
924 val conditionallyInvalidatedScopes = conditionallyInvalidatedScopes
925 val invalidatedScopes = invalidatedScopes
926 if (forgetConditionalScopes && conditionallyInvalidatedScopes.isNotEmpty()) {
927 observations.removeScopeIf { scope ->
928 scope in conditionallyInvalidatedScopes || scope in invalidatedScopes
929 }
930 conditionallyInvalidatedScopes.clear()
931 cleanUpDerivedStateObservations()
932 } else if (invalidatedScopes.isNotEmpty()) {
933 observations.removeScopeIf { scope -> scope in invalidatedScopes }
934 cleanUpDerivedStateObservations()
935 invalidatedScopes.clear()
936 }
937 }
938
cleanUpDerivedStateObservationsnull939 private fun cleanUpDerivedStateObservations() {
940 derivedStates.removeScopeIf { derivedState -> derivedState !in observations }
941 if (conditionallyInvalidatedScopes.isNotEmpty()) {
942 conditionallyInvalidatedScopes.removeIf { scope -> !scope.isConditional }
943 }
944 }
945
recordReadOfnull946 override fun recordReadOf(value: Any) {
947 // Not acquiring lock since this happens during composition with it already held
948 if (!areChildrenComposing) {
949 composer.currentRecomposeScope?.let {
950 it.used = true
951 val alreadyRead = it.recordRead(value)
952 if (!alreadyRead) {
953 if (value is StateObjectImpl) {
954 value.recordReadIn(ReaderKind.Composition)
955 }
956
957 observations.add(value, it)
958
959 // Record derived state dependency mapping
960 if (value is DerivedState<*>) {
961 val record = value.currentRecord
962 derivedStates.removeScope(value)
963 record.dependencies.forEachKey { dependency ->
964 if (dependency is StateObjectImpl) {
965 dependency.recordReadIn(ReaderKind.Composition)
966 }
967 derivedStates.add(dependency, value)
968 }
969 it.recordDerivedStateValue(value, record.currentValue)
970 }
971 }
972 }
973 }
974 }
975
invalidateScopeOfLockednull976 private fun invalidateScopeOfLocked(value: Any) {
977 // Invalidate any recompose scopes that read this value.
978 observations.forEachScopeOf(value) { scope ->
979 if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) {
980 // If we process this during recordWriteOf, ignore it when recording modifications
981 observationsProcessed.add(value, scope)
982 }
983 }
984 }
985
recordWriteOfnull986 override fun recordWriteOf(value: Any) =
987 synchronized(lock) {
988 invalidateScopeOfLocked(value)
989
990 // If writing to dependency of a derived value and the value is changed, invalidate the
991 // scopes that read the derived value.
992 derivedStates.forEachScopeOf(value) { invalidateScopeOfLocked(it) }
993 }
994
recomposenull995 override fun recompose(): Boolean =
996 synchronized(lock) {
997 drainPendingModificationsForCompositionLocked()
998 val pendingPausedComposition = pendingPausedComposition
999 if (pendingPausedComposition != null && !pendingPausedComposition.isRecomposing) {
1000 // If the composition is pending do not recompose it now as the recomposition
1001 // is in the control of the pausable composition and is supposed to happen when
1002 // the resume is called. However, this may cause the pausable composition to go
1003 // revert to an incomplete state. If isRecomposing is true then this is being
1004 // called in resume()
1005 pendingPausedComposition.markIncomplete()
1006 return false
1007 }
1008 guardChanges {
1009 guardInvalidationsLocked { invalidations ->
1010 val observer = observer()
1011 @Suppress("UNCHECKED_CAST")
1012 observer?.onBeginComposition(
1013 this,
1014 invalidations.asMap() as Map<RecomposeScope, Set<Any>>
1015 )
1016 composer.recompose(invalidations, shouldPause).also { shouldDrain ->
1017 // Apply would normally do this for us; do it now if apply shouldn't happen.
1018 if (!shouldDrain) drainPendingModificationsLocked()
1019 observer?.onEndComposition(this)
1020 }
1021 }
1022 }
1023 }
1024
insertMovableContentnull1025 override fun insertMovableContent(
1026 references: List<Pair<MovableContentStateReference, MovableContentStateReference?>>
1027 ) {
1028 runtimeCheck(references.fastAll { it.first.composition == this })
1029 guardChanges { composer.insertMovableContentReferences(references) }
1030 }
1031
disposeUnusedMovableContentnull1032 override fun disposeUnusedMovableContent(state: MovableContentState) {
1033 rememberManager.use(abandonSet, composer.errorContext) {
1034 val slotTable = state.slotTable
1035 slotTable.write { writer -> writer.removeCurrentGroup(rememberManager) }
1036 dispatchRememberObservers()
1037 }
1038 }
1039
applyChangesInLockednull1040 private fun applyChangesInLocked(changes: ChangeList) {
1041 rememberManager.prepare(abandonSet, composer.errorContext)
1042 try {
1043 if (changes.isEmpty()) return
1044 trace("Compose:applyChanges") {
1045 val applier = pendingPausedComposition?.pausableApplier ?: applier
1046 val rememberManager = pendingPausedComposition?.rememberManager ?: rememberManager
1047 applier.onBeginChanges()
1048
1049 // Apply all changes
1050 slotTable.write { slots ->
1051 changes.executeAndFlushAllPendingChanges(
1052 applier,
1053 slots,
1054 rememberManager,
1055 composer.errorContext
1056 )
1057 }
1058 applier.onEndChanges()
1059 }
1060
1061 // Side effects run after lifecycle observers so that any remembered objects
1062 // that implement RememberObserver receive onRemembered before a side effect
1063 // that captured it and operates on it can run.
1064 rememberManager.dispatchRememberObservers()
1065 rememberManager.dispatchSideEffects()
1066
1067 if (pendingInvalidScopes) {
1068 trace("Compose:unobserve") {
1069 pendingInvalidScopes = false
1070 observations.removeScopeIf { scope -> !scope.valid }
1071 cleanUpDerivedStateObservations()
1072 }
1073 }
1074 } finally {
1075 // Only dispatch abandons if we do not have any late changes or pending paused
1076 // compositions. The instances in the abandon set can be remembered in the late changes
1077 // or when the paused composition is applied.
1078 try {
1079 if (this.lateChanges.isEmpty() && pendingPausedComposition == null) {
1080 rememberManager.dispatchAbandons()
1081 }
1082 } finally {
1083 rememberManager.clear()
1084 }
1085 }
1086 }
1087
applyChangesnull1088 override fun applyChanges() {
1089 synchronized(lock) {
1090 guardChanges {
1091 applyChangesInLocked(changes)
1092 drainPendingModificationsLocked()
1093 }
1094 }
1095 }
1096
applyLateChangesnull1097 override fun applyLateChanges() {
1098 synchronized(lock) {
1099 guardChanges {
1100 if (lateChanges.isNotEmpty()) {
1101 applyChangesInLocked(lateChanges)
1102 }
1103 }
1104 }
1105 }
1106
changesAppliednull1107 override fun changesApplied() {
1108 synchronized(lock) {
1109 guardChanges {
1110 composer.changesApplied()
1111
1112 // By this time all abandon objects should be notified that they have been
1113 // abandoned.
1114 if (this.abandonSet.isNotEmpty()) {
1115 rememberManager.use(abandonSet, traceContext = composer.errorContext) {
1116 dispatchAbandons()
1117 }
1118 }
1119 }
1120 }
1121 }
1122
guardInvalidationsLockednull1123 private inline fun <T> guardInvalidationsLocked(
1124 block: (changes: ScopeMap<RecomposeScopeImpl, Any>) -> T
1125 ): T {
1126 val invalidations = takeInvalidations()
1127 return try {
1128 block(invalidations)
1129 } catch (e: Throwable) {
1130 this.invalidations = invalidations
1131 throw e
1132 }
1133 }
1134
guardChangesnull1135 private inline fun <T> guardChanges(block: () -> T): T =
1136 try {
1137 trackAbandonedValues(block)
1138 } catch (e: Throwable) {
1139 abandonChanges()
1140 throw e
1141 }
1142
abandonChangesnull1143 override fun abandonChanges() {
1144 pendingModifications.set(null)
1145 changes.clear()
1146 lateChanges.clear()
1147
1148 if (abandonSet.isNotEmpty()) {
1149 rememberManager.use(abandonSet, composer.errorContext) { dispatchAbandons() }
1150 }
1151 }
1152
invalidateAllnull1153 override fun invalidateAll() {
1154 synchronized(lock) { slotTable.slots.forEach { (it as? RecomposeScopeImpl)?.invalidate() } }
1155 }
1156
verifyConsistentnull1157 override fun verifyConsistent() {
1158 synchronized(lock) {
1159 if (!isComposing) {
1160 composer.verifyConsistent()
1161 slotTable.verifyWellFormed()
1162 validateRecomposeScopeAnchors(slotTable)
1163 }
1164 }
1165 }
1166
delegateInvalidationsnull1167 override fun <R> delegateInvalidations(
1168 to: ControlledComposition?,
1169 groupIndex: Int,
1170 block: () -> R
1171 ): R {
1172 return if (to != null && to != this && groupIndex >= 0) {
1173 invalidationDelegate = to as CompositionImpl
1174 invalidationDelegateGroup = groupIndex
1175 try {
1176 block()
1177 } finally {
1178 invalidationDelegate = null
1179 invalidationDelegateGroup = 0
1180 }
1181 } else block()
1182 }
1183
getAndSetShouldPauseCallbacknull1184 override fun getAndSetShouldPauseCallback(
1185 shouldPause: ShouldPauseCallback?
1186 ): ShouldPauseCallback? {
1187 val previous = this.shouldPause
1188 this.shouldPause = shouldPause
1189 return previous
1190 }
1191
invalidatenull1192 override fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
1193 if (scope.defaultsInScope) {
1194 scope.defaultsInvalid = true
1195 }
1196 val anchor = scope.anchor
1197 if (anchor == null || !anchor.valid)
1198 return InvalidationResult.IGNORED // The scope was removed from the composition
1199 if (!slotTable.ownsAnchor(anchor)) {
1200 // The scope might be owned by the delegate
1201 val delegate = synchronized(lock) { invalidationDelegate }
1202 if (delegate?.tryImminentInvalidation(scope, instance) == true)
1203 return InvalidationResult.IMMINENT // The scope was owned by the delegate
1204
1205 return InvalidationResult.IGNORED // The scope has not yet entered the composition
1206 }
1207 if (!scope.canRecompose)
1208 return InvalidationResult.IGNORED // The scope isn't able to be recomposed/invalidated
1209 return invalidateChecked(scope, anchor, instance)
1210 }
1211
recomposeScopeReleasednull1212 override fun recomposeScopeReleased(scope: RecomposeScopeImpl) {
1213 pendingInvalidScopes = true
1214 }
1215
1216 @Suppress("UNCHECKED_CAST")
getCompositionServicenull1217 override fun <T> getCompositionService(key: CompositionServiceKey<T>): T? =
1218 if (key == CompositionImplServiceKey) this as T else null
1219
1220 private fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean =
1221 isComposing && composer.tryImminentInvalidation(scope, instance)
1222
1223 private fun invalidateChecked(
1224 scope: RecomposeScopeImpl,
1225 anchor: Anchor,
1226 instance: Any?
1227 ): InvalidationResult {
1228 val delegate =
1229 synchronized(lock) {
1230 val delegate =
1231 invalidationDelegate?.let { changeDelegate ->
1232 // Invalidations are delegated when recomposing changes to movable content
1233 // that
1234 // is destined to be moved. The movable content is composed in the
1235 // destination
1236 // composer but all the recompose scopes point the current composer and will
1237 // arrive
1238 // here. this redirects the invalidations that will be moved to the
1239 // destination
1240 // composer instead of recording an invalid invalidation in the from
1241 // composer.
1242 if (slotTable.groupContainsAnchor(invalidationDelegateGroup, anchor)) {
1243 changeDelegate
1244 } else null
1245 }
1246 if (delegate == null) {
1247 if (tryImminentInvalidation(scope, instance)) {
1248 // The invalidation was redirected to the composer.
1249 return InvalidationResult.IMMINENT
1250 }
1251
1252 // Observer requires a map of scope -> states, so we have to fill it if observer
1253 // is set.
1254 val observer = observer()
1255 if (instance == null) {
1256 // invalidations[scope] containing ScopeInvalidated means it was invalidated
1257 // unconditionally.
1258 invalidations.set(scope, ScopeInvalidated)
1259 } else if (observer == null && instance !is DerivedState<*>) {
1260 // If observer is not set, we only need to add derived states to
1261 // invalidation,
1262 // as regular states are always going to invalidate.
1263 invalidations.set(scope, ScopeInvalidated)
1264 } else {
1265 if (!invalidations.anyScopeOf(scope) { it === ScopeInvalidated }) {
1266 invalidations.add(scope, instance)
1267 }
1268 }
1269 }
1270 delegate
1271 }
1272
1273 // We call through the delegate here to ensure we don't nest synchronization scopes.
1274 if (delegate != null) {
1275 return delegate.invalidateChecked(scope, anchor, instance)
1276 }
1277 parent.invalidate(this)
1278 return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED
1279 }
1280
removeObservationnull1281 internal fun removeObservation(instance: Any, scope: RecomposeScopeImpl) {
1282 observations.remove(instance, scope)
1283 }
1284
removeDerivedStateObservationnull1285 internal fun removeDerivedStateObservation(state: DerivedState<*>) {
1286 // remove derived state if it is not observed in other scopes
1287 if (state !in observations) {
1288 derivedStates.removeScope(state)
1289 }
1290 }
1291
1292 /**
1293 * This takes ownership of the current invalidations and sets up a new array map to hold the new
1294 * invalidations.
1295 */
takeInvalidationsnull1296 private fun takeInvalidations(): ScopeMap<RecomposeScopeImpl, Any> {
1297 val invalidations = invalidations
1298 this.invalidations = ScopeMap()
1299 return invalidations
1300 }
1301
1302 /**
1303 * Helper for [verifyConsistent] to ensure the anchor match there respective invalidation
1304 * scopes.
1305 */
validateRecomposeScopeAnchorsnull1306 private fun validateRecomposeScopeAnchors(slotTable: SlotTable) {
1307 val scopes = slotTable.slots.mapNotNull { it as? RecomposeScopeImpl }
1308 scopes.fastForEach { scope ->
1309 scope.anchor?.let { anchor ->
1310 checkPrecondition(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) {
1311 val dataIndex = slotTable.slots.indexOf(scope)
1312 "Misaligned anchor $anchor in scope $scope encountered, scope found at " +
1313 "$dataIndex"
1314 }
1315 }
1316 }
1317 }
1318
trackAbandonedValuesnull1319 private inline fun <T> trackAbandonedValues(block: () -> T): T {
1320 var success = false
1321 return try {
1322 block().also { success = true }
1323 } finally {
1324 if (!success && abandonSet.isNotEmpty()) {
1325 rememberManager.use(abandonSet, composer.errorContext) { dispatchAbandons() }
1326 }
1327 }
1328 }
1329
observernull1330 private fun observer(): CompositionObserver? {
1331 val holder = observerHolder
1332
1333 return if (holder.root) {
1334 holder.observer
1335 } else {
1336 val parentHolder = parent.observerHolder
1337 val parentObserver = parentHolder?.observer
1338 if (parentObserver != holder.observer) {
1339 holder.observer = parentObserver
1340 }
1341 parentObserver
1342 }
1343 }
1344
deactivatenull1345 override fun deactivate() {
1346 synchronized(lock) {
1347 val nonEmptySlotTable = slotTable.groupsSize > 0
1348 if (nonEmptySlotTable || abandonSet.isNotEmpty()) {
1349 trace("Compose:deactivate") {
1350 rememberManager.use(abandonSet, composer.errorContext) {
1351 if (nonEmptySlotTable) {
1352 applier.onBeginChanges()
1353 slotTable.write { writer ->
1354 writer.deactivateCurrentGroup(rememberManager)
1355 }
1356 applier.onEndChanges()
1357 dispatchRememberObservers()
1358 }
1359 dispatchAbandons()
1360 }
1361 }
1362 }
1363 observations.clear()
1364 derivedStates.clear()
1365 invalidations.clear()
1366 changes.clear()
1367 lateChanges.clear()
1368 composer.deactivate()
1369 }
1370 }
1371
1372 // This is only used in tests to ensure the stacks do not silently leak.
composerStacksSizesnull1373 internal fun composerStacksSizes(): Int = composer.stacksSize()
1374 }
1375
1376 internal object ScopeInvalidated
1377
1378 @ExperimentalComposeRuntimeApi
1379 internal class CompositionObserverHolder(
1380 var observer: CompositionObserver? = null,
1381 var root: Boolean = false,
1382 )
1383