blob: e76fd3c7edf4c47c2228daa4c4cafca910d671cd [file]
/*
* Copyright 2023 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.
*/
package androidx.compose.runtime.tooling
import androidx.compose.runtime.Composition
import androidx.compose.runtime.ExperimentalComposeRuntimeApi
import androidx.compose.runtime.ObservableCompositionServiceKey
import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.getCompositionService
/**
* Observe when new compositions are added to a recomposer. This, combined with,
* [CompositionObserver], allows observing when any composition is being performed.
*
* This observer is registered with a [Recomposer] by calling [Recomposer.observe].
*/
@Suppress("CallbackName")
@ExperimentalComposeRuntimeApi
public interface CompositionRegistrationObserver {
/**
* Called whenever a [Composition] is registered with a [Recomposer] for which this is an
* observer. A Composition is registered with its Recomposer when it begins its initial
* composition, before any content is added. When a [CompositionRegistrationObserver] is
* registered, this method will be called for all the [Recomposer]'s currently known
* composition.
*
* This method is called on the same thread that the [Composition] being registered is being
* composed on. During the initial dispatch, it is invoked on the same thread that the callback
* is being registered on. Implementations of this method should be thread safe as they might be
* called on an arbitrary thread.
*
* @param composition The [Composition] instance that is being registered with the recomposer.
*/
public fun onCompositionRegistered(composition: ObservableComposition)
/**
* Called whenever a [Composition] is unregistered with a [Recomposer] for which this is an
* observer. A Composition is unregistered from its Recomposer when the composition is
* [disposed][Composition.dispose]. This method is called on the same thread that the
* [Composition] being unregistered was composed on. Implementations of this method should be
* thread safe as they might be called on an arbitrary thread.
*
* @param composition The [Composition] instance that is being unregistered with the recomposer.
*/
public fun onCompositionUnregistered(composition: ObservableComposition)
}
/**
* Observe [RecomposeScope] management inside composition.
*
* The expected lifecycle of a [RecomposeScope] in composition is as follows:
* ```
* // In the composition phase:
* [onBeginComposition]
* ┃
* [onScopeEnter] ━┓ // Composition enters a scope
* ┃
* [onReadInScope] // Record reads inside the scope
* ┃
* ┗━ [onScopeEnter]
* ... // Potentially enter nested function scopes
* ┏━ [onScopeExit]
* ┃
* [onScopeExit] ━┛ // Composition leaves the scope
* ┃
* [onEndComposition]
*
* // In the apply changes phase:
* [onScopeDisposed] // Scope is discarded by composition and is no longer used.
* ```
*
* The scopes can be invalidated at any point either by values previously reported in
* [onReadInScope] or by calling [RecomposeScope.invalidate] directly. In these cases,
* [onScopeInvalidated] will be called with the associated instance or `null` if the scope was
* invalidated directly.
*
* Note that invalidation of the scope does not guarantee it will be composed. Some cases where it
* is not composed are:
* 1) The scope is no longer part of the composition (e.g the parent scope no longer executed the
* code branch the scope was a part of)
* 2) The scope is part of movable content that was moved out of the composition.
*
* In the case of movable content, the scope will be recomposed as part of a different composition
* when it is moved to that composition or it might be discarded (with a corresponding
* [onScopeDisposed] call) if no other composition claims it.
*/
@Suppress("CallbackName")
@ExperimentalComposeRuntimeApi
public interface CompositionObserver {
/** Called when the composition process begins for [composition] instance. */
public fun onBeginComposition(composition: ObservableComposition)
/** Called when [scope] enters the composition. */
public fun onScopeEnter(scope: RecomposeScope)
/**
* Called when read of [value] is recorded in [scope] during composition.
*
* Reads can be recorded without re-execution of the function associated with the scope, for
* example when derived state invalidates the scope with the same value as before.
*
* The instances passed to this method are only tracked between [onScopeEnter] and [onScopeExit]
* calls. Previously recorded instances should also be cleared when [onScopeEnter] is called
* again, to avoid keeping stale instances that are no longer tracked by composition. For
* example, this happens with `remember { state.value }`, with `state` recorded only during
* first composition.
*
* @param scope A [RecomposeScope] that the read occurred in.
* @param value A value that was recorded in [scope] and can invalidate it in the future. In
* most cases, [value] is a snapshot state instance.
*/
public fun onReadInScope(scope: RecomposeScope, value: Any)
/** Called when [RecomposeScope] exits composition. */
public fun onScopeExit(scope: RecomposeScope)
/** Called after composition process has been completed for [composition]. */
public fun onEndComposition(composition: ObservableComposition)
/**
* Called when [scope] is invalidated by composition or [RecomposeScope.invalidate] call.
*
* Note that for invalidations caused by a state change, this callback is not called immediately
* on the state write. Usually, invalidations from state changes are recorded right before
* recomposition starts or during composition (e.g. if `rememberUpdatedState` is used). This
* method is always guaranteed to be called before [onScopeEnter] for the corresponding
* invalidation is executed. If the scope was invalidated by [RecomposeScope.invalidate],
* however, this callback is executed before [RecomposeScope.invalidate] returns.
*
* @param scope A [RecomposeScope] that is invalidated.
* @param value A value that invalidated composition. Can be `null` if the scope was invalidated
* by calling [RecomposeScope.invalidate] directly.
*/
public fun onScopeInvalidated(scope: RecomposeScope, value: Any?)
/**
* Called when [RecomposeScope] is no longer used in composition. Can be called from any thread
* whenever composition applies changes or is disposed.
*/
public fun onScopeDisposed(scope: RecomposeScope)
}
/**
* The handle returned by [Composition.setObserver] and [Recomposer.observe]. Calling [dispose] will
* prevent further composition observation events from being sent to the registered observer.
*/
@ExperimentalComposeRuntimeApi
public interface CompositionObserverHandle {
/** Unregister the observer. */
public fun dispose()
}
/** A composition instance that supports observing lifecycle of its [RecomposeScope]. */
@ExperimentalComposeRuntimeApi
public interface ObservableComposition {
/**
* Observe the composition. Calling this twice on the same composition will implicitly dispose
* the previous observer. the [CompositionObserver] will be called for this composition and all
* sub-composition, transitively, for which this composition is a context. If [setObserver] is
* called on a sub-composition, it will override the parent composition observer for itself and
* all its sub-compositions.
*
* @param observer the observer that will be informed of composition events for this composition
* and all sub-compositions for which this composition is the composition context.
* @return a handle that allows the observer to be disposed and detached from the composition.
* Disposing an observer for a composition with a parent observer will begin sending the
* events to the parent composition's observer.
*/
public fun setObserver(observer: CompositionObserver): CompositionObserverHandle
}
/**
* Register an observer to be notified when a composition is added to or removed from the given
* [Recomposer]. When this method is called, the observer will be notified of all currently
* registered compositions per the documentation in
* [CompositionRegistrationObserver.onCompositionRegistered].
*
* @param observer the observer that will be informed of new compositions registered with this
* [Recomposer].
* @return a handle that allows the observer to be disposed and detached from the [Recomposer].
*/
@ExperimentalComposeRuntimeApi
public fun Recomposer.observe(
observer: CompositionRegistrationObserver
): CompositionObserverHandle {
return addCompositionRegistrationObserver(observer)
}
/**
* Observe the composition. Calling this twice on the same composition will implicitly dispose the
* previous observer. the [CompositionObserver] will be called for this composition and all
* sub-composition, transitively, for which this composition is a context. If [setObserver] is
* called on a sub-composition, it will override the parent composition observer for itself and all
* its sub-compositions.
*
* @param observer the observer that will be informed of composition events for this composition and
* all sub-compositions for which this composition is the composition context. Observing a
* composition will prevent the parent composition's observer from receiving composition events
* about this composition.
* @return a handle that allows the observer to be disposed and detached from the composition.
* Disposing an observer for a composition with a parent observer will begin sending the events to
* the parent composition's observer. A `null` indicates the composition does not support being
* observed.
*/
@ExperimentalComposeRuntimeApi
public fun Composition.setObserver(observer: CompositionObserver): CompositionObserverHandle? =
getCompositionService(ObservableCompositionServiceKey)?.setObserver(observer)