blob: 8b1b5e40451782ba7a1bd0f0585ea13fda05c4ee [file]
/*
* Copyright 2024 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.snapshots.tooling
import androidx.annotation.EmptySuper
import androidx.collection.ScatterSet
import androidx.compose.runtime.ExperimentalComposeRuntimeApi
import androidx.compose.runtime.collection.wrapIntoSet
import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
import androidx.compose.runtime.snapshots.ObserverHandle
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.StateObject
import androidx.compose.runtime.snapshots.fastForEach
import androidx.compose.runtime.snapshots.sync
/**
* An observer for the snapshot system that notifies an observer when a snapshot is created,
* applied, and/or disposed.
*
* All methods are called in the thread of the snapshot so all observers must be thread safe as they
* may be called from any thread.
*
* Calling any of the Snapshot API (including, reading or writing mutable state objects) is not
* supported and may produce inconsistent result or throw an exception.
*/
@ExperimentalComposeRuntimeApi
@Suppress("CallbackName")
public interface SnapshotObserver {
/**
* Called before a snapshot is created allowing reads and writes to the snapshot to be observed.
*
* This method is called in the same thread that creates the snapshot.
*
* @param parent the parent snapshot for the new snapshot if it is a nested snapshot or null
* otherwise.
* @param readonly whether the snapshot being created will be read-only.
* @return optional read and write observers that will be added to the snapshot created.
*/
public fun onPreCreate(parent: Snapshot?, readonly: Boolean): SnapshotInstanceObservers? = null
/**
* Called after snapshot is created.
*
* This is called prior to the instance being returned by [Snapshot.takeSnapshot] or
* [Snapshot.takeMutableSnapshot].
*
* This method is called in the same thread that creates the snapshot.
*
* @param snapshot the snapshot that was created.
* @param parent the parent snapshot for the new snapshot if it is a nested snapshot or null if
* it is a root snapshot.
* @param observers the read and write observers that were installed by the value returned by
* [onCreated]. This allows correlating which snapshot observers returned by [onPreCreate] to
* the [snapshot] that was created.
*/
@EmptySuper
public fun onCreated(
snapshot: Snapshot,
parent: Snapshot?,
observers: SnapshotInstanceObservers?,
) {}
/**
* Called while a snapshot is being disposed.
*
* This method is called in the same thread that disposes the snapshot.
*
* @param snapshot information about the snapshot that was created.
*/
@EmptySuper public fun onPreDispose(snapshot: Snapshot) {}
/**
* Called after a snapshot is applied.
*
* For nested snapshots, the changes will only be visible to the parent snapshot, not globally.
* Snapshots do not have a parent will have changes that are visible globally and such
* notification are equivalent the notification sent to [Snapshot.registerApplyObserver] and
* will include all objects modified by any nested snapshots that have been applied to the
* parent snapshot.
*
* This method is called in the same thread that applies the snapshot.
*
* @param snapshot the snapshot that was applied.
* @param changed the set of objects that were modified during the snapshot.
*/
@EmptySuper public fun onApplied(snapshot: Snapshot, changed: Set<Any>) {}
}
/**
* The return result of [SnapshotObserver.onPreCreate] allowing the reads and writes performed in
* the newly created snapshot to be observed
*/
@ExperimentalComposeRuntimeApi
public class SnapshotInstanceObservers(
/**
* Called whenever a state is read in the snapshot. This is called before the read observer
* passed to [Snapshot.takeSnapshot] or [Snapshot.takeMutableSnapshot].
*
* This method is called in the same thread that reads snapshot state.
*/
public val readObserver: ((Any) -> Unit)? = null,
/**
* Called just before a state object is written to the first time in the snapshot or a nested
* mutable snapshot. This might be called several times for the same object if nested mutable
* snapshots are created as the unmodified value may be needed by the nested snapshot so a new
* copy is created. This is not called for each write, only when the write results in the object
* be recorded as being modified requiring a copy to be made before the write completes. This is
* called before the write has been applied to the instance.
*
* This is called before the write observer passed to [Snapshot.takeMutableSnapshot].
*
* This method is called in the same thread that writes to the snapshot state.
*/
public val writeObserver: ((Any) -> Unit)? = null,
)
/**
* This is a tooling API and is not intended to be used in a production application as it will
* introduce global overhead to creating, applying and disposing all snapshots and, potentially, to
* reading and writing all state objects.
*
* Observe when snapshots are created, applied, and/or disposed. The observer can also install read
* and write observers on the snapshot being created.
*
* This method is thread-safe and calling [ObserverHandle.dispose] on the [ObserverHandle] returned
* is also thread-safe.
*
* @param snapshotObserver the snapshot observer to install.
* @return [ObserverHandle] an instance to unregister the [snapshotObserver].
*/
@ExperimentalComposeRuntimeApi
public fun Snapshot.Companion.observeSnapshots(snapshotObserver: SnapshotObserver): ObserverHandle {
sync { observers = (observers ?: persistentListOf()).add(snapshotObserver) }
return ObserverHandle {
sync {
val newObservers = observers?.remove(snapshotObserver)
observers = newObservers?.takeIf { it.isNotEmpty() }
}
}
}
@ExperimentalComposeRuntimeApi private var observers: PersistentList<SnapshotObserver>? = null
@ExperimentalComposeRuntimeApi
internal inline fun <R : Snapshot> creatingSnapshot(
parent: Snapshot?,
noinline readObserver: ((Any) -> Unit)?,
noinline writeObserver: ((Any) -> Unit)?,
readonly: Boolean,
crossinline block: (readObserver: ((Any) -> Unit)?, writeObserver: ((Any) -> Unit)?) -> R,
): R {
var observerMap: Map<SnapshotObserver, SnapshotInstanceObservers>? = null
val observers = observers
var actualReadObserver = readObserver
var actualWriteObserver = writeObserver
if (observers != null) {
val result = observers.mergeObservers(parent, readonly, readObserver, writeObserver)
val mappedObservers = result.first
actualReadObserver = mappedObservers.readObserver
actualWriteObserver = mappedObservers.writeObserver
observerMap = result.second
}
val result = block(actualReadObserver, actualWriteObserver)
observers?.dispatchCreatedObservers(parent, result, observerMap)
return result
}
@ExperimentalComposeRuntimeApi
internal fun PersistentList<SnapshotObserver>.mergeObservers(
parent: Snapshot?,
readonly: Boolean,
readObserver: ((Any) -> Unit)?,
writeObserver: ((Any) -> Unit)?,
): Pair<SnapshotInstanceObservers, Map<SnapshotObserver, SnapshotInstanceObservers>?> {
var currentReadObserver = readObserver
var currentWriteObserver = writeObserver
var observerMap: MutableMap<SnapshotObserver, SnapshotInstanceObservers>? = null
fastForEach { observer ->
val instance = observer.onPreCreate(parent, readonly)
if (instance != null) {
currentReadObserver = mergeObservers(instance.readObserver, currentReadObserver)
currentWriteObserver = mergeObservers(instance.writeObserver, currentWriteObserver)
(observerMap
?: run {
val newMap = mutableMapOf<SnapshotObserver, SnapshotInstanceObservers>()
observerMap = newMap
newMap
})[observer] = instance
}
}
return SnapshotInstanceObservers(currentReadObserver, currentWriteObserver) to observerMap
}
private fun mergeObservers(a: ((Any) -> Unit)?, b: ((Any) -> Unit)?): ((Any) -> Unit)? {
return if (a != null && b != null) {
{
a(it)
b(it)
}
} else a ?: b
}
@ExperimentalComposeRuntimeApi
internal fun PersistentList<SnapshotObserver>.dispatchCreatedObservers(
parent: Snapshot?,
result: Snapshot,
observerMap: Map<SnapshotObserver, SnapshotInstanceObservers>?,
) {
fastForEach { observer ->
val instance = observerMap?.get(observer)
observer.onCreated(result, parent, instance)
}
}
@OptIn(ExperimentalComposeRuntimeApi::class)
internal fun dispatchObserverOnPreDispose(snapshot: Snapshot) {
observers?.fastForEach { observer -> observer.onPreDispose(snapshot) }
}
@OptIn(ExperimentalComposeRuntimeApi::class)
internal fun dispatchObserverOnApplied(snapshot: Snapshot, changes: ScatterSet<StateObject>?) {
val observers = observers
if (!observers.isNullOrEmpty()) {
val wrappedChanges = changes?.wrapIntoSet() ?: emptySet()
observers.fastForEach { observer -> observer.onApplied(snapshot, wrappedChanges) }
}
}