| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| @file:OptIn(InternalComposeApi::class) |
| package androidx.compose.runtime |
| |
| import androidx.compose.runtime.collection.IdentityArrayMap |
| import androidx.compose.runtime.collection.IdentityArraySet |
| import androidx.compose.runtime.collection.IdentityScopeMap |
| import androidx.compose.runtime.collection.fastForEach |
| import androidx.compose.runtime.snapshots.fastAll |
| import androidx.compose.runtime.snapshots.fastAny |
| import androidx.compose.runtime.snapshots.fastForEach |
| import kotlin.coroutines.CoroutineContext |
| import kotlin.coroutines.EmptyCoroutineContext |
| |
| /** |
| * A composition object is usually constructed for you, and returned from an API that |
| * is used to initially compose a UI. For instance, [setContent] returns a Composition. |
| * |
| * The [dispose] method should be used when you would like to dispose of the UI and |
| * the Composition. |
| */ |
| interface Composition { |
| /** |
| * Returns true if any pending invalidations have been scheduled. An invalidation is schedule |
| * if [RecomposeScope.invalidate] has been called on any composition scopes create for the |
| * composition. |
| * |
| * Modifying [MutableState.value] of a value produced by [mutableStateOf] will |
| * automatically call [RecomposeScope.invalidate] for any scope that read [State.value] of |
| * the mutable state instance during composition. |
| * |
| * @see RecomposeScope |
| * @see mutableStateOf |
| */ |
| val hasInvalidations: Boolean |
| |
| /** |
| * True if [dispose] has been called. |
| */ |
| val isDisposed: Boolean |
| |
| /** |
| * Clear the hierarchy that was created from the composition and release resources allocated |
| * for composition. After calling [dispose] the composition will no longer be recomposed and |
| * calling [setContent] will throw an [IllegalStateException]. Calling [dispose] is |
| * idempotent, all calls after the first are a no-op. |
| */ |
| fun dispose() |
| |
| /** |
| * Update the composition with the content described by the [content] composable. After this |
| * has been called the changes to produce the initial composition has been calculated and |
| * applied to the composition. |
| * |
| * Will throw an [IllegalStateException] if the composition has been disposed. |
| * |
| * @param content A composable function that describes the content of the composition. |
| * @exception IllegalStateException thrown in the composition has been [dispose]d. |
| */ |
| fun setContent(content: @Composable () -> Unit) |
| } |
| |
| /** |
| * A controlled composition is a [Composition] that can be directly controlled by the caller. |
| * |
| * This is the interface used by the [Recomposer] to control how and when a composition is |
| * invalidated and subsequently recomposed. |
| * |
| * Normally a composition is controlled by the [Recomposer] but it is often more efficient for |
| * tests to take direct control over a composition by calling [ControlledComposition] instead of |
| * [Composition]. |
| * |
| * @see ControlledComposition |
| */ |
| sealed interface ControlledComposition : Composition { |
| /** |
| * True if the composition is actively compositing such as when actively in a call to |
| * [composeContent] or [recompose]. |
| */ |
| val isComposing: Boolean |
| |
| /** |
| * True after [composeContent] or [recompose] has been called and [applyChanges] is expected |
| * as the next call. An exception will be throw in [composeContent] or [recompose] is called |
| * while there are pending from the previous composition pending to be applied. |
| */ |
| val hasPendingChanges: Boolean |
| |
| /** |
| * Called by the parent composition in response to calling [setContent]. After this method |
| * the changes should be calculated but not yet applied. DO NOT call this method directly if |
| * this is interface is controlled by a [Recomposer], either use [setContent] or |
| * [Recomposer.composeInitial] instead. |
| * |
| * @param content A composable function that describes the tree. |
| */ |
| fun composeContent(content: @Composable () -> Unit) |
| |
| /** |
| * Record the values that were modified after the last call to [recompose] or from the |
| * initial call to [composeContent]. This should be called before [recompose] is called to |
| * record which parts of the composition need to be recomposed. |
| * |
| * @param values the set of values that have changed since the last composition. |
| */ |
| fun recordModificationsOf(values: Set<Any>) |
| |
| /** |
| * Returns true if any of the object instances in [values] is observed by this composition. |
| * This allows detecting if values changed by a previous composition will potentially affect |
| * this composition. |
| */ |
| fun observesAnyOf(values: Set<Any>): Boolean |
| |
| /** |
| * Execute [block] with [isComposing] set temporarily to `true`. This allows treating |
| * invalidations reported during [prepareCompose] as if they happened while composing to avoid |
| * double invalidations when propagating changes from a parent composition while before |
| * composing the child composition. |
| */ |
| fun prepareCompose(block: () -> Unit) |
| |
| /** |
| * Record that [value] has been read. This is used primarily by the [Recomposer] to inform the |
| * composer when the a [MutableState] instance has been read implying it should be observed |
| * for changes. |
| * |
| * @param value the instance from which a property was read |
| */ |
| fun recordReadOf(value: Any) |
| |
| /** |
| * Record that [value] has been modified. This is used primarily by the [Recomposer] to inform |
| * the composer when the a [MutableState] instance been change by a composable function. |
| */ |
| fun recordWriteOf(value: Any) |
| |
| /** |
| * Recompose the composition to calculate any changes necessary to the composition state and |
| * the tree maintained by the applier. No changes have been made yet. Changes calculated will |
| * be applied when [applyChanges] is called. |
| * |
| * @return returns `true` if any changes are pending and [applyChanges] should be called. |
| */ |
| fun recompose(): Boolean |
| |
| /** |
| * Insert the given list of movable content with their paired state in potentially a different |
| * composition. If the second part of the pair is null then the movable content should be |
| * inserted as new. If second part of the pair has a value then the state should be moved into |
| * the referenced location and then recomposed there. |
| */ |
| @InternalComposeApi |
| fun insertMovableContent( |
| references: List<Pair<MovableContentStateReference, MovableContentStateReference?>> |
| ) |
| |
| /** |
| * Dispose the value state that is no longer needed. |
| */ |
| @InternalComposeApi |
| fun disposeUnusedMovableContent(state: MovableContentState) |
| |
| /** |
| * Apply the changes calculated during [setContent] or [recompose]. If an exception is thrown |
| * by [applyChanges] the composition is irreparably damaged and should be [dispose]d. |
| */ |
| fun applyChanges() |
| |
| /** |
| * Apply change that must occur after the main bulk of changes have been applied. Late changes |
| * are the result of inserting movable content and it must be performed after [applyChanges] |
| * because, for content that have moved must be inserted only after it has been removed from |
| * the previous location. All deletes must be executed before inserts. To ensure this, all |
| * deletes are performed in [applyChanges] and all inserts are performed in [applyLateChanges]. |
| */ |
| fun applyLateChanges() |
| |
| /** |
| * Call when all changes, including late changes, have been applied. This signals to the |
| * composition that any transitory composition state can now be discarded. This is advisory |
| * only and a controlled composition will execute correctly when this is not called. |
| */ |
| fun changesApplied() |
| |
| /** |
| * Invalidate all invalidation scopes. This is called, for example, by [Recomposer] when the |
| * Recomposer becomes active after a previous period of inactivity, potentially missing more |
| * granular invalidations. |
| */ |
| fun invalidateAll() |
| |
| /** |
| * Throws an exception if the internal state of the composer has been corrupted and is no |
| * longer consistent. Used in testing the composer itself. |
| */ |
| @InternalComposeApi |
| fun verifyConsistent() |
| |
| /** |
| * Temporarily delegate all invalidations sent to this composition to the [to] composition. |
| * This is used when movable content moves between compositions. The recompose scopes are not |
| * redirected until after the move occurs during [applyChanges] and [applyLateChanges]. This is |
| * used to compose as if the scopes have already been changed. |
| */ |
| fun <R> delegateInvalidations( |
| to: ControlledComposition?, |
| groupIndex: Int, |
| block: () -> R |
| ): R |
| } |
| |
| /** |
| * The [CoroutineContext] that should be used to perform concurrent recompositions of this |
| * [ControlledComposition] when used in an environment supporting concurrent composition. |
| * |
| * See [Recomposer.runRecomposeConcurrentlyAndApplyChanges] as an example of configuring |
| * such an environment. |
| */ |
| // Implementation note: as/if this method graduates it should become a real method of |
| // ControlledComposition with a default implementation. |
| @ExperimentalComposeApi |
| val ControlledComposition.recomposeCoroutineContext: CoroutineContext |
| @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") |
| @ExperimentalComposeApi |
| get() = (this as? CompositionImpl)?.recomposeContext ?: EmptyCoroutineContext |
| |
| /** |
| * This method is the way to initiate a composition. Optionally, a [parent] |
| * [CompositionContext] can be provided to make the composition behave as a sub-composition of |
| * the parent or a [Recomposer] can be provided. |
| * |
| * It is important to call [Composition.dispose] this composer is no longer needed in order to |
| * release resources. |
| * |
| * @sample androidx.compose.runtime.samples.CustomTreeComposition |
| * |
| * @param applier The [Applier] instance to be used in the composition. |
| * @param parent The parent [CompositionContext]. |
| * |
| * @see Applier |
| * @see Composition |
| * @see Recomposer |
| */ |
| fun Composition( |
| applier: Applier<*>, |
| parent: CompositionContext |
| ): Composition = |
| CompositionImpl( |
| parent, |
| applier |
| ) |
| |
| /** |
| * This method is a way to initiate a composition. Optionally, a [parent] |
| * [CompositionContext] can be provided to make the composition behave as a sub-composition of |
| * the parent or a [Recomposer] can be provided. |
| * |
| * A controlled composition allows direct control of the composition instead of it being |
| * controlled by the [Recomposer] passed ot the root composition. |
| * |
| * It is important to call [Composition.dispose] this composer is no longer needed in order to |
| * release resources. |
| * |
| * @sample androidx.compose.runtime.samples.CustomTreeComposition |
| * |
| * @param applier The [Applier] instance to be used in the composition. |
| * @param parent The parent [CompositionContext]. |
| * |
| * @see Applier |
| * @see Composition |
| * @see Recomposer |
| */ |
| @TestOnly |
| fun ControlledComposition( |
| applier: Applier<*>, |
| parent: CompositionContext |
| ): ControlledComposition = |
| CompositionImpl( |
| parent, |
| applier |
| ) |
| |
| /** |
| * Create a [Composition] using [applier] to manage the composition, as a child of [parent]. |
| * |
| * When used in a configuration that supports concurrent recomposition, hint to the environment |
| * that [recomposeCoroutineContext] should be used to perform recomposition. Recompositions will |
| * be launched into the |
| */ |
| @ExperimentalComposeApi |
| fun Composition( |
| applier: Applier<*>, |
| parent: CompositionContext, |
| recomposeCoroutineContext: CoroutineContext |
| ): Composition = CompositionImpl( |
| parent, |
| applier, |
| recomposeContext = recomposeCoroutineContext |
| ) |
| |
| @TestOnly |
| @ExperimentalComposeApi |
| fun ControlledComposition( |
| applier: Applier<*>, |
| parent: CompositionContext, |
| recomposeCoroutineContext: CoroutineContext |
| ): ControlledComposition = CompositionImpl( |
| parent, |
| applier, |
| recomposeContext = recomposeCoroutineContext |
| ) |
| |
| private val PendingApplyNoModifications = Any() |
| |
| /** |
| * The implementation of the [Composition] interface. |
| * |
| * @param parent An optional reference to the parent composition. |
| * @param applier The applier to use to manage the tree built by the composer. |
| * @param recomposeContext The coroutine context to use to recompose this composition. If left |
| * `null` the controlling recomposer's default context is used. |
| */ |
| internal class CompositionImpl( |
| /** |
| * The parent composition from [rememberCompositionContext], for sub-compositions, or the an |
| * instance of [Recomposer] for root compositions. |
| */ |
| private val parent: CompositionContext, |
| |
| /** |
| * The applier to use to update the tree managed by the composition. |
| */ |
| private val applier: Applier<*>, |
| |
| recomposeContext: CoroutineContext? = null |
| ) : ControlledComposition, RecomposeScopeOwner { |
| /** |
| * `null` if a composition isn't pending to apply. |
| * `Set<Any>` or `Array<Set<Any>>` if there are modifications to record |
| * [PendingApplyNoModifications] if a composition is pending to apply, no modifications. |
| * any set contents will be sent to [recordModificationsOf] after applying changes |
| * before releasing [lock] |
| */ |
| private val pendingModifications = AtomicReference<Any?>(null) |
| |
| // Held when making changes to self or composer |
| private val lock = Any() |
| |
| /** |
| * A set of remember observers that were potentially abandoned between [composeContent] or |
| * [recompose] and [applyChanges]. When inserting new content any newly remembered |
| * [RememberObserver]s are added to this set and then removed as [RememberObserver.onRemembered] |
| * is dispatched. If any are left in this when exiting [applyChanges] they have been |
| * abandoned and are sent an [RememberObserver.onAbandoned] notification. |
| */ |
| private val abandonSet = HashSet<RememberObserver>() |
| |
| /** |
| * The slot table is used to store the composition information required for recomposition. |
| */ |
| internal val slotTable = SlotTable() |
| |
| /** |
| * A map of observable objects to the [RecomposeScope]s that observe the object. If the key |
| * object is modified the associated scopes should be invalidated. |
| */ |
| private val observations = IdentityScopeMap<RecomposeScopeImpl>() |
| |
| /** |
| * Used for testing. Returns the objects that are observed |
| */ |
| internal val observedObjects get() = observations.values.filterNotNull() |
| |
| /** |
| * A set of scopes that were invalidated conditionally (that is they were invalidated by a |
| * [derivedStateOf] object) by a call from [recordModificationsOf]. They need to be held in the |
| * [observations] map until invalidations are drained for composition as a later call to |
| * [recordModificationsOf] might later cause them to be unconditionally invalidated. |
| */ |
| private val conditionallyInvalidatedScopes = HashSet<RecomposeScopeImpl>() |
| |
| /** |
| * A map of object read during derived states to the corresponding derived state. |
| */ |
| private val derivedStates = IdentityScopeMap<DerivedState<*>>() |
| |
| /** |
| * Used for testing. Returns dependencies of derived states that are currently observed. |
| */ |
| internal val derivedStateDependencies get() = derivedStates.values.filterNotNull() |
| |
| /** |
| * Used for testing. Returns the conditional scopes being tracked by the composer |
| */ |
| internal val conditionalScopes: List<RecomposeScopeImpl> get() = |
| conditionallyInvalidatedScopes.toList() |
| |
| /** |
| * A list of changes calculated by [Composer] to be applied to the [Applier] and the |
| * [SlotTable] to reflect the result of composition. This is a list of lambdas that need to |
| * be invoked in order to produce the desired effects. |
| */ |
| private val changes = mutableListOf<Change>() |
| |
| /** |
| * A list of changes calculated by [Composer] to be applied after all other compositions have |
| * had [applyChanges] called. These changes move [MovableContent] state from one composition |
| * to another and must be applied after [applyChanges] because [applyChanges] copies and removes |
| * the state out of the previous composition so it can be inserted into the new location. As |
| * inserts might be earlier in the composition than the position it is deleted, this move must |
| * be done in two phases. |
| */ |
| private val lateChanges = mutableListOf<Change>() |
| |
| /** |
| * When an observable object is modified during composition any recompose scopes that are |
| * observing that object are invalidated immediately. Since they have already been processed |
| * there is no need to process them again, so this set maintains a set of the recompose |
| * scopes that were already dismissed by composition and should be ignored in the next call |
| * to [recordModificationsOf]. |
| */ |
| private val observationsProcessed = IdentityScopeMap<RecomposeScopeImpl>() |
| |
| /** |
| * A map of the invalid [RecomposeScope]s. If this map is non-empty the current state of |
| * the composition does not reflect the current state of the objects it observes and should |
| * be recomposed by calling [recompose]. Tbe value is a map of values that invalidated the |
| * scope. The scope is checked with these instances to ensure the value has changed. This is |
| * used to only invalidate the scope if a [derivedStateOf] object changes. |
| */ |
| private var invalidations = IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>() |
| |
| /** |
| * As [RecomposeScope]s are removed the corresponding entries in the observations set must be |
| * removed as well. This process is expensive so should only be done if it is certain the |
| * [observations] set contains [RecomposeScope] that is no longer needed. [pendingInvalidScopes] |
| * is set to true whenever a [RecomposeScope] is removed from the [slotTable]. |
| */ |
| internal var pendingInvalidScopes = false |
| |
| private var invalidationDelegate: CompositionImpl? = null |
| |
| private var invalidationDelegateGroup: Int = 0 |
| |
| /** |
| * The [Composer] to use to create and update the tree managed by this composition. |
| */ |
| private val composer: ComposerImpl = |
| ComposerImpl( |
| applier = applier, |
| parentContext = parent, |
| slotTable = slotTable, |
| abandonSet = abandonSet, |
| changes = changes, |
| lateChanges = lateChanges, |
| composition = this |
| ).also { |
| parent.registerComposer(it) |
| } |
| |
| /** |
| * The [CoroutineContext] override, if there is one, for this composition. |
| */ |
| private val _recomposeContext: CoroutineContext? = recomposeContext |
| |
| /** |
| * the [CoroutineContext] to use to [recompose] this composition. |
| */ |
| val recomposeContext: CoroutineContext |
| get() = _recomposeContext ?: parent.recomposeCoroutineContext |
| |
| /** |
| * Return true if this is a root (non-sub-) composition. |
| */ |
| val isRoot: Boolean = parent is Recomposer |
| |
| /** |
| * True if [dispose] has been called. |
| */ |
| private var disposed = false |
| |
| /** |
| * True if a sub-composition of this composition is current composing. |
| */ |
| private val areChildrenComposing get() = composer.areChildrenComposing |
| |
| /** |
| * The [Composable] function used to define the tree managed by this composition. This is set |
| * by [setContent]. |
| */ |
| var composable: @Composable () -> Unit = {} |
| |
| override val isComposing: Boolean |
| get() = composer.isComposing |
| |
| override val isDisposed: Boolean get() = disposed |
| |
| override val hasPendingChanges: Boolean |
| get() = synchronized(lock) { composer.hasPendingChanges } |
| |
| override fun setContent(content: @Composable () -> Unit) { |
| check(!disposed) { "The composition is disposed" } |
| this.composable = content |
| parent.composeInitial(this, composable) |
| } |
| |
| fun invalidateGroupsWithKey(key: Int) { |
| val scopesToInvalidate = synchronized(lock) { |
| slotTable.invalidateGroupsWithKey(key) |
| } |
| // Calls to invalidate must be performed without the lock as the they may cause the |
| // recomposer to take its lock to respond to the invalidation and that takes the locks |
| // in the opposite order of composition so if composition begins in another thread taking |
| // trying to take the recomposer lock with the composer lock held will deadlock. |
| val forceComposition = scopesToInvalidate == null || scopesToInvalidate.fastAny { |
| it.invalidateForResult(null) == InvalidationResult.IGNORED |
| } |
| if (forceComposition && composer.forceRecomposeScopes()) { |
| parent.invalidate(this) |
| } |
| } |
| |
| @Suppress("UNCHECKED_CAST") |
| private fun drainPendingModificationsForCompositionLocked() { |
| // Recording modifications may race for lock. If there are pending modifications |
| // and we won the lock race, drain them before composing. |
| when (val toRecord = pendingModifications.getAndSet(PendingApplyNoModifications)) { |
| null -> { |
| // Do nothing, just start composing. |
| } |
| PendingApplyNoModifications -> { |
| composeRuntimeError("pending composition has not been applied") |
| } |
| is Set<*> -> { |
| addPendingInvalidationsLocked(toRecord as Set<Any>, forgetConditionalScopes = true) |
| } |
| is Array<*> -> for (changed in toRecord as Array<Set<Any>>) { |
| addPendingInvalidationsLocked(changed, forgetConditionalScopes = true) |
| } |
| else -> composeRuntimeError("corrupt pendingModifications drain: $pendingModifications") |
| } |
| } |
| |
| @Suppress("UNCHECKED_CAST") |
| private fun drainPendingModificationsLocked() { |
| when (val toRecord = pendingModifications.getAndSet(null)) { |
| PendingApplyNoModifications -> { |
| // No work to do |
| } |
| is Set<*> -> { |
| addPendingInvalidationsLocked(toRecord as Set<Any>, forgetConditionalScopes = false) |
| } |
| is Array<*> -> for (changed in toRecord as Array<Set<Any>>) { |
| addPendingInvalidationsLocked(changed, forgetConditionalScopes = false) |
| } |
| null -> composeRuntimeError( |
| "calling recordModificationsOf and applyChanges concurrently is not supported" |
| ) |
| else -> composeRuntimeError( |
| "corrupt pendingModifications drain: $pendingModifications" |
| ) |
| } |
| } |
| |
| override fun composeContent(content: @Composable () -> Unit) { |
| // TODO: This should raise a signal to any currently running recompose calls |
| // to halt and return |
| guardChanges { |
| synchronized(lock) { |
| drainPendingModificationsForCompositionLocked() |
| guardInvalidationsLocked { invalidations -> |
| composer.composeContent(invalidations, content) |
| } |
| } |
| } |
| } |
| |
| override fun dispose() { |
| synchronized(lock) { |
| if (!disposed) { |
| disposed = true |
| composable = {} |
| |
| // Changes are deferred if the composition contains movable content that needs |
| // to be released. NOTE: Applying these changes leaves the slot table in |
| // potentially invalid state. The routine use to produce this change list reuses |
| // code that extracts movable content from groups that are being deleted. This code |
| // does not bother to correctly maintain the node counts of a group nested groups |
| // that are going to be removed anyway so the node counts of the groups affected |
| // are might be incorrect after the changes have been applied. |
| val deferredChanges = composer.deferredChanges |
| if (deferredChanges != null) { |
| applyChangesInLocked(deferredChanges) |
| } |
| |
| // Dispatch all the `onForgotten` events for object that are no longer part of a |
| // composition because this composition is being discarded. It is important that |
| // this is done after applying deferred changes above to avoid sending ` |
| // onForgotten` notification to objects that are still part of movable content that |
| // will be moved to a new location. |
| val nonEmptySlotTable = slotTable.groupsSize > 0 |
| if (nonEmptySlotTable || abandonSet.isNotEmpty()) { |
| val manager = RememberEventDispatcher(abandonSet) |
| if (nonEmptySlotTable) { |
| slotTable.write { writer -> |
| writer.removeCurrentGroup(manager) |
| } |
| applier.clear() |
| manager.dispatchRememberObservers() |
| manager.dispatchNodeCallbacks() |
| } |
| manager.dispatchAbandons() |
| } |
| composer.dispose() |
| } |
| } |
| parent.unregisterComposition(this) |
| } |
| |
| override val hasInvalidations get() = synchronized(lock) { invalidations.size > 0 } |
| |
| /** |
| * To bootstrap multithreading handling, recording modifications is now deferred between |
| * recomposition with changes to apply and the application of those changes. |
| * [pendingModifications] will contain a queue of changes to apply once all current changes |
| * have been successfully processed. Draining this queue is the responsibility of [recompose] |
| * if it would return `false` (changes do not need to be applied) or [applyChanges]. |
| */ |
| @Suppress("UNCHECKED_CAST") |
| override fun recordModificationsOf(values: Set<Any>) { |
| while (true) { |
| val old = pendingModifications.get() |
| val new: Any = when (old) { |
| null, PendingApplyNoModifications -> values |
| is Set<*> -> arrayOf(old, values) |
| is Array<*> -> (old as Array<Set<Any>>) + values |
| else -> error("corrupt pendingModifications: $pendingModifications") |
| } |
| if (pendingModifications.compareAndSet(old, new)) { |
| if (old == null) { |
| synchronized(lock) { |
| drainPendingModificationsLocked() |
| } |
| } |
| break |
| } |
| } |
| } |
| |
| override fun observesAnyOf(values: Set<Any>): Boolean { |
| for (value in values) { |
| if (value in observations || value in derivedStates) return true |
| } |
| return false |
| } |
| |
| override fun prepareCompose(block: () -> Unit) = composer.prepareCompose(block) |
| |
| private fun addPendingInvalidationsLocked(values: Set<Any>, forgetConditionalScopes: Boolean) { |
| var invalidated: HashSet<RecomposeScopeImpl>? = null |
| |
| fun invalidate(value: Any) { |
| observations.forEachScopeOf(value) { scope -> |
| if ( |
| !observationsProcessed.remove(value, scope) && |
| scope.invalidateForResult(value) != InvalidationResult.IGNORED |
| ) { |
| if (scope.isConditional && !forgetConditionalScopes) { |
| conditionallyInvalidatedScopes.add(scope) |
| } else { |
| val set = invalidated |
| ?: HashSet<RecomposeScopeImpl>().also { |
| invalidated = it |
| } |
| set.add(scope) |
| } |
| } |
| } |
| } |
| |
| values.fastForEach { value -> |
| if (value is RecomposeScopeImpl) { |
| value.invalidateForResult(null) |
| } else { |
| invalidate(value) |
| derivedStates.forEachScopeOf(value) { |
| invalidate(it) |
| } |
| } |
| } |
| |
| if (forgetConditionalScopes && conditionallyInvalidatedScopes.isNotEmpty()) { |
| observations.removeValueIf { scope -> |
| scope in conditionallyInvalidatedScopes || invalidated?.let { scope in it } == true |
| } |
| conditionallyInvalidatedScopes.clear() |
| cleanUpDerivedStateObservations() |
| } else { |
| invalidated?.let { |
| observations.removeValueIf { scope -> scope in it } |
| cleanUpDerivedStateObservations() |
| } |
| } |
| } |
| |
| private fun cleanUpDerivedStateObservations() { |
| derivedStates.removeValueIf { derivedState -> derivedState !in observations } |
| if (conditionallyInvalidatedScopes.isNotEmpty()) { |
| conditionallyInvalidatedScopes.removeValueIf { scope -> !scope.isConditional } |
| } |
| } |
| |
| override fun recordReadOf(value: Any) { |
| // Not acquiring lock since this happens during composition with it already held |
| if (!areChildrenComposing) { |
| composer.currentRecomposeScope?.let { |
| it.used = true |
| val alreadyRead = it.recordRead(value) |
| if (!alreadyRead) { |
| observations.add(value, it) |
| |
| // Record derived state dependency mapping |
| if (value is DerivedState<*>) { |
| derivedStates.removeScope(value) |
| for (dependency in value.dependencies) { |
| // skip over empty objects from dependency array |
| if (dependency == null) break |
| derivedStates.add(dependency, value) |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private fun invalidateScopeOfLocked(value: Any) { |
| // Invalidate any recompose scopes that read this value. |
| observations.forEachScopeOf(value) { scope -> |
| if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) { |
| // If we process this during recordWriteOf, ignore it when recording modifications |
| observationsProcessed.add(value, scope) |
| } |
| } |
| } |
| |
| override fun recordWriteOf(value: Any) = synchronized(lock) { |
| invalidateScopeOfLocked(value) |
| |
| // If writing to dependency of a derived value and the value is changed, invalidate the |
| // scopes that read the derived value. |
| derivedStates.forEachScopeOf(value) { |
| invalidateScopeOfLocked(it) |
| } |
| } |
| |
| override fun recompose(): Boolean = synchronized(lock) { |
| drainPendingModificationsForCompositionLocked() |
| guardChanges { |
| guardInvalidationsLocked { invalidations -> |
| composer.recompose(invalidations).also { shouldDrain -> |
| // Apply would normally do this for us; do it now if apply shouldn't happen. |
| if (!shouldDrain) drainPendingModificationsLocked() |
| } |
| } |
| } |
| } |
| |
| override fun insertMovableContent( |
| references: List<Pair<MovableContentStateReference, MovableContentStateReference?>> |
| ) { |
| runtimeCheck(references.fastAll { it.first.composition == this }) |
| guardChanges { |
| composer.insertMovableContentReferences(references) |
| } |
| } |
| |
| override fun disposeUnusedMovableContent(state: MovableContentState) { |
| val manager = RememberEventDispatcher(abandonSet) |
| val slotTable = state.slotTable |
| slotTable.write { writer -> |
| writer.removeCurrentGroup(manager) |
| } |
| manager.dispatchRememberObservers() |
| manager.dispatchNodeCallbacks() |
| } |
| |
| private fun applyChangesInLocked(changes: MutableList<Change>) { |
| val manager = RememberEventDispatcher(abandonSet) |
| try { |
| if (changes.isEmpty()) return |
| trace("Compose:applyChanges") { |
| applier.onBeginChanges() |
| |
| // Apply all changes |
| slotTable.write { slots -> |
| val applier = applier |
| changes.fastForEach { change -> |
| change(applier, slots, manager) |
| } |
| changes.clear() |
| } |
| applier.onEndChanges() |
| } |
| |
| // Side effects run after lifecycle observers so that any remembered objects |
| // that implement RememberObserver receive onRemembered before a side effect |
| // that captured it and operates on it can run. |
| manager.dispatchRememberObservers() |
| manager.dispatchNodeCallbacks() |
| manager.dispatchSideEffects() |
| |
| if (pendingInvalidScopes) { |
| trace("Compose:unobserve") { |
| pendingInvalidScopes = false |
| observations.removeValueIf { scope -> !scope.valid } |
| cleanUpDerivedStateObservations() |
| } |
| } |
| } finally { |
| // Only dispatch abandons if we do not have any late changes. The instances in the |
| // abandon set can be remembered in the late changes. |
| if (this.lateChanges.isEmpty()) |
| manager.dispatchAbandons() |
| } |
| } |
| |
| override fun applyChanges() { |
| synchronized(lock) { |
| guardChanges { |
| applyChangesInLocked(changes) |
| drainPendingModificationsLocked() |
| } |
| } |
| } |
| |
| override fun applyLateChanges() { |
| synchronized(lock) { |
| guardChanges { |
| if (lateChanges.isNotEmpty()) { |
| applyChangesInLocked(lateChanges) |
| } |
| } |
| } |
| } |
| |
| override fun changesApplied() { |
| synchronized(lock) { |
| guardChanges { |
| composer.changesApplied() |
| |
| // By this time all abandon objects should be notified that they have been abandoned. |
| if (this.abandonSet.isNotEmpty()) { |
| RememberEventDispatcher(abandonSet).dispatchAbandons() |
| } |
| } |
| } |
| } |
| |
| private inline fun <T> guardInvalidationsLocked( |
| block: (changes: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>) -> T |
| ): T { |
| val invalidations = takeInvalidations() |
| return try { |
| block(invalidations) |
| } catch (e: Exception) { |
| this.invalidations = invalidations |
| throw e |
| } |
| } |
| |
| private inline fun <T> guardChanges(block: () -> T): T = |
| try { |
| trackAbandonedValues(block) |
| } catch (e: Exception) { |
| abandonChanges() |
| throw e |
| } |
| |
| private fun abandonChanges() { |
| pendingModifications.set(null) |
| changes.clear() |
| lateChanges.clear() |
| abandonSet.clear() |
| } |
| |
| override fun invalidateAll() { |
| synchronized(lock) { |
| slotTable.slots.forEach { (it as? RecomposeScopeImpl)?.invalidate() } |
| } |
| } |
| |
| override fun verifyConsistent() { |
| synchronized(lock) { |
| if (!isComposing) { |
| composer.verifyConsistent() |
| slotTable.verifyWellFormed() |
| validateRecomposeScopeAnchors(slotTable) |
| } |
| } |
| } |
| |
| override fun <R> delegateInvalidations( |
| to: ControlledComposition?, |
| groupIndex: Int, |
| block: () -> R |
| ): R { |
| return if (to != null && to != this && groupIndex >= 0) { |
| invalidationDelegate = to as CompositionImpl |
| invalidationDelegateGroup = groupIndex |
| try { |
| block() |
| } finally { |
| invalidationDelegate = null |
| invalidationDelegateGroup = 0 |
| } |
| } else block() |
| } |
| |
| override fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult { |
| if (scope.defaultsInScope) { |
| scope.defaultsInvalid = true |
| } |
| val anchor = scope.anchor |
| if (anchor == null || !anchor.valid) |
| return InvalidationResult.IGNORED // The scope was removed from the composition |
| if (!slotTable.ownsAnchor(anchor)) { |
| // The scope might be owned by the delegate |
| val delegate = synchronized(lock) { invalidationDelegate } |
| if (delegate?.tryImminentInvalidation(scope, instance) == true) |
| return InvalidationResult.IMMINENT // The scope was owned by the delegate |
| |
| return InvalidationResult.IGNORED // The scope has not yet entered the composition |
| } |
| if (!scope.canRecompose) |
| return InvalidationResult.IGNORED // The scope isn't able to be recomposed/invalidated |
| return invalidateChecked(scope, anchor, instance) |
| } |
| |
| override fun recomposeScopeReleased(scope: RecomposeScopeImpl) { |
| pendingInvalidScopes = true |
| } |
| |
| private fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean = |
| isComposing && composer.tryImminentInvalidation(scope, instance) |
| |
| private fun invalidateChecked( |
| scope: RecomposeScopeImpl, |
| anchor: Anchor, |
| instance: Any? |
| ): InvalidationResult { |
| val delegate = synchronized(lock) { |
| val delegate = invalidationDelegate?.let { changeDelegate -> |
| // Invalidations are delegated when recomposing changes to movable content that |
| // is destined to be moved. The movable content is composed in the destination |
| // composer but all the recompose scopes point the current composer and will arrive |
| // here. this redirects the invalidations that will be moved to the destination |
| // composer instead of recording an invalid invalidation in the from composer. |
| if (slotTable.groupContainsAnchor(invalidationDelegateGroup, anchor)) { |
| changeDelegate |
| } else null |
| } |
| if (delegate == null) { |
| if (tryImminentInvalidation(scope, instance)) { |
| // The invalidation was redirected to the composer. |
| return InvalidationResult.IMMINENT |
| } |
| |
| // invalidations[scope] containing an explicit null means it was invalidated |
| // unconditionally. |
| if (instance == null) { |
| invalidations[scope] = null |
| } else { |
| invalidations.addValue(scope, instance) |
| } |
| } |
| delegate |
| } |
| |
| // We call through the delegate here to ensure we don't nest synchronization scopes. |
| if (delegate != null) { |
| return delegate.invalidateChecked(scope, anchor, instance) |
| } |
| parent.invalidate(this) |
| return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED |
| } |
| |
| internal fun removeObservation(instance: Any, scope: RecomposeScopeImpl) { |
| observations.remove(instance, scope) |
| } |
| |
| internal fun removeDerivedStateObservation(state: DerivedState<*>) { |
| // remove derived state if it is not observed in other scopes |
| if (state !in observations) { |
| derivedStates.removeScope(state) |
| } |
| } |
| |
| /** |
| * This takes ownership of the current invalidations and sets up a new array map to hold the |
| * new invalidations. |
| */ |
| private fun takeInvalidations(): IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?> { |
| val invalidations = invalidations |
| this.invalidations = IdentityArrayMap() |
| return invalidations |
| } |
| |
| /** |
| * Helper for [verifyConsistent] to ensure the anchor match there respective invalidation |
| * scopes. |
| */ |
| private fun validateRecomposeScopeAnchors(slotTable: SlotTable) { |
| val scopes = slotTable.slots.mapNotNull { it as? RecomposeScopeImpl } |
| scopes.fastForEach { scope -> |
| scope.anchor?.let { anchor -> |
| check(scope in slotTable.slotsOf(anchor.toIndexFor(slotTable))) { |
| val dataIndex = slotTable.slots.indexOf(scope) |
| "Misaligned anchor $anchor in scope $scope encountered, scope found at " + |
| "$dataIndex" |
| } |
| } |
| } |
| } |
| |
| private inline fun <T> trackAbandonedValues(block: () -> T): T { |
| var success = false |
| return try { |
| block().also { |
| success = true |
| } |
| } finally { |
| if (!success && abandonSet.isNotEmpty()) { |
| RememberEventDispatcher(abandonSet).dispatchAbandons() |
| } |
| } |
| } |
| |
| /** |
| * Helper for collecting remember observers for later strictly ordered dispatch. |
| */ |
| private class RememberEventDispatcher( |
| private val abandoning: MutableSet<RememberObserver> |
| ) : RememberManager { |
| private val remembering = mutableListOf<RememberObserver>() |
| private val forgetting = mutableListOf<RememberObserver>() |
| private val sideEffects = mutableListOf<() -> Unit>() |
| private var deactivating: MutableList<ComposeNodeLifecycleCallback>? = null |
| private var releasing: MutableList<ComposeNodeLifecycleCallback>? = null |
| |
| override fun remembering(instance: RememberObserver) { |
| forgetting.lastIndexOf(instance).let { index -> |
| if (index >= 0) { |
| forgetting.removeAt(index) |
| abandoning.remove(instance) |
| } else { |
| remembering.add(instance) |
| } |
| } |
| } |
| |
| override fun forgetting(instance: RememberObserver) { |
| remembering.lastIndexOf(instance).let { index -> |
| if (index >= 0) { |
| remembering.removeAt(index) |
| abandoning.remove(instance) |
| } else { |
| forgetting.add(instance) |
| } |
| } |
| } |
| |
| override fun sideEffect(effect: () -> Unit) { |
| sideEffects += effect |
| } |
| |
| override fun deactivating(instance: ComposeNodeLifecycleCallback) { |
| (deactivating ?: mutableListOf<ComposeNodeLifecycleCallback>().also { |
| deactivating = it |
| }) += instance |
| } |
| |
| override fun releasing(instance: ComposeNodeLifecycleCallback) { |
| (releasing ?: mutableListOf<ComposeNodeLifecycleCallback>().also { |
| releasing = it |
| }) += instance |
| } |
| |
| fun dispatchRememberObservers() { |
| // Send forgets |
| if (forgetting.isNotEmpty()) { |
| trace("Compose:onForgotten") { |
| for (i in forgetting.size - 1 downTo 0) { |
| val instance = forgetting[i] |
| if (instance !in abandoning) { |
| instance.onForgotten() |
| } |
| } |
| } |
| } |
| |
| // Send remembers |
| if (remembering.isNotEmpty()) { |
| trace("Compose:onRemembered") { |
| remembering.fastForEach { instance -> |
| abandoning.remove(instance) |
| instance.onRemembered() |
| } |
| } |
| } |
| } |
| |
| fun dispatchSideEffects() { |
| if (sideEffects.isNotEmpty()) { |
| trace("Compose:sideeffects") { |
| sideEffects.fastForEach { sideEffect -> |
| sideEffect() |
| } |
| sideEffects.clear() |
| } |
| } |
| } |
| |
| fun dispatchAbandons() { |
| if (abandoning.isNotEmpty()) { |
| trace("Compose:abandons") { |
| val iterator = abandoning.iterator() |
| while (iterator.hasNext()) { |
| val instance = iterator.next() |
| iterator.remove() |
| instance.onAbandoned() |
| } |
| } |
| } |
| } |
| |
| fun dispatchNodeCallbacks() { |
| val deactivating = deactivating |
| if (!deactivating.isNullOrEmpty()) { |
| trace("Compose:deactivations") { |
| for (i in deactivating.size - 1 downTo 0) { |
| val instance = deactivating[i] |
| instance.onDeactivate() |
| } |
| } |
| deactivating.clear() |
| } |
| |
| val releasing = releasing |
| if (!releasing.isNullOrEmpty()) { |
| // note that in contrast with objects from `forgetting` we will invoke the callback |
| // even for objects being abandoned. |
| trace("Compose:releases") { |
| for (i in releasing.size - 1 downTo 0) { |
| val instance = releasing[i] |
| instance.onRelease() |
| } |
| } |
| releasing.clear() |
| } |
| } |
| } |
| } |
| |
| /** |
| * Apply Code Changes will invoke the two functions before and after a code swap. |
| * |
| * This forces the whole view hierarchy to be redrawn to invoke any code change that was |
| * introduce in the code swap. |
| * |
| * All these are private as within JVMTI / JNI accessibility is mostly a formality. |
| */ |
| private class HotReloader { |
| companion object { |
| // Called before Dex Code Swap |
| @Suppress("UNUSED_PARAMETER") |
| private fun saveStateAndDispose(context: Any): Any { |
| return Recomposer.saveStateAndDisposeForHotReload() |
| } |
| |
| // Called after Dex Code Swap |
| @Suppress("UNUSED_PARAMETER") |
| private fun loadStateAndCompose(token: Any) { |
| Recomposer.loadStateAndComposeForHotReload(token) |
| } |
| |
| @TestOnly |
| internal fun simulateHotReload(context: Any) { |
| loadStateAndCompose(saveStateAndDispose(context)) |
| } |
| |
| @TestOnly |
| internal fun invalidateGroupsWithKey(key: Int) { |
| return Recomposer.invalidateGroupsWithKey(key) |
| } |
| |
| @TestOnly |
| internal fun getCurrentErrors(): List<RecomposerErrorInfo> { |
| return Recomposer.getCurrentErrors() |
| } |
| |
| @TestOnly |
| internal fun clearErrors() { |
| return Recomposer.clearErrors() |
| } |
| } |
| } |
| |
| /** |
| * @suppress |
| */ |
| @TestOnly |
| fun simulateHotReload(context: Any) = HotReloader.simulateHotReload(context) |
| |
| /** |
| * @suppress |
| */ |
| @TestOnly |
| fun invalidateGroupsWithKey(key: Int) = HotReloader.invalidateGroupsWithKey(key) |
| |
| /** |
| * @suppress |
| */ |
| // suppressing for test-only api |
| @Suppress("ListIterator") |
| @TestOnly |
| fun currentCompositionErrors(): List<Pair<Exception, Boolean>> = |
| HotReloader.getCurrentErrors() |
| .map { it.cause to it.recoverable } |
| |
| /** |
| * @suppress |
| */ |
| @TestOnly |
| fun clearCompositionErrors() = HotReloader.clearErrors() |
| |
| private fun <K : Any, V : Any> IdentityArrayMap<K, IdentityArraySet<V>?>.addValue( |
| key: K, |
| value: V |
| ) { |
| if (key in this) { |
| this[key]?.add(value) |
| } else { |
| this[key] = IdentityArraySet<V>().also { it.add(value) } |
| } |
| } |
| |
| /** |
| * This is provided natively in API 26 and this should be removed if 26 is made the lowest API |
| * level supported |
| */ |
| private inline fun <E> HashSet<E>.removeValueIf(predicate: (E) -> Boolean) { |
| val iter = iterator() |
| while (iter.hasNext()) { |
| if (predicate(iter.next())) { |
| iter.remove() |
| } |
| } |
| } |