understanding compose

types

ComponentActivity::setContent

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
)
  1. 1.
    obtains a ComposeView to upsert either from this.window.decorView or by constructing one
  2. 2.
    updates the ComposeView
    1. 1.
      on the ComposeView, calls .setParentCompositionContext(parent); sets this.parentContext = parent
      1. 1.
        the setter might call this.ensureCompositionCreated()
    2. 2.
      on the ComposeView, calls .setContent(content)
      1. 1.
        sets this.content.value = content
      2. 2.
        if this.isAttachedToWindow, calls this.createComposition() (→ this.ensureCompositionCreated())
  3. 3.
    inserts the ComposeView if it’s new
    1. 1.
      on the ComponentActivity, calls .setOwners(), which passes this: ComponentActivity
      • if this.window.decorView.findViewTree{Lifecycle, ViewModelStore, SavedStateRegistry}Owner() == null
      • to this.window.decorView.setViewTree{Lifecycle, ViewModelStore, SavedStateRegistry}Owner respectively
    2. 2.
      on the ComponentActivity, calls .setContentView(/* ComposeView */, DefaultActivityContentLayoutParams)
      private val DefaultActivityContentLayoutParams = ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT,
          ViewGroup.LayoutParams.WRAP_CONTENT,
      )
      • populates the activity window decor view with the new ComposeView
      • the layout parameters request the window make the view as tall and wide as its content

