blob: 51bb2b102504d75d1cf4ff2aef0015645fd63489 [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:Suppress("unused")
package androidx.compose
import android.view.Choreographer
import androidx.annotation.CheckResult
import androidx.compose.annotations.Hide
import androidx.compose.frames.AbstractRecord
import androidx.compose.frames.Framed
import androidx.compose.frames.Record
import androidx.compose.frames._created
import androidx.compose.frames.readable
import androidx.compose.frames.writable
import kotlin.reflect.KProperty
/**
* This is just a sentinel object that represents the absence of an explicit key being defined. This is necessary because
* we want `null` to be a valid key, and not the absence of one.
*/
private val absentKey = object {}
/**
* This creates a composite key of any value to be used as the key for the group of an effect
*/
private fun joinKey(left: Any?, right: Any?): Any = JoinedKey(left, right)
/**
* The Effect class is really an opaque class that holds a block of executable code that is meant to be executed positionally in the
* context of a composition. Since this block is positional, it must also know how to construct its own "key".
*
* NOTE: Effects are equivalent to [Composable] functions that are able to have a return value, where the [unaryPlus] operator
* is the manner in which it is "invoked". Effect will likely go away in favor of unifying with [Composable].
*
* @see [Composable]
*/
@EffectsDsl
class Effect<T> internal constructor(
/**
* This is a lambda that gets executed in the context of this effect. In most cases, this is where the logic of the effect goes.
*/
@PublishedApi
internal val block: Effect<T>.() -> T,
/**
* This is an optional parameter to store a key or combined key that will be used when the effect gets positionally memoized.
*/
private val dataKey: Any? = absentKey
) {
/**
* Effects need to interact with the ViewComposer in order to work, however most custom effects will simply compose themselves
* from the primitives, as opposed to using the ViewComposer directly.
*/
@Hide
lateinit var context: Composer<*>
/**
* We add a composer here with type Unit so that it blocks people from invoking composables where there is a
* receiver scope of type Effect<T>
*
* @suppress
*/
@Hide
val composer = Unit
/**
* This method constructs the key of the effect. This is done primarily just as a performance optimization for the common
* case of `dataKey` not being defined, which should be the common case
*/
@Hide
fun constructKey(key: Int) = if (dataKey === absentKey) key else joinKey(
key,
dataKey
)
/**
* This is the call that "unwraps" the effect into a value, and participates in positional memoization. It is important
* that this call gets inlined, as it is where the `sourceLocation()` gets used and does the positional memoization.
*
* The interesting thing here is that we see that an Effect is really little more than a controlled way of creating
* a group in the slot table.
*/
@Suppress("NOTHING_TO_INLINE")
@Hide
/* inline */ fun resolve(
composerContext: Composer<*>,
key: Int = sourceLocation()
): T = with(composerContext) {
this@Effect.context = this
startGroup(constructKey(key))
val result = block()
endGroup()
return result
}
/**
* The unaryPlus operator, in the scope of an effect, is an alias to resolving the effect
*/
@Suppress("NOTHING_TO_INLINE")
/* inline */ operator fun <V> Effect<V>.unaryPlus(): V = resolve(this@Effect.context)
}
/**
* This is the public version of the Effect constructor. It is meant to be used to compose effects together to create custom effects.
*
* For example, a custom `observeUser` Effect might look something like this:
*
* fun observeUser(userId: Int) = effectOf<User?> {
* val user = +stateFor<User?>(userId) { null }
* +onCommit(userId) {
* val subscription = UserAPI.subscribeToUser(userId) {
* user.value = it
* }
* onDispose {
* subscription.unsubscribe()
* }
* }
* user.value
* }
*
* @param block the executable block of code that returns the value of the effect, run in the context of the Effect
*/
fun <T> effectOf(block: Effect<T>.() -> T): Effect<T> =
Effect(block)
/**
* A CommitScope represents an object that executes some code and has a cleanup in the context of the Composition lifecycle.
* It has an "onDispose" operation to cleanup anything that it created whenever it leaves the composition.
*/
@EffectsDsl
interface CommitScope {
/**
* Provide a lambda which will be executed as this CommitScope leaves the composition. It will be executed only once. Use this to
* schedule cleanup for anything that you construct during the CommitScope's creation.
*
* @param callback A callback to be executed when this CommitScope leaves the composition.
*/
fun onDispose(callback: () -> Unit)
}
/**
* For convenience, this is just an empty lambda that we will call on CommitScopes where the user has not defined an
* onDispose. Saving this into a constant saves us an allocation on every initialization
*/
private val emptyDispose: () -> Unit = {}
@PublishedApi
internal class PreCommitScopeImpl(
internal val onCommit: CommitScope.() -> Unit
) : CommitScope, CompositionLifecycleObserver {
private var disposeCallback = emptyDispose
override fun onDispose(callback: () -> Unit) {
assert(disposeCallback === emptyDispose) {
"onDispose(...) should only be called once"
}
disposeCallback = callback
}
override fun onEnter() {
onCommit(this)
}
override fun onLeave() {
disposeCallback()
}
}
@PublishedApi
internal class PostCommitScopeImpl(
internal val onCommit: CommitScope.() -> Unit
) : CommitScope, CompositionLifecycleObserver, Choreographer.FrameCallback {
private var disposeCallback = emptyDispose
private var hasRun = false
override fun onDispose(callback: () -> Unit) {
assert(disposeCallback === emptyDispose) {
"onDispose(...) should only be called once"
}
disposeCallback = callback
}
override fun doFrame(frameTimeNanos: Long) {
hasRun = true
onCommit(this)
}
override fun onEnter() {
// TODO(lmr): we should eventually move this to an expect/actual "scheduler" of some sort
Choreographer.getInstance().postFrameCallback(this)
}
override fun onLeave() {
// If `onCommit` hasn't executed yet, we should not call `onDispose`. We should document
// somewhere the invariants we intend to have around call order for these.
if (hasRun) {
disposeCallback()
} else {
Choreographer.getInstance().removeFrameCallback(this)
}
}
}
/**
* The key effect is a primitive effect that allows for an effect to have a custom group key. This allows for effects to be
* associated with data. If you are constructing effects based on data such as lists or collections, keys can be used to help
* Compose determine which effects should be removed or added. Any other effects can be created inside of the block of the
* key effect.
*
* Example:
*
* for (el in elements)
* val selected = +key(el.id) { +state { false } }
* ListItem(item=el, selected=selected)
*
* @param v1 The value to use as the key. This will be compared to its previous value using `Object.equals`
* @param block The block to execute other effects in
*/
@CheckResult(suggest = "+")
fun <T, V1> key(v1: V1, block: Effect<T>.() -> T) =
Effect(block, v1)
/**
* The key effect is a primitive effect that allows for an effect to have a custom group key. This allows for effects to be
* associated with data. If you are constructing effects based on data such as lists or collections, keys can be used to help
* Compose determine which effects should be removed or added. Any other effects can be created inside of the block of the
* key effect.
*
* A compound key will be created from both [v1] and [v2].
*
* Example:
*
* for (el in elements)
* val selected = +key(el.id, parentId) { +state { false } }
* ListItem(item=el, selected=selected)
*
* @param v1 The first value to use as a key. This will be compared to its previous value using `Object.equals`
* @param v2 The second value to use as a key. This will be compared to its previous value using `Object.equals`
* @param block The block to execute other effects in
*/
@CheckResult(suggest = "+")
fun <T, V1, V2> key(v1: V1, v2: V2, block: Effect<T>.() -> T) =
Effect(block, joinKey(v1, v2))
/**
* The key effect is a primitive effect that allows for an effect to have a custom group key. This allows for effects to be
* associated with data. If you are constructing effects based on data such as lists or collections, keys can be used to help
* Compose determine which effects should be removed or added. Any other effects can be created inside of the block of the key
* effect.
*
* Example:
*
* for (el in elements)
* val selected = +key(el.id, parentId) { +state { false } }
* ListItem(item=el, selected=selected)
*
* @param inputs The set of values to be used to create a compound key. This will be compared to its previous value using `Object.equals`
* @param block The block to execute other effects in
*/
@CheckResult(suggest = "+")
fun <T> key(vararg inputs: Any?, block: Effect<T>.() -> T) =
Effect(
block,
inputs.reduce { acc, item -> joinKey(acc, item) })
/**
* An Effect that positionally memoizes the result of a computation.
*
* @param calculation A function to produce the result
* @return The result of the calculation, or the cached value from the composition
*/
@CheckResult(suggest = "+")
/* inline */ fun <T> memo(/* crossinline */ calculation: () -> T) = effectOf<T> {
context.remember(calculation)
}
/**
* An Effect that positionally memoizes the result of a computation.
*
* @param v1 An input to the memoization. If this value changes, the calculation will be re-executed.
* @param calculation A function to produce the result
* @return The result of the calculation, or the cached value from the composition
*/
@CheckResult(suggest = "+")
/* inline */ fun <T, /* reified */ V1> memo(
v1: V1,
/* crossinline */
calculation: () -> T
) = effectOf<T> {
context.remember(v1, calculation)
}
/**
* An Effect that positionally memoizes the result of a computation.
*
* @param v1 An input to the memoization. If this value changes, the calculation will be re-executed.
* @param v2 An input to the memoization. If this value changes, the calculation will be re-executed.
* @param calculation A function to produce the result
* @return The result of the calculation, or the cached value from the composition
*/
@CheckResult(suggest = "+")
/* inline */ fun <T, /* reified */ V1, /* reified */ V2> memo(
v1: V1,
v2: V2,
/* crossinline */
calculation: () -> T
) = effectOf<T> {
context.remember(v1, v2, calculation)
}
/**
* An Effect that positionally memoizes the result of a computation.
*
* @param inputs The inputs to the memoization. If any of these values change, the calculation will be re-executed.
* @param calculation A function to produce the result
* @return The result of the calculation, or the cached value from the composition
*/
@CheckResult(suggest = "+")
fun <T> memo(vararg inputs: Any?, calculation: () -> T) = effectOf<T> {
context.remember(*inputs) { calculation() }
}
/**
* An effect used to observe the lifecycle of the composition. The [callback] will execute once initially after the first composition
* is applied, and then will not fire again. The [callback] will get executed with a receiver scope that has an
* [onDispose][CommitScope.onDispose] method which can be used to schedule a callback to be executed once whenever the effect leaves
* the composition
*
* The `onActive` effect is essentially a convenience effect for `onCommit(true) { ... }`.
*
* @param callback The lambda to execute when the composition commits for the first time and becomes active.
*
* @see [onCommit]
* @see [onPreCommit]
* @see [onDispose]
*/
@CheckResult(suggest = "+")
fun onActive(callback: CommitScope.() -> Unit) = effectOf<Unit> {
context.remember { PostCommitScopeImpl(callback) }
}
/**
* An effect used to schedule work to be done when the effect leaves the composition.
*
* The `onDispose` effect is essentially a convenience effect for `onActive { onDispose { ... } }`.
*
* @param callback The lambda to be executed when the effect leaves the composition.
*
* @see [onCommit]
* @see [onPreCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
fun onDispose(callback: () -> Unit) = onActive { onDispose(callback) }
/**
* The onCommit effect is a lifecycle effect that will execute [callback] every time the composition commits. It is useful for
* executing code in lock-step with composition that has side-effects. The [callback] will get executed with a receiver scope that has an
* [onDispose][CommitScope.onDispose] method which can be used to schedule a callback to schedule code that cleans up the code in the
* callback.
*
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onPreCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
fun onCommit(callback: CommitScope.() -> Unit) = effectOf<Unit> {
context.changed(PostCommitScopeImpl(callback))
}
/**
* The onCommit effect is a lifecycle effect that will execute [callback] every time the inputs to the effect have changed. It is useful for
* executing code in lock-step with composition that has side-effects that are based on the inputs. The [callback] will get executed with a
* receiver scope that has an [onDispose][CommitScope.onDispose] method which can be used to schedule a callback to schedule code that
* cleans up the code in the callback.
*
* @param v1 The input which will be compared across compositions to determine if [callback] will get executed.
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onPreCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
/* inline */ fun </* reified */ V1> onCommit(
v1: V1,
/* noinline */
callback: CommitScope.() -> Unit
) = effectOf<Unit> {
context.remember(v1) { PostCommitScopeImpl(callback) }
}
/**
* The onCommit effect is a lifecycle effect that will execute [callback] every time the inputs to the effect have changed. It is useful for
* executing code in lock-step with composition that has side-effects that are based on the inputs. The [callback] will get executed with a
* receiver scope that has an [onDispose][CommitScope.onDispose] method which can be used to schedule a callback to schedule code that
* cleans up the code in the callback.
*
* @param v1 An input value which will be compared across compositions to determine if [callback] will get executed.
* @param v2 An input value which will be compared across compositions to determine if [callback] will get executed.
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onPreCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
/* inline */ fun </* reified */ V1, /* reified */ V2> onCommit(
v1: V1,
v2: V2,
/* noinline */
callback: CommitScope.() -> Unit
) = effectOf<Unit> {
context.remember(v1, v2) { PostCommitScopeImpl(callback) }
}
/**
* The onCommit effect is a lifecycle effect that will execute [callback] every time the inputs to the effect have changed. It is useful for
* executing code in lock-step with composition that has side-effects that are based on the inputs. The [callback] will get executed with a
* receiver scope that has an [onDispose][CommitScope.onDispose] method which can be used to schedule a callback to schedule code that
* cleans up the code in the callback.
*
* @param inputs A set of inputs which will be compared across compositions to determine if [callback] will get executed.
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onPreCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
fun onCommit(vararg inputs: Any?, callback: CommitScope.() -> Unit) =
effectOf<Unit> {
context.remember(*inputs) { PostCommitScopeImpl(callback) }
}
/**
* The onPreCommit effect is a lifecycle effect that will execute [callback] every time the composition commits,
* but before those changes have been reflected on the screen. It is useful for executing code that needs to
* update in response to a composition and it is critical that the previous results are never seen by the user.
* If it is not critical, [onCommit] is recommended instead. The [callback] will get executed with a receiver scope that has an
* [onDispose][CommitScope.onDispose] method which can be used to schedule a callback to schedule code that cleans up the code in the
* callback.
*
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onPreCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
fun onPreCommit(callback: CommitScope.() -> Unit) =
effectOf<Unit> {
context.changed(PreCommitScopeImpl(callback))
}
/**
* The onPreCommit effect is a lifecycle effect that will execute [callback] every time the inputs to the
* effect have changed, but before those changes have been reflected on the screen. It is useful for executing
* code that needs to update in response to a composition and it is critical that the previous results are
* never seen by the user. If it is not critical, [onCommit] is recommended instead. The [callback] will get
* executed with a receiver scope that has an [onDispose][CommitScope.onDispose] method which can be used to
* schedule a callback to schedule code that cleans up the code in the callback.
*
* @param v1 The input which will be compared across compositions to determine if [callback] will get executed.
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
/* inline */ fun </* reified */ V1> onPreCommit(
v1: V1,
/* noinline */
callback: CommitScope.() -> Unit
) = effectOf<Unit> {
context.remember(v1) { PreCommitScopeImpl(callback) }
}
/**
* The onPreCommit effect is a lifecycle effect that will execute [callback] every time the inputs to the
* effect have changed, but before those changes have been reflected on the screen. It is useful for executing
* code that needs to update in response to a composition and it is critical that the previous results are
* never seen by the user. If it is not critical, [onCommit] is recommended instead. The [callback] will get
* executed with a receiver scope that has an [onDispose][CommitScope.onDispose] method which can be used to
* schedule a callback to schedule code that cleans up the code in the callback.
*
* @param v1 An input value which will be compared across compositions to determine if [callback] will get executed.
* @param v2 An input value which will be compared across compositions to determine if [callback] will get executed.
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
/* inline */ fun </* reified */ V1, /* reified */ V2> onPreCommit(
v1: V1,
v2: V2,
/* noinline */
callback: CommitScope.() -> Unit
) = effectOf<Unit> {
context.remember(v1, v2) { PreCommitScopeImpl(callback) }
}
/**
* The onPreCommit effect is a lifecycle effect that will execute [callback] every time the inputs to the
* effect have changed, but before those changes have been reflected on the screen. It is useful for executing
* code that needs to update in response to a composition and it is critical that the previous results are
* never seen by the user. If it is not critical, [onCommit] is recommended instead. The [callback] will get
* executed with a receiver scope that has an [onDispose][CommitScope.onDispose] method which can be used to
* schedule a callback to schedule code that cleans up the code in the callback.
*
* @param inputs A set of inputs which will be compared across compositions to determine if [callback] will get executed.
* @param callback The lambda to be executed when the effect is committed to the composition.
*
* @see [onDispose]
* @see [onCommit]
* @see [onActive]
*/
@CheckResult(suggest = "+")
fun onPreCommit(vararg inputs: Any?, callback: CommitScope.() -> Unit) =
effectOf<Unit> {
context.remember(*inputs) { PreCommitScopeImpl(callback) }
}
/**
* An effect to introduce a state value of type [T] into a composition.
*
* This is useful when you have a value that you would like to locally mutate and use in the context of a composition. Since
* the returned [State] instance implements [Model], changes to the [State.value] property will be automatically tracked in
* composition and schedule a recompose.
*
* The [State] class can be used several different ways. For example, the most basic way is to store the returned state
* value into a local immutable variable, and then set the [State.value] property on it.
*
* Example:
*
* @Composable
* fun Example() {
* val count = +state { 0 }
*
* TextView(text="You clicked ${count.value} times")
* Button(
* text="Click me",
* onClick={ count.value += 1 }
* )
* }
*
* Additionally, you can destructure the [State] object into a value and a "setter" function.
*
* Example:
*
* @Composable
* fun Example() {
* val (count, setCount) = +state { 0 }
*
* TextView(text="You clicked ${count} times")
* Button(
* text="Click me",
* onClick={ setCount(count + 1) }
* )
* }
*
* Finally, the [State] instance can be used as a variable delegate to a local mutable variable.
*
* Example:
*
* @Composable
* fun Example() {
* var count by +state { 0 }
*
* TextView(text="You clicked $count times")
* Button(
* text="Click me",
* onClick={ count += 1 }
* )
* }
*
*
* @param init A factory function to create the initial value of this state
* @return An [Model] instance of [State] that wraps the value.
*
* @see [stateFor]
* @see [model]
* @see [modelFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <T> state(/* crossinline */ init: () -> T) =
memo { State(init()) }
/**
* An effect to introduce a state value of type [T] into a composition that will last as long as the input [v1] does not change.
*
* This is useful when you have a value that you would like to locally mutate and use in the context of a composition, and its
* value is scoped to another value and you want it to be reset every time the other value changes.
*
* The returned [State] instance implements [Model] so that changes to the [State.value] property will be automatically tracked in
* composition and schedule a recompose.
*
* @param v1 An input value that, when changed, will cause the state to reset and [init] to be rerun
* @param init A factory function to create the initial value of this state
* @return An [Model] instance of [State] that wraps the value.
*
* @see [state]
* @see [model]
* @see [modelFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <T, /* reified */ V1> stateFor(v1: V1, /* crossinline */ init: () -> T) =
memo(v1) { State(init()) }
/**
* An effect to introduce a state value of type [T] into a composition that will last as long as the inputs [v1] and [v2] do not change.
*
* This is useful when you have a value that you would like to locally mutate and use in the context of a composition, and its
* value is scoped to another value and you want it to be reset every time the other value changes.
*
* The returned [State] instance implements [Model] so that changes to the [State.value] property will be automatically tracked in
* composition and schedule a recompose.
*
* @param v1 An input value that, when changed, will cause the state to reset and [init] to be rerun
* @param v2 An input value that, when changed, will cause the state to reset and [init] to be rerun
* @param init A factory function to create the initial value of this state
* @return An [Model] instance of [State] that wraps the value.
*
* @see [state]
* @see [model]
* @see [modelFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <T, /* reified */ V1, /* reified */ V2> stateFor(
v1: V1,
v2: V2,
/* crossinline */
init: () -> T
) = memo(v1, v2) { State(init()) }
/**
* An effect to introduce a state value of type [T] into a composition that will last as long as the inputs [inputs] do not change.
*
* This is useful when you have a value that you would like to locally mutate and use in the context of a composition, and its
* value is scoped to another value and you want it to be reset every time the other value changes.
*
* The returned [State] instance implements [Model] so that changes to the [State.value] property will be automatically tracked in
* composition and schedule a recompose.
*
* @param inputs A set of inputs such that, when any of them have changed, will cause the state to reset and [init] to be rerun
* @param init A factory function to create the initial value of this state
* @return An [Model] instance of [State] that wraps the value.
*
* @see [state]
* @see [model]
* @see [modelFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <T> stateFor(vararg inputs: Any?, /* crossinline */ init: () -> T) =
memo(*inputs) { State(init()) }
/**
* The State class is an @Model class meant to wrap around a single value. It is used in the
* `+state` and `+stateFor` effects.
*
* @property value the wrapped value
*
* @see [Model]
* @see [state]
* @see [stateFor]
*/
@Model
class State<T> @PublishedApi internal constructor(value: T) : Framed {
/* NOTE(lmr): When this module is compiled with IR, we will need to remove the below Framed implementation */
@Suppress("UNCHECKED_CAST")
var value: T
get() = next.readable(this).value
set(value) {
next.writable(this).value = value
}
private var next: StateRecord<T> =
StateRecord(value)
init {
_created(this)
}
// NOTE(lmr): ideally we can compile `State` with our own compiler so that this is not visible
@Hide
override val firstFrameRecord: Record
get() = next
// NOTE(lmr): ideally we can compile `State` with our own compiler so that this is not visible
@Hide
override fun prependFrameRecord(value: Record) {
value.next = next
@Suppress("UNCHECKED_CAST")
next = value as StateRecord<T>
}
private class StateRecord<T>(myValue: T) : AbstractRecord() {
override fun assign(value: Record) {
@Suppress("UNCHECKED_CAST")
this.value = (value as StateRecord<T>).value
}
override fun create(): Record =
StateRecord(value)
var value: T = myValue
}
/**
* The componentN() operators allow state objects to be used with the property destructuring syntax
*
* var (foo, setFoo) = +state { 0 }
* setFoo(123) // set
* foo == 123 // get
*/
operator fun component1(): T = value
operator fun component2(): (T) -> Unit = { value = it }
/**
* The getValue/setValue operators allow State to be used as a local variable with a delegate:
*
* var foo by +state { 0 }
* foo += 123 // uses setValue(...)
* foo == 123 // uses getValue(...)
*/
operator fun getValue(thisObj: Any?, property: KProperty<*>): T = value
operator fun setValue(thisObj: Any?, property: KProperty<*>, next: T) {
value = next
}
}
/**
* The model effect is an alias to the `memo` effect, but the semantics behind how it is used are different from
* memoization, so we provide new named functions for the different use cases.
*
* In the case of memoization, the "inputs" of the calculation should be provided for correctness, implying if the
* inputs have not changed, the cached result and executing the calculation again would produce semantically identical
* results.
*
* In the case of "model", we are actually *intentionally* under-specifying the inputs of the calculation to cause an
* object to be cached across compositions. In this case, the calculation function is *not* a pure function of the inputs,
* and instead we are relying on the "incorrect" memoization to produce state that survives across compositions.
*
* Because these usages are so contradictory to one another, we provide a `model` alias for `memo` that is expected to
* be used in these cases instead of `memo`.
*/
/**
* An effect to introduce a state value of type [T] into a composition. The [init] lambda will be called only once to create the
* initial value, and then that instance will be used for the lifetime of the composition.
*
* This is useful when you have a [Model] class that you would like to instantiate and use in the context of a composition. Since
* the returned.
*
* @param init A lambda that creates the [Model] instance
* @return The Effect which resolves to the result of [init] cached across compositions
*
* @see [modelFor]
* @see [state]
* @see [stateFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <T> model(/* crossinline */ init: () -> T) = memo { init() }
/**
* An effect to introduce a state value of type [T] into a composition. The [init] lambda will be called only once to create the
* initial value, and then that instance will be used as long as the input [v1] has not changed.
*
* This is useful when you have a [Model] class that you would like to instantiate and use in the context of a composition. Since
* the returned.
*
* @param v1 An input value that, when changed, will cause the state to reset and [init] to be rerun
* @param init A lambda that creates the [Model] instance
* @return The Effect which resolves to the result of [init] cached across compositions
*
* @see [model]
* @see [state]
* @see [stateFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <T, /* reified */ V1> modelFor(v1: V1, /* crossinline */ init: () -> T) =
memo(v1) { init() }
/**
* An effect to introduce a state value of type [T] into a composition. The [init] lambda will be called only once to create the
* initial value, and then that instance will be used as long as the inputs [v1] and [v2] have not changed.
*
* This is useful when you have a [Model] class that you would like to instantiate and use in the context of a composition. Since
* the returned.
*
* @param v1 An input value that, when changed, will cause the state to reset and [init] to be rerun
* @param v2 An input value that, when changed, will cause the state to reset and [init] to be rerun
* @param init A lambda that creates the [Model] instance
* @return The Effect which resolves to the result of [init] cached across compositions
*
* @see [model]
* @see [state]
* @see [stateFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <
T,
/* reified */ V1,
/* reified */ V2
> modelFor(v1: V1, v2: V2, /* crossinline */ init: () -> T) =
memo(v1, v2) { init() }
/**
* An effect to introduce a state value of type [T] into a composition. The [init] lambda will be called only once to create the
* initial value, and then that instance will be used as long as the the values of [inputs] have not changed.
*
* This is useful when you have a [Model] class that you would like to instantiate and use in the context of a composition. Since
* the returned.
*
* @param inputs A set of inputs such that, when any of them have changed, the state will reset and [init] will be rerun
* @param init A lambda that creates the [Model] instance
* @return The Effect which resolves to the result of [init] cached across compositions
*
* @see [model]
* @see [state]
* @see [stateFor]
*/
@CheckResult(suggest = "+")
/* inline */ fun <T> modelFor(vararg inputs: Any?, /* crossinline */ init: () -> T) =
memo(*inputs) { init() }
/**
* An Effect used to get the value of an ambient at a specific position during composition.
*
* @param key The Ambient that you want to consume the value of
* @return An Effect that resolves to the current value of the Ambient
*
* @see [Ambient]
*/
@CheckResult(suggest = "+")
fun <T> ambient(key: Ambient<T>) = effectOf<T> {
context.consume(key)
}
/**
* An Effect to get the nearest invalidation lambda to the current point of composition. This can be used to
* trigger an invalidation on the composition locally to cause a recompose.
*/
val invalidate = effectOf<() -> Unit> {
val scope = context.currentInvalidate ?: error("no recompose scope found")
return@effectOf { scope.invalidate?.let { it(false) } }
}
/**
* An Effect to construct a CompositionReference at the current point of composition. This can be used
* to run a separate composition in the context of the current one, preserving ambients and propagating
* invalidations.
*/
fun compositionReference() = effectOf<CompositionReference> {
context.buildReference()
}
/**
* IMPORTANT:
* This global operator is TEMPORARY, and should be removed whenever an answer for contextual composers is reached. At that time, the
* unaryPlus operator on the composer itself is the one that should be used.
*
* Resolves the effect and returns the result.
*/
@Suppress("NOTHING_TO_INLINE")
/* inline */ operator fun <T> Effect<T>.unaryPlus(): T = resolve(currentComposerNonNull)