[go: nahoru, domu]

blob: 06e94c9f314d8367ad18ab9cb760cb2948d82b3e [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("DEPRECATION", "UNUSED_PARAMETER")
package androidx.ui.core
import androidx.compose.ObserverMap
import androidx.compose.frames.FrameCommitObserver
import androidx.compose.frames.FrameReadObserver
import androidx.compose.frames.observeAllReads
import androidx.ui.util.fastForEach
/**
* Allows for easy model read observation. To begin observe a change, you must pass a
* non-lambda `onCommit` listener to the [observeReads] method.
*
* When a state change has been committed, the `onCommit` listener will be called
* with the `targetObject` as the argument. There are no order guarantees for
* `onCommit` listener calls. Commit callbacks are made on the thread that model changes
* are committed, so the [commitExecutor] allows the developer to control the thread on which the
* `onCommit`calls are made. An example use would be to have the executor shift to the
* the UI thread for the `onCommit` callbacks to be made.
*
* A different ModelObserver should be used with each thread that [observeReads] is called on.
*
* @param commitExecutor The executor on which all `onCommit` calls will be made.
*/
@Deprecated("Frames have been replaced by snapshots",
ReplaceWith(
"SnapshotStateObserver",
"androidx.compose.snapshots"
)
)
class ModelObserver(private val commitExecutor: (command: () -> Unit) -> Unit) {
private val commitObserver: FrameCommitObserver = { committed, _ ->
var hasValues = false
// This array is in the same order as commitMaps
@Suppress("DEPRECATION_ERROR")
val targetsArray = synchronized(commitMaps) {
Array(commitMaps.size) { index ->
commitMaps[index].map.get(committed).apply {
if (isNotEmpty())
hasValues = true
}
}
}
if (hasValues) {
commitExecutor {
callOnCommit(targetsArray)
}
}
}
/**
* The [FrameReadObserver] used by this [ModelObserver] during [observeReads].
*/
private val readObserver: FrameReadObserver = { model ->
if (!isPaused) {
@Suppress("DEPRECATION_ERROR")
synchronized(commitMaps) {
currentMap!!.add(model, currentTarget!!)
}
}
}
/**
* List of all [CommitMap]s. When [observeReads] is called, there will be
* a [CommitMap] associated with its `onCommit` callback in this list. The list
* only grows.
*/
private val commitMaps = mutableListOf<CommitMap<*>>()
/**
* Method to call when unsubscribing from the commit observer.
*/
private var commitUnsubscribe: (() -> Unit)? = null
/**
* `true` when an [observeReads] is in progress and [readObserver] is active and
* `false` when [readObserver] is no longer observing changes.
*/
private var isObserving = false
/**
* `true` when [pauseObservingReads] is called and read observations should no
* longer be considered invalidations for the `onCommit` callback.
*/
private var isPaused = false
/**
* The [ObserverMap] that should be added to when a model is read during [observeReads].
*/
private var currentMap: ObserverMap<Any, Any>? = null
/**
* The target associated with the active [observeReads] call.
*/
private var currentTarget: Any? = null
/**
* Test-only access to the internal commit listener. This is used for benchmarking
* the commit notification callback.
*
* @suppress
*/
val frameCommitObserver: FrameCommitObserver
@InternalCoreApi
get() = commitObserver
/**
* Executes [block], observing model reads during its execution.
* The [target] is stored as a weak reference to be passed to [onCommit] when a change to the
* model has been detected.
*
* Observation for [target] will be paused when a new [observeReads] call is made or when
* [pauseObservingReads] is called.
*
* Any previous observation with the given [target] and [onCommit] will be
* cleared and only the new observation on [block] will be stored. It is important that
* the same instance of [onCommit] is used between calls or previous references will
* not be cleared.
*
* The [onCommit] will be called when a model that was accessed during [block] has been
* committed, and it will be called with [commitExecutor].
*/
fun <T : Any> observeReads(target: T, onCommit: (T) -> Unit, block: () -> Unit) {
val oldMap = currentMap
val oldTarget = currentTarget
val oldPaused = isPaused
currentMap = @Suppress("DEPRECATION_ERROR") synchronized(commitMaps) {
ensureMap(onCommit).apply { removeValue(target) }
}
currentTarget = target
isPaused = false
if (!isObserving) {
isObserving = true
observeAllReads(readObserver, block)
isObserving = false
} else {
block()
}
currentMap = oldMap
currentTarget = oldTarget
isPaused = oldPaused
}
/**
* Stops observing model reads while executing [block]. Model reads may be restarted
* by calling [observeReads] inside [block].
*/
fun pauseObservingReads(block: () -> Unit) {
val oldPaused = isPaused
isPaused = true
block()
isPaused = oldPaused
}
/**
* Clears all model read observations for a given [target]. This clears values for all
* `onCommit` methods passed in [observeReads].
*/
fun clear(target: Any) {
@Suppress("DEPRECATION_ERROR")
synchronized(commitMaps) {
commitMaps.fastForEach { commitMap ->
commitMap.map.removeValue(target)
}
}
}
/**
* Starts or stops watching for model commits based on [enabled].
*/
fun enableModelUpdatesObserving(enabled: Boolean): Unit = error("deprecated")
/**
* Calls the `onCommit` callback for the given targets.
*/
private fun callOnCommit(targetsArray: Array<List<Any>>) {
for (i in 0..targetsArray.lastIndex) {
val targets = targetsArray[i]
if (targets.isNotEmpty()) {
@Suppress("DEPRECATION_ERROR")
val commitCaller = synchronized(commitMaps) { commitMaps[i] }
commitCaller.callOnCommit(targets)
}
}
}
/**
* Returns the [ObserverMap] within [commitMaps] associated with [onCommit] or a newly-
* inserted one if it doesn't exist.
*/
private fun <T : Any> ensureMap(onCommit: (T) -> Unit): ObserverMap<Any, Any> {
val index = commitMaps.indexOfFirst { it.onCommit === onCommit }
if (index == -1) {
val commitMap = CommitMap(onCommit)
commitMaps.add(commitMap)
return commitMap.map
}
return commitMaps[index].map
}
/**
* Used to tie an `onCommit` to its target by type. This works around some difficulties in
* unchecked casts with kotlin.
*/
@Suppress("UNCHECKED_CAST")
private class CommitMap<T : Any>(val onCommit: (T) -> Unit) {
/**
* ObserverMap (key = model, value = target). These are the models that have been
* read during the target's [ModelObserver.observeReads].
*/
val map = ObserverMap<Any, Any>()
/**
* Calls the `onCommit` callback for targets affected by the given committed values.
*/
fun callOnCommit(targets: List<Any>) {
targets.forEach { target ->
onCommit(target as T)
}
}
}
}