AbstractComposeView::ensureCompositionCreated

  1. 1.
    calls this.resolveParentCompositionContext()
    1. 1.
      returns this.parentContext if !null
    2. 2.
      returns this.findViewTreeCompositionContext()?.cacheIfAlive() if !null
      1. 1.
        goes up the View hierarchy tree until the first View::compositionContext: CompositionContext? that’s !null found
      2. 2.
        if !null, used if .isAlive; cached in this.cachedViewTreeCompositionContext = WeakReference(it)
    3. 3.
      returns this.cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive } if !null
    4. 4.
      returns this.windowRecomposer.cacheIfAlive() otherwise; View::windowRecomposer: Recomposer is a computed property
      1. 1.
        expects (invariant) this.isAttachedToWindow
      2. 2.
        goes up the View hierarchy tree for the first one parented by an R.id.content view
      3. 3.
        if the View::compositionContext: CompositionContext? for the found view
        • is Recomposer → returns the Recomposer
        • nullWindowRecomposerPolicy.createAndInstallWindowRecomposer passed the found view
          • an internal method on object WindowRecomposerPolicy
            • .factory: AtomicReference<WindowRecomposerFactory>; references WindowRecomposerFactory.LifecycleAware
          • fun interface WindowRecomposerFactory implemented by types (+ functions) that instantiate Recomposer for android windows
            • fun createRecomposer(windowRootView: View): Recomposer; the single abstract method → the interface also implemented by functions with the same signature
            • .LifecycleAware: WindowRecomposerFactory; a lambda that calls on the argument & returns .createLifecycleAwareWindowRecomposer()
              fun View.createLifecycleAwareWindowRecomposer(
                  coroutineContext: CoroutineContext = EmptyCoroutineContext,
                  lifecycle: Lifecycle? = null,
              ): Recomposer
              1. 1.
                declares val baseContext: CoroutineContext
                • every coroutine has a context which stores metadata; consists of elements that are themselves contexts
                • EmptyCoroutineContext: represents no metadata; consists of zero elements
                • ContinuationInterceptor: wraps a Continuation<T> in another that intercepts and handles resumption
                • MonotonicFrameClock: a time source for display frames + performs operations on the next frame
                1. 1.
                  initialised with AndroidUiDispatcher.CurrentThread + coroutineContext if coroutineContext[ContinuationInterceptor | MonotonicFrameClock] == null
                2. 2.
                  initialised with coroutineContext otherwise
              2. 2.
                declares val pausableClock: PausableMonotonicFrameClock; wraps baseContext[MonotonicFrameClock] in a paused PausableMonotonicFrameClock
              3. 3.
                var systemDurationScaleSettingConsumer: MotionDurationScaleImpl? = null
              4. 4.
                val motionDurationScale: MotionDurationScale initialised with
                1. 1.
                  baseContext[MotionDurationScale] if !null
                2. 2.
                  MotionDurationScaleImpl() otherwise; also assigned to systemDurationScaleSettingConsumer
              5. 5.
                declares val contextWithClockAndMotionScale: CoroutineContext; initialised with the sum of
                • baseContext; the coroutineContext argument (+? AndroidUiDispatcher.CurrentThread)
                • pausableClock ?: EmptyCoroutineContext; no clock metadata if there was no baseContext[MonotonicFrameClock]
                • motionDurationScale
              6. 6.
                declares val recomposer: Recomposer = Recomposer(contextWithClockAndMotionScale)
                • Recomposer::pauseCompositionFrameClock called on it on construction
              7. 7.
                declares val runRecomposeScope: CoroutineScope = CoroutineScope(contextWithClockAndMotionScale)
                • coroutines associated with scopes → organised into trees
              8. 8.
                adds a LifecycleEventObserver to the lifecycle of the view tree
                • lifecycle ?: this.findViewTreeLifecycleOwner()?.lifecycle used if !null; panics otherwise
                • Lifecycle.Event.ON_CREATE → launches a coroutine in a child of runRecomposeScope
                  1. 1.
                    calls recomposer.runRecomposeAndApplyChanges(); suspends in loop
                    public suspend fun runRecomposeAndApplyChanges(): Unit
                    1. 1.
                      calls Recomposer::recompositionRunner on the receiver
                      private suspend fun recompositionRunner(
                          block: suspend CoroutineScope.(MonotonicFrameClock) -> Unit
                      )
                      1. 1.
                        declares val parentFrameClock: MonotonicFrameClock = coroutineContext.monotonicFrameClock
                        • kotlin.coroutines.coroutineContext fetches the CoroutineContext of the current coroutine
                      2. 2.
                        starts and drives to completion in the current coroutine a child coroutine with this.broadcastFrameClock: BroadcastFrameClock as its context that does
                        1. 1.
                          declares val callingJob = coroutineContext.job; identifies the child coroutine
                        2. 2.
                          calls this.registerRunnerJob(callingJob)
                          private fun registerRunnerJob(callingJob: Job): Unit
                          1. 1.
                            throws this.closeCause: Throwable? if !null
                          2. 2.
                            throws if the current state is either State.{ ShutDown, ShuttingDown }
                          3. 3.
                            throws if this.runnerJob: Job? != null; there’s already an ongoing job
                          4. 4.
                            assigns the argument to this.runnerJob: Job?
                          5. 5.
                            calls this.deriveStateLocked(); sets the state to State.Idle if successful
                            private fun deriveStateLocked(): CancellableContinuation<Unit>?
                            1. 1.
                              returns null if the current state is State.{ ShutDown, ShuttingDown } after clearing/freeing resources
                            2. 2.
                              declares val newState: Recomposer.State initialised with
                              1. 1.
                                State.Inactive if this.errorState != null
                              2. 2.
                                State.{ InactivePendingWork | Inactive } if this.runnerJob == null
                                1. 1.
                                  clears this.snapshotInvalidations: MutableScatterSet<Any>
                                2. 2.
                                  clears this.compositionInvalidations: MutableVector<ControlledComposition>
                                3. 3.
                                  State.InactivePendingWork used if this.{ hasBroadcastFrameClockAwaitersLocked || hasNextFrameEndAwaitersLocked }
                                4. 4.
                                  State.Inactive used otherwise
                              3. 3.
                                State.PendingWork if
                                • this.compositionInvalidations.isNotEmpty()
                                • or this.snapshotInvalidations.isNotEmpty()
                                • or this.compositionsAwaitingApply.isNotEmpty()
                                • or this.movableContentAwaitingInsert.isNotEmpty()
                                • or this.concurrentCompositionsOutstanding > 0
                                • or this.hasBroadcastFrameClockAwaitersLocked
                                • or this.hasNextFrameEndAwaitersLocked
                                • or this.movableContentRemoved.isNotEmpty()
                              4. 4.
                                State.Idle otherwise; taken on this call path
                            3. 3.
                              assigns the newState to the current state of this
                            4. 4.
                              returns this.workContinuation: CancellableContinuation<Unit>? + leaves behind null if newState == State.PendingWork
                            5. 5.
                              returns null otherwise
                        3. 3.
                          installs a global snapshot apply oberser that
                          1. 1.
                            does nothing unless State.{ Idle | PendingWork }
                          2. 2.
                            adds every changed state object to this.snapshotInvalidations: MutableScatterSet<Any>
                            • unless skippable; it is StateObjectImpl && !it.isReadIn(ReaderKind.Composition)
                            • newState = State.PendingWork in the next call to this::deriveStateLocked
                          3. 3.
                            calls this.deriveStateLocked()?.resume(Unit)
                            • this.workContinuation taken and resumed; null left behind
                  2. 2.
                    not reached until the loop exited or the coroutine cancelled; removes the observer from the view tree + cleans up
                • Lifecycle.Event.ON_START → calls
                  1. 1.
                    pausableClock?.resume()
                  2. 2.
                    recomposer.resumeCompositionFrameClock()
                • Lifecycle.Event.ON_STOP → calls recomposer.pauseCompositionFrameClock()
                • Lifecycle.Event.ON_DESTROY → calls recomposer.cancel()
        • else → throws if reached
        internal fun createAndInstallWindowRecomposer(rootView: View): Recomposer
        1. 1.
          declares val newRecomposer: Recomposer; calls on the object WindowRecomposerPolicy and assigns .factory.get().createRecomposer(rootView)
          • WindowRecomposerPolicy::factory: AtomicReference<WindowRecomposerFactory> references WindowRecomposerFactory::LifecycleAware
          • WindowRecomposerFactory::LifecycleAware: WindowRecomposerFactory assigned a WindowRecomposerFactory implementor
  2. 2.
    calls this.setContent
    internal fun AbstractComposeView.setContent(
        parent: CompositionContext,
        content: @Composable () -> Unit,
    ): Composition
    • extension; unrelated to ComposeView::setContent by inheritance
    • parent passed the return value of this.resolveParentCompositionContext()
    • content calls this.Content() when called
    1. 1.
      calls GlobalSnapshotManager.ensureStarted()
    2. 2.
      obtains a AndroidComposeView either from this.mChildren or by constructing one
      1. 1.
        this.getChildAt(0) as? AndroidComposeView used if this.childCount > 0
      2. 2.
        otherwise uses AndroidComposeView(this.context, parent.effectCoroutineContext)
        • View::context: Context; an environment role valid for a lifetime & shared by every reference site
        • the AndroidComposeView (added) made the only child of the this: AbstractComposeView parent (all other children removed)
    3. 3.
      return doSetContent(/* ... */)
      private fun doSetContent(
          owner: AndroidComposeView,
          parent: CompositionContext,
          content: @Composable () -> Unit,
      ): Composition
      • owner passed the obtained AndroidComposeView child
      • parent relayed the parent: CompositionContext from AbstractComposeView::setContent
      • content relayed the content: @Composable () -> Unit from AbstractComposeView::setContent
      1. 1.
        obtains a WrappedComposition: Composition either from the owner or by constructing one
        1. 1.
          owner.view.getTag(R.id.wrapped_composition_tag) as? WrappedComposition used if !null
        2. 2.
          otherwise WrappedComposition(owner, Composition(UiApplier(owner.root), parent)) added to the owner and used
          public fun Composition(applier: Applier<*>, parent: CompositionContext)
              : Composition
              = CompositionImpl(parent, applier)
          • parent is the Recomposer: CompositionContext returned and relayed from AbstractComposeView::resolveParentCompositionContext
          • the CompositionImpl constructed with a ComposerImpl registered with the parent; nop
      2. 2.
        calls .setContent(content) on the obtained WrappedComposition and then returns it

