/*
* Copyright 2020 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
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/**
* Schedule [effect] to run when the current composition completes successfully and applies
* changes. [SideEffect] can be used to apply side effects to objects managed by the
* composition that are not backed by [snapshots][androidx.compose.runtime.snapshots.Snapshot] so
* as not to leave those objects in an inconsistent state if the current composition operation
* fails.
*
* [effect] will always be run on the composition's apply dispatcher and appliers are never run
* concurrent with themselves, one another, applying changes to the composition tree, or running
* [CompositionLifecycleObserver] event callbacks. [SideEffect]s are always run after
* [CompositionLifecycleObserver] event callbacks.
*
* A [SideEffect] runs after **every** recomposition. To launch an ongoing task spanning
* potentially many recompositions, see [LaunchedEffect]. To manage an event subscription or other
* object lifecycle, see [DisposableEffect].
*/
@Composable
@ComposableContract(restartable = false)
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
/**
* Receiver scope for [DisposableEffect] that offers the [onDispose] clause that should be
* the last statement in any call to [DisposableEffect].
*/
class DisposableEffectScope {
/**
* Provide [onDisposeEffect] to the [DisposableEffect] to run when it leaves the composition
* or its subject changes.
*/
inline fun onDispose(
crossinline onDisposeEffect: () -> Unit
): DisposableEffectDisposable = object : DisposableEffectDisposable {
override fun dispose() {
onDisposeEffect()
}
}
}
interface DisposableEffectDisposable {
fun dispose()
}
private val InternalDisposableEffectScope = DisposableEffectScope()
private class DisposableEffectImpl(
private val effect: DisposableEffectScope.() -> DisposableEffectDisposable
) : CompositionLifecycleObserver {
private var onDispose: DisposableEffectDisposable? = null
override fun onEnter() {
onDispose = InternalDisposableEffectScope.effect()
}
override fun onLeave() {
onDispose?.dispose()
onDispose = null
}
}
private const val DisposableEffectNoParamError =
"DisposableEffect must provide one or more 'subject' parameters that define the identity of " +
"the DisposableEffect and determine when its previous effect should be disposed and " +
"a new effect started for the new subject."
private const val LaunchedEffectNoParamError =
"LaunchedEffect must provide one or more 'subject' parameters that define the identity of " +
"the LaunchedEffect and determine when its previous effect coroutine should be cancelled " +
"and a new effect launched for the new subject."
@Composable
@ComposableContract(restartable = false)
@Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER")
@Deprecated(DisposableEffectNoParamError, level = DeprecationLevel.ERROR)
fun DisposableEffect(
effect: DisposableEffectScope.() -> DisposableEffectDisposable
): Unit = error(DisposableEffectNoParamError)
/**
* A side effect of composition that must run for any new unique value of [subject] and must be
* reversed or cleaned up if [subject] changes or if the [DisposableEffect] leaves the composition.
*
* A [DisposableEffect]'s _subject_ is a value that defines the identity of the
* [DisposableEffect]. If a subject changes, the [DisposableEffect] must
* [dispose][DisposableEffectScope.onDispose] its current [effect] and reset by calling [effect]
* again. Examples of subjects include:
*
* * Observable objects that the effect subscribes to
* * Unique request parameters to an operation that must cancel and retry if those parameters change
*
* [DisposableEffect] may be used to initialize or subscribe to a subject and reinitialize
* when a different subject is provided, performing cleanup for the old operation before
* initializing the new. For example:
*
* @sample androidx.compose.runtime.samples.disposableEffectSample
*
* A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause
* as the final statement in its [effect] block. If your operation does not require disposal
* it might be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should
* be managed by the composition.
*
* There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call
* to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run
* on the composition's apply dispatcher and appliers are never run concurrent with themselves,
* one another, applying changes to the composition tree, or running [CompositionLifecycleObserver]
* event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
fun DisposableEffect(
subject: Any?,
effect: DisposableEffectScope.() -> DisposableEffectDisposable
) {
remember(subject) { DisposableEffectImpl(effect) }
}
/**
* A side effect of composition that must run for any new unique value of [subject1] or [subject2]
* and must be reversed or cleaned up if [subject1] or [subject2] changes, or if the
* [DisposableEffect] leaves the composition.
*
* A [DisposableEffect]'s _subject_ is a value that defines the identity of the
* [DisposableEffect]. If a subject changes, the [DisposableEffect] must
* [dispose][DisposableEffectScope.onDispose] its current [effect] and reset by calling [effect]
* again. Examples of subjects include:
*
* * Observable objects that the effect subscribes to
* * Unique request parameters to an operation that must cancel and retry if those parameters change
*
* [DisposableEffect] may be used to initialize or subscribe to a subject and reinitialize
* when a different subject is provided, performing cleanup for the old operation before
* initializing the new. For example:
*
* @sample androidx.compose.runtime.samples.disposableEffectSample
*
* A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause
* as the final statement in its [effect] block. If your operation does not require disposal
* it might be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should
* be managed by the composition.
*
* There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call
* to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run
* on the composition's apply dispatcher and appliers are never run concurrent with themselves,
* one another, applying changes to the composition tree, or running [CompositionLifecycleObserver]
* event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
fun DisposableEffect(
subject1: Any?,
subject2: Any?,
effect: DisposableEffectScope.() -> DisposableEffectDisposable
) {
remember(subject1, subject2) { DisposableEffectImpl(effect) }
}
/**
* A side effect of composition that must run for any new unique value of [subject1], [subject2]
* or [subject3] and must be reversed or cleaned up if [subject1], [subject2] or [subject3]
* changes, or if the [DisposableEffect] leaves the composition.
*
* A [DisposableEffect]'s _subject_ is a value that defines the identity of the
* [DisposableEffect]. If a subject changes, the [DisposableEffect] must
* [dispose][DisposableEffectScope.onDispose] its current [effect] and reset by calling [effect]
* again. Examples of subjects include:
*
* * Observable objects that the effect subscribes to
* * Unique request parameters to an operation that must cancel and retry if those parameters change
*
* [DisposableEffect] may be used to initialize or subscribe to a subject and reinitialize
* when a different subject is provided, performing cleanup for the old operation before
* initializing the new. For example:
*
* @sample androidx.compose.runtime.samples.disposableEffectSample
*
* A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause
* as the final statement in its [effect] block. If your operation does not require disposal
* it might be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should
* be managed by the composition.
*
* There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call
* to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run
* on the composition's apply dispatcher and appliers are never run concurrent with themselves,
* one another, applying changes to the composition tree, or running [CompositionLifecycleObserver]
* event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
fun DisposableEffect(
subject1: Any?,
subject2: Any?,
subject3: Any?,
effect: DisposableEffectScope.() -> DisposableEffectDisposable
) {
remember(subject1, subject2, subject3) { DisposableEffectImpl(effect) }
}
/**
* A side effect of composition that must run for any new unique value of [subjects] and must
* be reversed or cleaned up if any [subjects] change or if the [DisposableEffect] leaves the
* composition.
*
* A [DisposableEffect]'s _subject_ is a value that defines the identity of the
* [DisposableEffect]. If a subject changes, the [DisposableEffect] must
* [dispose][DisposableEffectScope.onDispose] its current [effect] and reset by calling [effect]
* again. Examples of subjects include:
*
* * Observable objects that the effect subscribes to
* * Unique request parameters to an operation that must cancel and retry if those parameters change
*
* [DisposableEffect] may be used to initialize or subscribe to a subject and reinitialize
* when a different subject is provided, performing cleanup for the old operation before
* initializing the new. For example:
*
* @sample androidx.compose.runtime.samples.disposableEffectSample
*
* A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause
* as the final statement in its [effect] block. If your operation does not require disposal
* it might be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should
* be managed by the composition.
*
* There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call
* to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run
* on the composition's apply dispatcher and appliers are never run concurrent with themselves,
* one another, applying changes to the composition tree, or running [CompositionLifecycleObserver]
* event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
@Suppress("ArrayReturn")
fun DisposableEffect(
vararg subjects: Any?,
effect: DisposableEffectScope.() -> DisposableEffectDisposable
) {
remember(*subjects) { DisposableEffectImpl(effect) }
}
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : CompositionLifecycleObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onEnter() {
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onLeave() {
job?.cancel()
job = null
}
}
/**
* When [LaunchedEffect] enters the composition it will launch [block] into the composition's
* [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] when the [LaunchedEffect]
* leaves the composition.
*
* It is an error to call [LaunchedEffect] without at least one `subject` parameter.
*/
@Deprecated(LaunchedEffectNoParamError, level = DeprecationLevel.ERROR)
@Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER")
@Composable
fun LaunchedEffect(
block: suspend CoroutineScope.() -> Unit
): Unit = error(LaunchedEffectNoParamError)
/**
* When [LaunchedEffect] enters the composition it will launch [block] into the composition's
* [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when
* [LaunchedEffect] is recomposed with a different [subject]. The coroutine will be
* [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition.
*
* This function should **not** be used to (re-)launch ongoing tasks in response to callback
* events by way of storing callback data in [MutableState] passed to [subject]. Instead, see
* [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs
* scoped to the composition in response to event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
fun LaunchedEffect(
subject: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(subject) { LaunchedEffectImpl(applyContext, block) }
}
/**
* When [LaunchedEffect] enters the composition it will launch [block] into the composition's
* [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when
* [LaunchedEffect] is recomposed with a different [subject1] or [subject2]. The coroutine will be
* [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition.
*
* This function should **not** be used to (re-)launch ongoing tasks in response to callback
* events by way of storing callback data in [MutableState] passed to [key]. Instead, see
* [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs
* scoped to the composition in response to event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
fun LaunchedEffect(
subject1: Any?,
subject2: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(subject1, subject2) { LaunchedEffectImpl(applyContext, block) }
}
/**
* When [LaunchedEffect] enters the composition it will launch [block] into the composition's
* [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when
* [LaunchedEffect] is recomposed with a different [subject1], [subject2] or [subject3].
* The coroutine will be [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition.
*
* This function should **not** be used to (re-)launch ongoing tasks in response to callback
* events by way of storing callback data in [MutableState] passed to [key]. Instead, see
* [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs
* scoped to the composition in response to event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
fun LaunchedEffect(
subject1: Any?,
subject2: Any?,
subject3: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(subject1, subject2, subject3) { LaunchedEffectImpl(applyContext, block) }
}
/**
* When [LaunchedEffect] enters the composition it will launch [block] into the composition's
* [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when
* [LaunchedEffect] is recomposed with any different [subjects]. The coroutine will be
* [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition.
*
* This function should **not** be used to (re-)launch ongoing tasks in response to callback
* events by way of storing callback data in [MutableState] passed to [key]. Instead, see
* [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs
* scoped to the composition in response to event callbacks.
*/
@Composable
@ComposableContract(restartable = false)
@Suppress("ArrayReturn")
fun LaunchedEffect(
vararg subjects: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(*subjects) { LaunchedEffectImpl(applyContext, block) }
}