[go: nahoru, domu]

blob: 06e94c9f314d8367ad18ab9cb760cb2948d82b3e [file] [log] [blame]
George Mount6623eb42019-12-04 14:38:44 -08001/*
2 * Copyright 2019 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
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -070017@file:Suppress("DEPRECATION", "UNUSED_PARAMETER")
18
George Mount6623eb42019-12-04 14:38:44 -080019package androidx.ui.core
20
George Mount6623eb42019-12-04 14:38:44 -080021import androidx.compose.ObserverMap
George Mount6623eb42019-12-04 14:38:44 -080022import androidx.compose.frames.FrameCommitObserver
23import androidx.compose.frames.FrameReadObserver
George Mount81ddeb12019-12-18 10:46:26 -080024import androidx.compose.frames.observeAllReads
George Mountde2dac72020-05-18 13:20:08 -070025import androidx.ui.util.fastForEach
George Mount6623eb42019-12-04 14:38:44 -080026
27/**
28 * Allows for easy model read observation. To begin observe a change, you must pass a
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -070029 * non-lambda `onCommit` listener to the [observeReads] method.
George Mount6623eb42019-12-04 14:38:44 -080030 *
Leland Richardsonfcf76b32020-05-13 16:58:59 -070031 * When a state change has been committed, the `onCommit` listener will be called
George Mount6623eb42019-12-04 14:38:44 -080032 * with the `targetObject` as the argument. There are no order guarantees for
George Mount81ddeb12019-12-18 10:46:26 -080033 * `onCommit` listener calls. Commit callbacks are made on the thread that model changes
34 * are committed, so the [commitExecutor] allows the developer to control the thread on which the
35 * `onCommit`calls are made. An example use would be to have the executor shift to the
36 * the UI thread for the `onCommit` callbacks to be made.
George Mount6623eb42019-12-04 14:38:44 -080037 *
George Mount81ddeb12019-12-18 10:46:26 -080038 * A different ModelObserver should be used with each thread that [observeReads] is called on.
39 *
40 * @param commitExecutor The executor on which all `onCommit` calls will be made.
George Mount6623eb42019-12-04 14:38:44 -080041 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -070042@Deprecated("Frames have been replaced by snapshots",
43 ReplaceWith(
44 "SnapshotStateObserver",
45 "androidx.compose.snapshots"
46 )
47)
George Mount81ddeb12019-12-18 10:46:26 -080048class ModelObserver(private val commitExecutor: (command: () -> Unit) -> Unit) {
Chuck Jazdzewskifb6db652019-11-25 08:30:51 -080049 private val commitObserver: FrameCommitObserver = { committed, _ ->
George Mount83c19ce2019-12-13 12:59:47 -080050 var hasValues = false
Ralston Da Silva231e5cc2020-03-05 00:33:03 -080051 // This array is in the same order as commitMaps
Jim Sprocha88c07a2020-06-25 13:00:03 -070052 @Suppress("DEPRECATION_ERROR")
Ralston Da Silva231e5cc2020-03-05 00:33:03 -080053 val targetsArray = synchronized(commitMaps) {
54 Array(commitMaps.size) { index ->
55 commitMaps[index].map.get(committed).apply {
56 if (isNotEmpty())
57 hasValues = true
George Mount6623eb42019-12-04 14:38:44 -080058 }
59 }
60 }
George Mount83c19ce2019-12-13 12:59:47 -080061 if (hasValues) {
George Mount81ddeb12019-12-18 10:46:26 -080062 commitExecutor {
George Mount83c19ce2019-12-13 12:59:47 -080063 callOnCommit(targetsArray)
George Mount6623eb42019-12-04 14:38:44 -080064 }
65 }
66 }
67
George Mount81ddeb12019-12-18 10:46:26 -080068 /**
69 * The [FrameReadObserver] used by this [ModelObserver] during [observeReads].
70 */
71 private val readObserver: FrameReadObserver = { model ->
72 if (!isPaused) {
Jim Sprocha88c07a2020-06-25 13:00:03 -070073 @Suppress("DEPRECATION_ERROR")
George Mount81ddeb12019-12-18 10:46:26 -080074 synchronized(commitMaps) {
75 currentMap!!.add(model, currentTarget!!)
76 }
George Mount6623eb42019-12-04 14:38:44 -080077 }
78 }
79
80 /**
George Mount81ddeb12019-12-18 10:46:26 -080081 * List of all [CommitMap]s. When [observeReads] is called, there will be
82 * a [CommitMap] associated with its `onCommit` callback in this list. The list
83 * only grows.
84 */
85 private val commitMaps = mutableListOf<CommitMap<*>>()
86
87 /**
88 * Method to call when unsubscribing from the commit observer.
89 */
90 private var commitUnsubscribe: (() -> Unit)? = null
91
92 /**
93 * `true` when an [observeReads] is in progress and [readObserver] is active and
94 * `false` when [readObserver] is no longer observing changes.
95 */
96 private var isObserving = false
97
98 /**
99 * `true` when [pauseObservingReads] is called and read observations should no
100 * longer be considered invalidations for the `onCommit` callback.
101 */
102 private var isPaused = false
103
104 /**
105 * The [ObserverMap] that should be added to when a model is read during [observeReads].
106 */
107 private var currentMap: ObserverMap<Any, Any>? = null
108
109 /**
110 * The target associated with the active [observeReads] call.
111 */
112 private var currentTarget: Any? = null
113
114 /**
George Mountfcdeacb2019-12-06 09:24:47 -0800115 * Test-only access to the internal commit listener. This is used for benchmarking
116 * the commit notification callback.
117 *
Louis Pullen-Freilich36724492020-03-20 13:30:02 +0000118 * @suppress
George Mountfcdeacb2019-12-06 09:24:47 -0800119 */
120 val frameCommitObserver: FrameCommitObserver
Nikolay Igotti9ea0c1f2020-06-30 12:27:21 +0300121 @InternalCoreApi
George Mountfcdeacb2019-12-06 09:24:47 -0800122 get() = commitObserver
123
124 /**
George Mount6623eb42019-12-04 14:38:44 -0800125 * Executes [block], observing model reads during its execution.
126 * The [target] is stored as a weak reference to be passed to [onCommit] when a change to the
127 * model has been detected.
128 *
129 * Observation for [target] will be paused when a new [observeReads] call is made or when
130 * [pauseObservingReads] is called.
131 *
132 * Any previous observation with the given [target] and [onCommit] will be
133 * cleared and only the new observation on [block] will be stored. It is important that
134 * the same instance of [onCommit] is used between calls or previous references will
135 * not be cleared.
136 *
137 * The [onCommit] will be called when a model that was accessed during [block] has been
George Mount81ddeb12019-12-18 10:46:26 -0800138 * committed, and it will be called with [commitExecutor].
George Mount6623eb42019-12-04 14:38:44 -0800139 */
140 fun <T : Any> observeReads(target: T, onCommit: (T) -> Unit, block: () -> Unit) {
George Mount81ddeb12019-12-18 10:46:26 -0800141 val oldMap = currentMap
142 val oldTarget = currentTarget
143 val oldPaused = isPaused
Ralston Da Silva231e5cc2020-03-05 00:33:03 -0800144
Jim Sprocha88c07a2020-06-25 13:00:03 -0700145 currentMap = @Suppress("DEPRECATION_ERROR") synchronized(commitMaps) {
Ralston Da Silva231e5cc2020-03-05 00:33:03 -0800146 ensureMap(onCommit).apply { removeValue(target) }
George Mount6623eb42019-12-04 14:38:44 -0800147 }
George Mount81ddeb12019-12-18 10:46:26 -0800148 currentTarget = target
149 isPaused = false
150 if (!isObserving) {
151 isObserving = true
152 observeAllReads(readObserver, block)
153 isObserving = false
154 } else {
155 block()
156 }
157 currentMap = oldMap
158 currentTarget = oldTarget
159 isPaused = oldPaused
George Mount6623eb42019-12-04 14:38:44 -0800160 }
161
162 /**
163 * Stops observing model reads while executing [block]. Model reads may be restarted
164 * by calling [observeReads] inside [block].
165 */
166 fun pauseObservingReads(block: () -> Unit) {
George Mount81ddeb12019-12-18 10:46:26 -0800167 val oldPaused = isPaused
168 isPaused = true
169 block()
170 isPaused = oldPaused
George Mount6623eb42019-12-04 14:38:44 -0800171 }
172
173 /**
174 * Clears all model read observations for a given [target]. This clears values for all
175 * `onCommit` methods passed in [observeReads].
176 */
177 fun clear(target: Any) {
Jim Sprocha88c07a2020-06-25 13:00:03 -0700178 @Suppress("DEPRECATION_ERROR")
George Mount83c19ce2019-12-13 12:59:47 -0800179 synchronized(commitMaps) {
George Mountde2dac72020-05-18 13:20:08 -0700180 commitMaps.fastForEach { commitMap ->
George Mount83c19ce2019-12-13 12:59:47 -0800181 commitMap.map.removeValue(target)
George Mount6623eb42019-12-04 14:38:44 -0800182 }
183 }
184 }
185
186 /**
187 * Starts or stops watching for model commits based on [enabled].
188 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700189 fun enableModelUpdatesObserving(enabled: Boolean): Unit = error("deprecated")
George Mount6623eb42019-12-04 14:38:44 -0800190
George Mount83c19ce2019-12-13 12:59:47 -0800191 /**
192 * Calls the `onCommit` callback for the given targets.
193 */
194 private fun callOnCommit(targetsArray: Array<List<Any>>) {
195 for (i in 0..targetsArray.lastIndex) {
196 val targets = targetsArray[i]
197 if (targets.isNotEmpty()) {
Jim Sprocha88c07a2020-06-25 13:00:03 -0700198 @Suppress("DEPRECATION_ERROR")
George Mount83c19ce2019-12-13 12:59:47 -0800199 val commitCaller = synchronized(commitMaps) { commitMaps[i] }
200 commitCaller.callOnCommit(targets)
201 }
George Mount6623eb42019-12-04 14:38:44 -0800202 }
203 }
204
205 /**
George Mount83c19ce2019-12-13 12:59:47 -0800206 * Returns the [ObserverMap] within [commitMaps] associated with [onCommit] or a newly-
207 * inserted one if it doesn't exist.
208 */
209 private fun <T : Any> ensureMap(onCommit: (T) -> Unit): ObserverMap<Any, Any> {
210 val index = commitMaps.indexOfFirst { it.onCommit === onCommit }
211 if (index == -1) {
212 val commitMap = CommitMap(onCommit)
213 commitMaps.add(commitMap)
214 return commitMap.map
215 }
216 return commitMaps[index].map
217 }
218
219 /**
George Mount6623eb42019-12-04 14:38:44 -0800220 * Used to tie an `onCommit` to its target by type. This works around some difficulties in
221 * unchecked casts with kotlin.
222 */
223 @Suppress("UNCHECKED_CAST")
George Mount83c19ce2019-12-13 12:59:47 -0800224 private class CommitMap<T : Any>(val onCommit: (T) -> Unit) {
225 /**
226 * ObserverMap (key = model, value = target). These are the models that have been
227 * read during the target's [ModelObserver.observeReads].
228 */
229 val map = ObserverMap<Any, Any>()
230
231 /**
232 * Calls the `onCommit` callback for targets affected by the given committed values.
233 */
234 fun callOnCommit(targets: List<Any>) {
George Mount6623eb42019-12-04 14:38:44 -0800235 targets.forEach { target ->
236 onCommit(target as T)
237 }
238 }
239 }
George Mount83c19ce2019-12-13 12:59:47 -0800240}