(WrappedComposition as Composition)::setContent

override fun setContent(content: @Composable () -> Unit)
  1. 1.
    obtains the ViewTreeOwners of this.owner: AndroidComposeView if it’s attached to a view, or on attach later
    • on the initial (<activity> as ComponentActivity)::onCreateComponentActivity::setContentComposeView::setContent call path
      • called before it’s attached to a window; AbstractComposeView::ensureCompositionCreated not reached
      • attached to a window after (<activity> as ComponentActivity)::onCreate has returned
    • on the initial attach, AbstractComposeView::onAttachedToWindow called; its children attached after the callback has returned (ie not yet)
      • AbstractComposeView::onAttachedToWindowAbstractComposeView::ensureCompositionCreatedAbstractComposeView::setContentdoSetContentWrappedComposition::setContent called
      • WrappedComposition::owner: AndroidComposeView not attached yet, but owner.parent as AbstractComposeView is
    • after AbstractComposeView::onAttachedToWindow has returned, this.owner: AndroidComposeView attached
      • AndroidComposeView::onAttachedToWindow called; instantiates ViewTreeOwners → obtained for the remaining steps
      • the ComponentActivity registered for (this: AndroidComposeView).findViewTree{Lifecycle, SavedStateRegistry, ViewModelStore}Owner()
  2. 2.
    returns if this.disposed, otherwise sets this.lastContent = content and continues
  3. 3.
    if this.addedToLifecycle == null; ie if it’s the first time the flow has reached the conditional
    1. 1.
      assigns the .lifecycleOwner.lifecycle: Lifecycle of the obtained ViewTreeOwners to this.addedToLifecycle
      • that of the ComponentActivity on this code path; already created, started, and resumed
    2. 2.
      calls .addObserver(this) on it; observed by this: LifecycleEventObserver
      • addObserver brings this to the current state of this.addedToLifecycle (created → started → resumed)
      • Lifecycle.Event.ON_DESTROYthis.dispose()
      • Lifecycle.Event.ON_CREATE (called on add here) → calls this.setContent(this.lastContent)
  4. 4.
    otherwise (if this.addedToLifecycle != null), provides the following steps for updating (calculating and applying changes to) its original: Composition if the observed lifetime .currentState.isAtLeast(Lifecycle.State.CREATED)
    • calls CompositionImpl::setContent on this call path
    1. 1.
      exposes the composition to tooling; no business logic
      1. 1.
        retrieves a MutableSet<CompositionData>? if present from either the AndroidComposeView or its parent
      2. 2.
        registers the composition with the set → enables editor/debugging/etc support
    2. 2.
      launches effects tied to the lifecycle of the composition for jobs that suspend in a loop
      • emits accessibility events
      • listens for layout changes and reports to the system
    3. 3.
      relays the content relayed from (AbstractComposeView::setContent →) doSetContent to be run with the provided composition locals
      CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
          ProvideAndroidCompositionLocals(this.owner, content)
      }
      • LocalInspectionTables mapped to the inspection data set retrieved and populated above
        • .current: MutableSet<CompositionData>? visible in content
      internal fun ProvideAndroidCompositionLocals(
          owner: AndroidComposeView,
          content: @Composable () -> Unit,
      )
      • owner passed the WrappedComposition::owner: AndroidComposeView
      • content relayed the content: @Composable () -> Unit from (AbstractComposeView::setContent →) doSetContent
      • provides more composition locals for, and calls, the content
        • eg LocalContext mapped to owner.context: Context!

