1 /*
<lambda>null2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.runtime.snapshots.tooling
18
19 import androidx.collection.ScatterSet
20 import androidx.compose.runtime.ExperimentalComposeRuntimeApi
21 import androidx.compose.runtime.collection.wrapIntoSet
22 import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
23 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
24 import androidx.compose.runtime.snapshots.ObserverHandle
25 import androidx.compose.runtime.snapshots.Snapshot
26 import androidx.compose.runtime.snapshots.StateObject
27 import androidx.compose.runtime.snapshots.fastForEach
28 import androidx.compose.runtime.snapshots.sync
29
30 /**
31 * An observer for the snapshot system that notifies an observer when a snapshot is created,
32 * applied, and/or disposed.
33 *
34 * All methods are called in the thread of the snapshot so all observers must be thread safe as they
35 * may be called from any thread.
36 *
37 * Calling any of the Snapshot API (including, reading or writing mutable state objects) is not
38 * supported and may produce inconsistent result or throw an exception.
39 */
40 @ExperimentalComposeRuntimeApi
41 @Suppress("CallbackName")
42 interface SnapshotObserver {
43 /**
44 * Called before a snapshot is created allowing reads and writes to the snapshot to be observed.
45 *
46 * This method is called in the same thread that creates the snapshot.
47 *
48 * @param parent the parent snapshot for the new snapshot if it is a nested snapshot or null
49 * otherwise.
50 * @param readonly whether the snapshot being created will be read-only.
51 * @return optional read and write observers that will be added to the snapshot created.
52 */
53 fun onPreCreate(parent: Snapshot?, readonly: Boolean): SnapshotInstanceObservers? = null
54
55 /**
56 * Called after snapshot is created.
57 *
58 * This is called prior to the instance being returned by [Snapshot.takeSnapshot] or
59 * [Snapshot.takeMutableSnapshot].
60 *
61 * This method is called in the same thread that creates the snapshot.
62 *
63 * @param snapshot the snapshot that was created.
64 * @param parent the parent snapshot for the new snapshot if it is a nested snapshot or null if
65 * it is a root snapshot.
66 * @param observers the read and write observers that were installed by the value returned by
67 * [onCreated]. This allows correlating which snapshot observers returned by [onPreCreate] to
68 * the [snapshot] that was created.
69 */
70 fun onCreated(snapshot: Snapshot, parent: Snapshot?, observers: SnapshotInstanceObservers?) {}
71
72 /**
73 * Called while a snapshot is being disposed.
74 *
75 * This method is called in the same thread that disposes the snapshot.
76 *
77 * @param snapshot information about the snapshot that was created.
78 */
79 fun onPreDispose(snapshot: Snapshot) {}
80
81 /**
82 * Called after a snapshot is applied.
83 *
84 * For nested snapshots, the changes will only be visible to the parent snapshot, not globally.
85 * Snapshots do not have a parent will have changes that are visible globally and such
86 * notification are equivalent the notification sent to [Snapshot.registerApplyObserver] and
87 * will include all objects modified by any nested snapshots that have been applied to the
88 * parent snapshot.
89 *
90 * This method is called in the same thread that applies the snapshot.
91 *
92 * @param snapshot the snapshot that was applied.
93 * @param changed the set of objects that were modified during the snapshot.
94 */
95 fun onApplied(snapshot: Snapshot, changed: Set<Any>) {}
96 }
97
98 /**
99 * The return result of [SnapshotObserver.onPreCreate] allowing the reads and writes performed in
100 * the newly created snapshot to be observed
101 */
102 @ExperimentalComposeRuntimeApi
103 class SnapshotInstanceObservers(
104 /**
105 * Called whenever a state is read in the snapshot. This is called before the read observer
106 * passed to [Snapshot.takeSnapshot] or [Snapshot.takeMutableSnapshot].
107 *
108 * This method is called in the same thread that reads snapshot state.
109 */
110 val readObserver: ((Any) -> Unit)? = null,
111
112 /**
113 * Called just before a state object is written to the first time in the snapshot or a nested
114 * mutable snapshot. This might be called several times for the same object if nested mutable
115 * snapshots are created as the unmodified value may be needed by the nested snapshot so a new
116 * copy is created. This is not called for each write, only when the write results in the object
117 * be recorded as being modified requiring a copy to be made before the write completes. This is
118 * called before the write has been applied to the instance.
119 *
120 * This is called before the write observer passed to [Snapshot.takeMutableSnapshot].
121 *
122 * This method is called in the same thread that writes to the snapshot state.
123 */
124 val writeObserver: ((Any) -> Unit)? = null,
125 )
126
127 /**
128 * This is a tooling API and is not intended to be used in a production application as it will
129 * introduce global overhead to creating, applying and disposing all snapshots and, potentially, to
130 * reading and writing all state objects.
131 *
132 * Observe when snapshots are created, applied, and/or disposed. The observer can also install read
133 * and write observers on the snapshot being created.
134 *
135 * This method is thread-safe and calling [ObserverHandle.dispose] on the [ObserverHandle] returned
136 * is also thread-safe.
137 *
138 * @param snapshotObserver the snapshot observer to install.
139 * @return [ObserverHandle] an instance to unregister the [snapshotObserver].
140 */
141 @ExperimentalComposeRuntimeApi
Snapshotnull142 fun Snapshot.Companion.observeSnapshots(snapshotObserver: SnapshotObserver): ObserverHandle {
143 sync { observers = (observers ?: persistentListOf()).add(snapshotObserver) }
144 return ObserverHandle {
145 sync {
146 val newObservers = observers?.remove(snapshotObserver)
147 observers = newObservers?.takeIf { it.isNotEmpty() }
148 }
149 }
150 }
151
152 @ExperimentalComposeRuntimeApi private var observers: PersistentList<SnapshotObserver>? = null
153
154 @ExperimentalComposeRuntimeApi
creatingSnapshotnull155 internal inline fun <R : Snapshot> creatingSnapshot(
156 parent: Snapshot?,
157 noinline readObserver: ((Any) -> Unit)?,
158 noinline writeObserver: ((Any) -> Unit)?,
159 readonly: Boolean,
160 crossinline block: (readObserver: ((Any) -> Unit)?, writeObserver: ((Any) -> Unit)?) -> R
161 ): R {
162 var observerMap: Map<SnapshotObserver, SnapshotInstanceObservers>? = null
163 val observers = observers
164 var actualReadObserver = readObserver
165 var actualWriteObserver = writeObserver
166 if (observers != null) {
167 val result = observers.mergeObservers(parent, readonly, readObserver, writeObserver)
168 val mappedObservers = result.first
169 actualReadObserver = mappedObservers.readObserver
170 actualWriteObserver = mappedObservers.writeObserver
171 observerMap = result.second
172 }
173 val result = block(actualReadObserver, actualWriteObserver)
174 observers?.dispatchCreatedObservers(parent, result, observerMap)
175 return result
176 }
177
178 @ExperimentalComposeRuntimeApi
mergeObserversnull179 internal fun PersistentList<SnapshotObserver>.mergeObservers(
180 parent: Snapshot?,
181 readonly: Boolean,
182 readObserver: ((Any) -> Unit)?,
183 writeObserver: ((Any) -> Unit)?,
184 ): Pair<SnapshotInstanceObservers, Map<SnapshotObserver, SnapshotInstanceObservers>?> {
185 var currentReadObserver = readObserver
186 var currentWriteObserver = writeObserver
187 var observerMap: MutableMap<SnapshotObserver, SnapshotInstanceObservers>? = null
188 fastForEach { observer ->
189 val instance = observer.onPreCreate(parent, readonly)
190 if (instance != null) {
191 currentReadObserver = mergeObservers(instance.readObserver, currentReadObserver)
192 currentWriteObserver = mergeObservers(instance.writeObserver, currentWriteObserver)
193 (observerMap
194 ?: run {
195 val newMap = mutableMapOf<SnapshotObserver, SnapshotInstanceObservers>()
196 observerMap = newMap
197 newMap
198 })[observer] = instance
199 }
200 }
201 return SnapshotInstanceObservers(currentReadObserver, currentWriteObserver) to observerMap
202 }
203
mergeObserversnull204 private fun mergeObservers(a: ((Any) -> Unit)?, b: ((Any) -> Unit)?): ((Any) -> Unit)? {
205 return if (a != null && b != null) {
206 {
207 a(it)
208 b(it)
209 }
210 } else a ?: b
211 }
212
213 @ExperimentalComposeRuntimeApi
dispatchCreatedObserversnull214 internal fun PersistentList<SnapshotObserver>.dispatchCreatedObservers(
215 parent: Snapshot?,
216 result: Snapshot,
217 observerMap: Map<SnapshotObserver, SnapshotInstanceObservers>?
218 ) {
219 fastForEach { observer ->
220 val instance = observerMap?.get(observer)
221 observer.onCreated(result, parent, instance)
222 }
223 }
224
225 @OptIn(ExperimentalComposeRuntimeApi::class)
dispatchObserverOnPreDisposenull226 internal fun dispatchObserverOnPreDispose(snapshot: Snapshot) {
227 observers?.fastForEach { observer -> observer.onPreDispose(snapshot) }
228 }
229
230 @OptIn(ExperimentalComposeRuntimeApi::class)
dispatchObserverOnAppliednull231 internal fun dispatchObserverOnApplied(snapshot: Snapshot, changes: ScatterSet<StateObject>?) {
232 val observers = observers
233 if (!observers.isNullOrEmpty()) {
234 val wrappedChanges = changes?.wrapIntoSet() ?: emptySet()
235 observers.fastForEach { observer -> observer.onApplied(snapshot, wrappedChanges) }
236 }
237 }
238