| /* |
| * 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. |
| */ |
| |
| @file:OptIn( |
| ExperimentalComposeApi::class, |
| InternalComposeApi::class |
| ) |
| package androidx.compose.runtime |
| |
| import androidx.compose.runtime.dispatch.DefaultMonotonicFrameClock |
| import androidx.compose.runtime.dispatch.MonotonicFrameClock |
| import androidx.compose.runtime.snapshots.Snapshot |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.CoroutineStart |
| import kotlinx.coroutines.ExperimentalCoroutinesApi |
| import kotlinx.coroutines.Job |
| import kotlinx.coroutines.NonCancellable |
| import kotlinx.coroutines.coroutineScope |
| import kotlinx.coroutines.launch |
| import kotlinx.coroutines.suspendCancellableCoroutine |
| import kotlin.coroutines.Continuation |
| import kotlin.coroutines.CoroutineContext |
| import kotlin.coroutines.coroutineContext |
| import kotlin.coroutines.resume |
| |
| /** |
| * Runs [block] with a new, active [Recomposer] applying changes in the calling [CoroutineContext]. |
| */ |
| suspend fun withRunningRecomposer( |
| block: suspend CoroutineScope.(recomposer: Recomposer) -> Unit |
| ): Unit = coroutineScope { |
| val recomposer = Recomposer() |
| val recompositionJob = launch { recomposer.runRecomposeAndApplyChanges() } |
| block(recomposer) |
| recompositionJob.cancel() |
| } |
| |
| /** |
| * The scheduler for performing recomposition and applying updates to one or more [Composition]s. |
| * [frameClock] is used to align changes with display frames. |
| */ |
| class Recomposer(var embeddingContext: EmbeddingContext = EmbeddingContext()) { |
| |
| /** |
| * This collection is its own lock, shared with [invalidComposersAwaiter] |
| */ |
| private val invalidComposers = mutableSetOf<Composer<*>>() |
| |
| /** |
| * The continuation to resume when there are invalid composers to process. |
| */ |
| private var invalidComposersAwaiter: Continuation<Unit>? = null |
| |
| /** |
| * Track if any outstanding invalidated composers are awaiting recomposition. |
| * This latch is closed any time we resume invalidComposersAwaiter and opened |
| * by [recomposeAndApplyChanges] when it suspends when it has no further work to do. |
| */ |
| private val idlingLatch = Latch() |
| |
| /** |
| * Enforces that only one caller of [runRecomposeAndApplyChanges] is active at a time |
| * while carrying its calling scope. Used to [launchEffect] on the apply dispatcher. |
| */ |
| // TODO(adamp) convert to atomicfu once ready |
| private val applyingScope = AtomicReference<CoroutineScope?>(null) |
| |
| private val broadcastFrameClock = BroadcastFrameClock { |
| synchronized(invalidComposers) { |
| invalidComposersAwaiter?.let { |
| invalidComposersAwaiter = null |
| idlingLatch.closeLatch() |
| it.resume(Unit) |
| } |
| } |
| } |
| val frameClock: MonotonicFrameClock get() = broadcastFrameClock |
| |
| /** |
| * Await the invalidation of any associated [Composer]s, recompose them, and apply their |
| * changes to their associated [Composition]s if recomposition is successful. |
| * |
| * While [runRecomposeAndApplyChanges] is running, [awaitIdle] will suspend until there are no |
| * more invalid composers awaiting recomposition. |
| * |
| * This method never returns. Cancel the calling [CoroutineScope] to stop. |
| */ |
| suspend fun runRecomposeAndApplyChanges(): Nothing { |
| coroutineScope { |
| recomposeAndApplyChanges(this, Long.MAX_VALUE) |
| } |
| error("this function never returns") |
| } |
| |
| /** |
| * Await the invalidation of any associated [Composer]s, recompose them, and apply their |
| * changes to their associated [Composition]s if recomposition is successful. Any launched |
| * effects of composition will be launched into the receiver [CoroutineScope]. |
| * |
| * While [runRecomposeAndApplyChanges] is running, [awaitIdle] will suspend until there are no |
| * more invalid composers awaiting recomposition. |
| * |
| * This method returns after recomposing [frameCount] times. |
| */ |
| suspend fun recomposeAndApplyChanges( |
| applyCoroutineScope: CoroutineScope, |
| frameCount: Long |
| ) { |
| var framesRemaining = frameCount |
| val toRecompose = mutableListOf<Composer<*>>() |
| |
| if (!applyingScope.compareAndSet(null, applyCoroutineScope)) { |
| error("already recomposing and applying changes") |
| } |
| |
| // Cache this so we don't go looking for it each time through the loop. |
| val frameClock = coroutineContext[MonotonicFrameClock] ?: DefaultMonotonicFrameClock |
| |
| try { |
| idlingLatch.closeLatch() |
| while (frameCount == Long.MAX_VALUE || framesRemaining-- > 0L) { |
| // Don't hold the monitor lock across suspension. |
| val hasInvalidComposers = synchronized(invalidComposers) { |
| invalidComposers.isNotEmpty() |
| } |
| if (!hasInvalidComposers && !broadcastFrameClock.hasAwaiters) { |
| // Suspend until we have something to do |
| suspendCancellableCoroutine<Unit> { co -> |
| synchronized(invalidComposers) { |
| if (invalidComposers.isEmpty()) { |
| invalidComposersAwaiter = co |
| idlingLatch.openLatch() |
| } else { |
| // We raced and lost, someone invalidated between our check |
| // and suspension. Resume immediately. |
| co.resume(Unit) |
| return@suspendCancellableCoroutine |
| } |
| } |
| co.invokeOnCancellation { |
| synchronized(invalidComposers) { |
| if (invalidComposersAwaiter === co) { |
| invalidComposersAwaiter = null |
| } |
| } |
| } |
| } |
| } |
| |
| // Align work with the next frame to coalesce changes. |
| // Note: it is possible to resume from the above with no recompositions pending, |
| // instead someone might be awaiting our frame clock dispatch below. |
| frameClock.withFrameNanos { frameTime -> |
| trace("recomposeFrame") { |
| // Propagate the frame time to anyone who is awaiting from the |
| // recomposer clock. |
| broadcastFrameClock.sendFrame(frameTime) |
| |
| // Ensure any global changes are observed |
| Snapshot.sendApplyNotifications() |
| |
| // ...and make sure we know about any pending invalidations the commit |
| // may have caused before recomposing - Handler messages can't run between |
| // input processing and the frame clock pulse! |
| FrameManager.synchronize() |
| |
| // ...and pick up any stragglers as a result of the above snapshot sync |
| synchronized(invalidComposers) { |
| toRecompose.addAll(invalidComposers) |
| invalidComposers.clear() |
| } |
| |
| if (toRecompose.isNotEmpty()) { |
| for (i in 0 until toRecompose.size) { |
| performRecompose(toRecompose[i]) |
| } |
| toRecompose.clear() |
| } |
| } |
| } |
| } |
| } finally { |
| applyingScope.set(null) |
| // If we're not still running frames, we're effectively idle. |
| idlingLatch.openLatch() |
| } |
| } |
| |
| private class CompositionCoroutineScopeImpl( |
| override val coroutineContext: CoroutineContext, |
| frameClock: MonotonicFrameClock |
| ) : CompositionCoroutineScope(), MonotonicFrameClock by frameClock |
| |
| /** |
| * Implementation note: we launch effects undispatched so they can begin immediately during |
| * the apply step. This function is only called internally by [launchInComposition] |
| * implementations during [CompositionLifecycleObserver] callbacks dispatched on the |
| * applying scope, so we consider this safe. |
| */ |
| @OptIn(ExperimentalCoroutinesApi::class) |
| internal fun launchEffect( |
| block: suspend CompositionCoroutineScope.() -> Unit |
| ): Job = applyingScope.get()?.launch(start = CoroutineStart.UNDISPATCHED) { |
| CompositionCoroutineScopeImpl(coroutineContext, frameClock).block() |
| } ?: error("apply scope missing; runRecomposeAndApplyChanges must be running") |
| |
| // TODO this is temporary until more of this logic moves to Composition |
| internal val applyingCoroutineContext: CoroutineContext? |
| get() = applyingScope.get()?.coroutineContext |
| |
| @Suppress("PLUGIN_WARNING", "PLUGIN_ERROR", "ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE") |
| internal fun composeInitial( |
| composable: @Composable () -> Unit, |
| composer: Composer<*> |
| ) { |
| val composerWasComposing = composer.isComposing |
| val prevComposer = currentComposerInternal |
| try { |
| try { |
| composer.isComposing = true |
| currentComposerInternal = composer |
| FrameManager.composing { |
| trace("Compose:recompose") { |
| var complete = false |
| try { |
| composer.startRoot() |
| composer.startGroup(invocationKey, invocation) |
| invokeComposable(composer, composable) |
| composer.endGroup() |
| composer.endRoot() |
| complete = true |
| } finally { |
| if (!complete) composer.abortRoot() |
| } |
| } |
| } |
| } finally { |
| composer.isComposing = composerWasComposing |
| } |
| // TODO(b/143755743) |
| if (!composerWasComposing) { |
| Snapshot.notifyObjectsInitialized() |
| } |
| composer.applyChanges() |
| |
| if (!composerWasComposing) { |
| // Ensure that any state objects created during applyChanges are seen as changed |
| // if modified after this call. |
| Snapshot.notifyObjectsInitialized() |
| } |
| } finally { |
| currentComposerInternal = prevComposer |
| } |
| } |
| |
| private fun performRecompose(composer: Composer<*>): Boolean { |
| if (composer.isComposing) return false |
| val prevComposer = currentComposerInternal |
| val hadChanges: Boolean |
| try { |
| currentComposerInternal = composer |
| composer.isComposing = true |
| hadChanges = FrameManager.composing { |
| composer.recompose() |
| } |
| composer.applyChanges() |
| } finally { |
| composer.isComposing = false |
| currentComposerInternal = prevComposer |
| } |
| return hadChanges |
| } |
| |
| fun hasPendingChanges(): Boolean = |
| !idlingLatch.isOpen || synchronized(invalidComposers) { invalidComposers.isNotEmpty() } |
| |
| internal fun scheduleRecompose(composer: Composer<*>) { |
| synchronized(invalidComposers) { |
| invalidComposers.add(composer) |
| invalidComposersAwaiter?.let { |
| invalidComposersAwaiter = null |
| idlingLatch.closeLatch() |
| it.resume(Unit) |
| } |
| } |
| } |
| |
| /** |
| * Suspends until the currently pending recomposition frame is complete. |
| * Any recomposition for this recomposer triggered by actions before this call begins |
| * will be complete and applied (if recomposition was successful) when this call returns. |
| * |
| * If [runRecomposeAndApplyChanges] is not currently running the [Recomposer] is considered idle |
| * and this method will not suspend. |
| */ |
| suspend fun awaitIdle(): Unit = idlingLatch.await() |
| |
| companion object { |
| private val embeddingContext by lazy { EmbeddingContext() } |
| /** |
| * Retrieves [Recomposer] for the current thread. Needs to be the main thread. |
| */ |
| @TestOnly |
| fun current(): Recomposer { |
| return mainRecomposer ?: run { |
| val mainScope = CoroutineScope(NonCancellable + |
| embeddingContext.mainThreadCompositionContext()) |
| |
| Recomposer(embeddingContext).also { |
| mainRecomposer = it |
| @OptIn(ExperimentalCoroutinesApi::class) |
| mainScope.launch(start = CoroutineStart.UNDISPATCHED) { |
| it.runRecomposeAndApplyChanges() |
| } |
| } |
| } |
| } |
| |
| private var mainRecomposer: Recomposer? = null |
| } |
| } |