CompositionImpl::setContent

override fun setContent(content: @Composable () -> Unit)
  1. 1.
    calls this.clearDeactivated(): Boolean; sets this.state = RUNNING if this.state == DEACTIVATED & returns the predicate
  2. 2.
    checks the invariants; throws unless
    • this.state == RUNNING; either new (RUNNING) or reactivated (RUNNINGDEACTIVATED)
      • INCONSISTENTthis: CompositionImpl must be disposed of because the previous composition was cancelled
      • DISPOSED → invalid for use because this: CompositionImpl has been disposed of
      • DEACTIVATED → should be activated again before setting the content
    • this.pendingPausedComposition == null; a pausable composition is already in progress
  3. 3.
    if reactivated, calls (this.composer: ComposerImpl).startReuseFromRoot()
    fun startReuseFromRoot() {
        reusingGroup = rootKey
        reusing = true
    }
  4. 4.
    calls (this: CompositionImpl).composeInitial(content); relays the provided steps to expose itself to tooling, launch effects, and run the registered @Composable
    1. 1.
      sets this.composable = content; saves the provided steps to update (calculate and apply changes to) itself
    2. 2.
      calls this.parent.composeInitial(this, this.composable)
      • CompositionContext::composeInitial delegates up the hierarchy to the Recomposer: CompositionContext impl
  5. 5.
    if reactivated, calls this.composer.endReuseFromRoot()
    fun endReuseFromRoot() {
        requirePrecondition(!isComposing && reusingGroup == rootKey) {
            "Cannot disable reuse from root if it was caused by other groups"
        }
        reusingGroup = -1
        reusing = false
    }

