[go: nahoru, domu]

blob: 8778ddf48c74ff5fc3a14d9390e6f7514f0f7448 [file] [log] [blame]
/*
* 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
/**
* 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.
*/
val hasInvalidations: Boolean
/**
* True if [dispose] has been called.
*/
val isDisposed: Boolean
/**
* Clear the hierarchy that was created from the composition.
*/
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 tree.
* @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
*/
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>)
/**
* 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
/**
* 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()
/**
* 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()
}
/**
* 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 composition reference, if applicable. Default is null.
*
* @see Applier
* @see Composition
* @see Recomposer
*/
fun Composition(
applier: Applier<*>,
parent: CompositionContext
): Composition =
CompositionImpl(
parent,
applier
)
/**
* 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.
*
* 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 composition reference, if applicable. Default is null.
*
* @see Applier
* @see Composition
* @see Recomposer
*/
@TestOnly
fun ControlledComposition(
applier: Applier<*>,
parent: CompositionContext
): ControlledComposition =
CompositionImpl(
parent,
applier
)
/**
* @param parent An optional reference to the parent composition.
* @param applier The applier to use to manage the tree built by the composer.
* @param onDispose A callback to be triggered when [dispose] is called.
*/
internal class CompositionImpl(
private val parent: CompositionContext,
applier: Applier<*>,
private val onDispose: (() -> Unit)? = null
) : ControlledComposition {
private val composer: ComposerImpl = ComposerImpl(applier, parent, this).also {
parent.registerComposer(it)
}
/**
* Return true if this is a root (non-sub-) composition.
*/
val isRoot: Boolean = parent is Recomposer
private var disposed = false
var composable: @Composable () -> Unit = {}
override val isComposing: Boolean
get() = composer.isComposing
override val isDisposed: Boolean = disposed
override val hasPendingChanges: Boolean
get() = composer.hasPendingChanges
override fun setContent(content: @Composable () -> Unit) {
check(!disposed) { "The composition is disposed" }
this.composable = content
parent.composeInitial(this, composable)
}
override fun composeContent(content: @Composable () -> Unit) {
composer.composeContent(content)
}
@OptIn(ExperimentalComposeApi::class)
override fun dispose() {
if (!disposed) {
disposed = true
composable = {}
composer.dispose()
parent.unregisterComposition(this)
onDispose?.invoke()
}
}
override val hasInvalidations get() = composer.hasInvalidations
override fun recordModificationsOf(values: Set<Any>) {
composer.recordModificationsOf(values)
}
override fun recordReadOf(value: Any) {
composer.recordReadOf(value)
}
override fun recordWriteOf(value: Any) {
composer.recordWriteOf(value)
}
override fun recompose(): Boolean = composer.recompose()
override fun applyChanges() {
composer.applyChanges()
}
override fun invalidateAll() {
composer.invalidateAll()
}
override fun verifyConsistent() {
composer.verifyConsistent()
}
}
/**
* 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))
}
}
}
/**
* @suppress
*/
@TestOnly
fun simulateHotReload(context: Any) = HotReloader.simulateHotReload(context)