Recomposer::composeInitial

internal override fun composeInitial(
    composition: ControlledComposition,
    content: @Composable () -> Unit,
)
  1. 1.
    decides to advance the global snapshot later unless the method was entered while in ComposerImpl::{recomposeToGroupEnd, recomposeMovableContent, prepareCompose, doCompose}
  2. 2.
    calls this.registerCompositionLocked(composition) if composition !in this.knownCompositionsLocked()
    • registered with every observer in Recomposer::registrationObservers: MutableObjectList<CompositionRegistrationObserver>? if the composition is ObservableComposition
  3. 3.
    tries this.composing(composition, null) { composition.composeContent(content) } and returns on throw
    private inline fun <T> composing(
        composition: ControlledComposition,
        modifiedValues: MutableScatterSet<Any>?,
        block: () -> T,
    ): T
    • composition relayed the composition: ControlledComposition from Recomposer::composeInitial
    • block passed a partial application of composition::composeContent to content
      • content executes the provided steps to update WrappedComposition::original: Composition in a snapshot
    1. 1.
      takes a mutable snapshot with
      • a read observer: { value -> composition.recordReadOf(value) }
      • a write observer:
        { value ->
            composition.recordWriteOf(value)
            modifiedValues?.add(value)
        }
    2. 2.
      enters the snapshot and runs the block; fully applies composition::composeContent to content
    3. 3.
      relays the return value of the block after applying the snapshot; throws & disposes on failure
  4. 4.
    calls this.addKnownCompositionLocked(composition) if composition !in this.knownCompositionsLocked()
    • caches the composition in Recomposer::_knownCompositions: MutableList<ControlledComposition>
    • Recomposer: _knownCompositionsCache: List<ControlledComposition>? assigned a clone on mutation
  5. 5.
    advances the global snapshot if it was decided to do so earlier
  6. 6.
    tries this.performInitialMovableContentInserts(composition); returns on throw
  7. 7.
    tries to apply all changes; returns on throw
    1. 1.
      composition.applyChanges()
    2. 2.
      composition.applyLateChanges()
  8. 8.
    advances the global snapshot again if it was decided to do so earlier

(CompositionImpl as ControlledComposition)::recordReadOf

override fun recordReadOf(value: Any): Unit
  1. 1.
    returns immediately in case
    • this.areChildrenComposing; nop if notified of a read from a child composition
    • if this.composer.currentRecomposeScope is null; aliased scope: RecomposeScopeImpl otherwise
      • retrieved from this.composer.invalidateStack: Stack<RecomposeScopeImpl>; populated in
        • ComposerImpl::recomposeToGroupEnd
        • ComposerImpl::addRecomposeScope RecomposeScopeImpl(composition as CompositionImpl)
  2. 2.
    marks the scope as used; sets scope.used = true
  3. 3.
    calls scope.recordRead(value); as part of tracking the value, the composition records the read in the current scope: RecomposeScopeImpl
    • true if already recorded → already tracked by the composition
    fun recordRead(instance: Any): Boolean
    • a RecomposeScopeImpl method that records a read of the instance; invalidated on change
    • given a new currentToken: Int each time a composition starts composing in the recompose scope
    • maps in the scope each read instance to the currentToken: Int at the time of the read
    1. 1.
      immediately returns false if (this: RecomposeScopeImpl).rereading since reset
    2. 2.
      RecomposeScopeImpl::trackedInstances: MutableObjectIntMap<Any>? = null lazily assigned a !null instance
      • optimised to map object keys to Int values
      • .put(key: K, value: Int, default: Int): Int
        • maps the key to the value
        • returns the previous value the key was mapped to, or the default if new
    3. 3.
      returns whether .put(instance, this.currentToken, default = -1) equals this.currentToken: Int
      • true if instance was previously mapped to this.currentToken
  4. 4.
    calls this.observer()?.onReadInScope(scope, value)
    private fun observer()
        : CompositionObserver?
        = this.observerHolder.current()
    • implementors of CompositionObserver registered with compositions & have callbacks invoked by the respective compositions
    • used to observe recompose scope management in a composition by tooling for inspection/debugging
  5. 5.
    returns if already tracked
  6. 6.
    records that the value has been read in a composition if StateObjectImpl for optimisation purposes
    • internal abstract class StateObjectImpl: StateObject; instantiated by factories such as mutableStateOf
    • StateObjectImpl::readerKind: AtomicInt
      • set in StateObjectImpl::recordReadIn(reader: ReaderKind): Unit
      • compared against StateObjectImpl::isReadIn(reader: ReaderKind): Boolean
  7. 7.
    calls this.observations.add(value, scope); adds scope to the population of recompose scopes that the value is read in
    • ScopeMap<K: Any, V: Any> wraps a MutableScatterMap<Any, Any>
      • each K key accumulates a population of V elements; first mapped to an element then a set
      • this.observations: ScopeMap<Any, RecomposeScopeImpl>; state objects → the populations of recompose scopes they’re read in
      • this.derivedStates: ScopeMap<Any, DerivedState<*>>; state objects → the populations of derived states from it
    • ScopeMap::add calls MutableScatterMap::compute; the given key mapped to
      • the V if there’s no existing entry or the key is already mapped to the element
      • otherwise a MutableScatterSet<V> of every element the key’s been mapped to
  8. 8.
    maps dependencies if DerivedState<*> in this.derivedStates: ScopeMap<Any, DerivedState<*>>

(CompositionImpl as ControlledComposition)::recordWriteOf

override fun recordWriteOf(value: Any)
  1. 1.
    calls this::invalidateScopeOfLocked
    1. 1.
      passed value; invalidates all recompose scopes that read it
      private fun invalidateScopeOfLocked(value: Any)
      1. 1.
        repeats for each scope: RecomposeScopeImpl in the population that the value is mapped to in this.observations: ScopeMap<Any, RecomposeScopeImpl>
      2. 2.
        skips to the next iteration unless scope.invalidateForResult(value) == InvalidationResult.IMMINENT
        1. 1.
          calls RecomposeScopeOwner::invalidate on the scope.owner: RecomposeScopeOwner? if !null
          fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult
          • calls (CompositionImpl as RecomposeScopeOwner)::invalidate on this call path
          • scope relayed the scope: RecomposeScopeImpl receiver of the invalidateForResult method
          • instance relayed the value: Any? argument from the invalidateForResult method
          1. 1.
            sets scope.defaultsInvalid = true if scope.defaultsInScope; the default params also refreshed
          2. 2.
            declares val anchor: Anchor? = scope.anchor; the slice in the slot table that the recompose scope corresonds to
          3. 3.
            returns InvalidationResult.IGNORED if anchor == null || !anchor.valid
            • makes the recomposition nop since the scope is not part of the composition (anymore)
          4. 4.
            returns InvalidationResult.{IMMINENT | IGNORED} if !this.slotTable.ownsAnchor(anchor)
            • reached if still part of the slot table
            • the scope not owned by this; queries a delegate composition
            1. 1.
              declares val delegate: CompositionImpl? = synchronized(this.lock) { this.invalidationDelegate }
              • the property temporarily !null while inside this::delegateInvalidations
              • where there are a ControlledComposition that’s not this
              • and a groupIndex: Int that’s >= 0
            2. 2.
              returns InvalidationResult.IMMINENT if delegate?.tryImminentInvalidation(scope, instance) == true
              • CompositionImpl::recordWriteOf run on each state write in a snapshot where the composition is composed
              • true if the scope will be recomposed by the delegate within the current (before the next) composition pass
            3. 3.
              returns InvalidationResult.IGNORED otherwise; to be recomposed by neither this nor the delegate
          5. 5.
            returns InvalidationResult.IGNORED if !scope.canRecompose; has opted out or is unable
            • reached if still part of the slot table & directly owned by the scope, not a delegate
          6. 6.
            returns this.invalidateChecked(scope, anchor, instance)
            • reached if still part of the slot table & directly owned by the scope, not a delegate & eligible for recomposition
            • also notifies on return this.observer() unless InvalidationResult.IGNORED
            1. 1.
              declares val delegate: CompositionImpl?; initialised with
              1. 1.
                this.invalidationDelegate if this.slotTable.groupContainsAnchor(this.invalidationDelegateGroup, anchor)
                • this.invalidationDelegateGroup: Int = 0 assigned a different value while in this::delegateInvalidations
                • true if (the slice corresponded to by) the recompose scope (as pointed to by the anchor) is in a movable content
              2. 2.
                return InvalidationResult.IMMINENT (recursion base) if this.tryImminentInvalidation(scope, instance)
                • CompositionImpl::recordWriteOf run on each state write in a snapshot where the composition is composed
                • true if the scope will be recomposed by this within the current (before the next) composition pass
              3. 3.
                null otherwise
                1. 1.
                  calls this.invalidations.set(scope, ScopeInvalidated) if instance == null || instance !is DerivedState<*>
                  • this.invalidations: ScopeMap<RecomposeScopeImpl, Any>; recompose scopes → the populations of outdated state objects they read
                  • ScopeMap::set overrides and replaces the current population (if present) with the given element
                  • internal object ScopeInvalidated indicates that an invalidated recompose scope doesn’t track the population of outdated states it read (eg when RecomposeScopeImpl::invalidate has passed instance = null to RecomposeScopeOwner::invalidate)
                2. 2.
                  calls this.invalidations.add(scope, instance) if !this.invalidations.anyScopeOf(scope) { it === ScopeInvalidated }
                  • ie if this.invalidations doesn’t map the scope to ScopeInvalidated (→ unconditionally recomposed)
                  • reached if DerivedState<*>; kept track of since derived states can avoid recomposition sometimes
                  • populates the population of outdated states read by the scope
            2. 2.
              recurses if delegate != null; returns delegate.invalidateChecked(scope, anchor, instance)
            3. 3.
              calls this.parent.invalidate(this); notifies the CompositionContext
              internal abstract fun invalidate(composition: ControlledComposition)
              • calls (Recomposer as CompositionContext)::invalidate on the call path from ComponentActivity::setContent
              1. 1.
                adds the composition to Recomposer::compositionInvalidations: MutableVector<ControlledComposition> if not already contained
              2. 2.
                resumes the Recomposer::workContinuation: CancellableContinuation<Unit>? if eligible
                • every call to a suspend function needs a coroutine to run in
                • the continuation of a suspend function call is delimited by the coroutine and reified for use in CPS
                • when a suspend function returns a T, the Continuation<T> (the rest of the coroutine) is resumed with a T
            4. 4.
              returns (recursion base)
              • InvalidationResult.DEFERRED if this.isComposing
              • InvalidationResult.SCHEDULED otherwise
        2. 2.
          otherwise returns InvalidationResult.IGNORED
      3. 3.
        calls this.observationsProcessed.add(value, scope) if unskipped
        • this.observationsProcessed: ScopeMap<Any, RecomposeScopeImpl>; state objects → the populations of recompose scopes that are recomposed in the current pass
    2. 2.
      passed each in the population of derived states from the value; invalidates all recompose scopes that read each