1 /*
<lambda>null2 * Copyright 2021 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
18
19 import androidx.collection.MutableObjectIntMap
20 import androidx.collection.MutableScatterMap
21 import androidx.collection.ScatterSet
22 import androidx.compose.runtime.platform.makeSynchronizedObject
23 import androidx.compose.runtime.platform.synchronized
24 import androidx.compose.runtime.snapshots.fastAny
25 import androidx.compose.runtime.snapshots.fastForEach
26 import androidx.compose.runtime.tooling.CompositionObserverHandle
27 import androidx.compose.runtime.tooling.RecomposeScopeObserver
28
29 /**
30 * Represents a recomposable scope or section of the composition hierarchy. Can be used to manually
31 * invalidate the scope to schedule it for recomposition.
32 */
33 interface RecomposeScope {
34 /**
35 * Invalidate the corresponding scope, requesting the composer recompose this scope.
36 *
37 * This method is thread safe.
38 */
39 fun invalidate()
40 }
41
42 private const val changedLowBitMask = 0b001_001_001_001_001_001_001_001_001_001_0
43 private const val changedHighBitMask = changedLowBitMask shl 1
44 private const val changedMask = (changedLowBitMask or changedHighBitMask).inv()
45
46 /**
47 * A compiler plugin utility function to change $changed flags from Different(10) to Same(01) for
48 * when captured by restart lambdas. All parameters are passed with the same value as it was
49 * previously invoked with and the changed flags should reflect that.
50 */
51 @PublishedApi
updateChangedFlagsnull52 internal fun updateChangedFlags(flags: Int): Int {
53 val lowBits = flags and changedLowBitMask
54 val highBits = flags and changedHighBitMask
55 return ((flags and changedMask) or
56 (lowBits or (highBits shr 1)) or
57 ((lowBits shl 1) and highBits))
58 }
59
60 private const val UsedFlag = 0x001
61 private const val DefaultsInScopeFlag = 0x002
62 private const val DefaultsInvalidFlag = 0x004
63 private const val RequiresRecomposeFlag = 0x008
64 private const val SkippedFlag = 0x010
65 private const val RereadingFlag = 0x020
66 private const val ForcedRecomposeFlag = 0x040
67 private const val ForceReusing = 0x080
68 private const val Paused = 0x100
69 private const val Resuming = 0x200
70
71 internal interface RecomposeScopeOwner {
invalidatenull72 fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult
73
74 fun recomposeScopeReleased(scope: RecomposeScopeImpl)
75
76 fun recordReadOf(value: Any)
77 }
78
79 private val callbackLock = makeSynchronizedObject()
80
81 /**
82 * A RecomposeScope is created for a region of the composition that can be recomposed independently
83 * of the rest of the composition. The composer will position the slot table to the location stored
84 * in [anchor] and call [block] when recomposition is requested. It is created by
85 * [Composer.startRestartGroup] and is used to track how to restart the group.
86 */
87 internal class RecomposeScopeImpl(owner: RecomposeScopeOwner?) : ScopeUpdateScope, RecomposeScope {
88
89 private var flags: Int = 0
90
91 private var owner: RecomposeScopeOwner? = owner
92
93 /**
94 * An anchor to the location in the slot table that start the group associated with this
95 * recompose scope.
96 */
97 var anchor: Anchor? = null
98
99 /**
100 * Return whether the scope is valid. A scope becomes invalid when the slots it updates are
101 * removed from the slot table. For example, if the scope is in the then clause of an if
102 * statement that later becomes false.
103 */
104 val valid: Boolean
105 get() = owner != null && anchor?.valid ?: false
106
107 val canRecompose: Boolean
108 get() = block != null
109
110 /**
111 * Used is set when the [RecomposeScopeImpl] is used by, for example, [currentRecomposeScope].
112 * This is used as the result of [Composer.endRestartGroup] and indicates whether the lambda
113 * that is stored in [block] will be used.
114 */
115 var used: Boolean
116 get() = flags and UsedFlag != 0
117 set(value) {
118 flags =
119 if (value) {
120 flags or UsedFlag
121 } else {
122 flags and UsedFlag.inv()
123 }
124 }
125
126 /**
127 * Used to force a scope to the reusing state when a composition is paused while reusing
128 * content.
129 */
130 var reusing: Boolean
131 get() = flags and ForceReusing != 0
132 set(value) {
133 flags =
134 if (value) {
135 flags or ForceReusing
136 } else {
137 flags and ForceReusing.inv()
138 }
139 }
140
141 /** Used to flag a scope as paused for pausable compositions */
142 var paused: Boolean
143 get() = flags and Paused != 0
144 set(value) {
145 flags =
146 if (value) {
147 flags or Paused
148 } else {
149 flags and Paused.inv()
150 }
151 }
152
153 /** Used to flag a scope as paused for pausable compositions */
154 var resuming: Boolean
155 get() = flags and Resuming != 0
156 set(value) {
157 flags =
158 if (value) {
159 flags or Resuming
160 } else {
161 flags and Resuming.inv()
162 }
163 }
164
165 /**
166 * Set to true when the there are function default calculations in the scope. These are treated
167 * as a special case to avoid having to create a special scope for them. If these change the
168 * this scope needs to be recomposed but the default values can be skipped if they where not
169 * invalidated.
170 */
171 var defaultsInScope: Boolean
172 get() = flags and DefaultsInScopeFlag != 0
173 set(value) {
174 if (value) {
175 flags = flags or DefaultsInScopeFlag
176 } else {
177 flags = flags and DefaultsInScopeFlag.inv()
178 }
179 }
180
181 /**
182 * Tracks whether any of the calculations in the default values were changed. See
183 * [defaultsInScope] for details.
184 */
185 var defaultsInvalid: Boolean
186 get() = flags and DefaultsInvalidFlag != 0
187 set(value) {
188 if (value) {
189 flags = flags or DefaultsInvalidFlag
190 } else {
191 flags = flags and DefaultsInvalidFlag.inv()
192 }
193 }
194
195 /**
196 * Tracks whether the scope was invalidated directly but was recomposed because the caller was
197 * recomposed. This ensures that a scope invalidated directly will recompose even if its
198 * parameters are the same as the previous recomposition.
199 */
200 var requiresRecompose: Boolean
201 get() = flags and RequiresRecomposeFlag != 0
202 set(value) {
203 if (value) {
204 flags = flags or RequiresRecomposeFlag
205 } else {
206 flags = flags and RequiresRecomposeFlag.inv()
207 }
208 }
209
210 /** The lambda to call to restart the scopes composition. */
211 private var block: ((Composer, Int) -> Unit)? = null
212
213 /** The recompose scope observer, if one is registered. */
214 @ExperimentalComposeRuntimeApi private var observer: RecomposeScopeObserver? = null
215
216 /**
217 * Restart the scope's composition. It is an error if [block] was not updated. The code
218 * generated by the compiler ensures that when the recompose scope is used then [block] will be
219 * set but it might occur if the compiler is out-of-date (or ahead of the runtime) or incorrect
220 * direct calls to [Composer.startRestartGroup] and [Composer.endRestartGroup].
221 */
222 @OptIn(ExperimentalComposeRuntimeApi::class)
223 fun compose(composer: Composer) {
224 val block = block
225 val observer = observer
226 if (observer != null && block != null) {
227 observer.onBeginScopeComposition(this)
228 try {
229 block(composer, 1)
230 } finally {
231 observer.onEndScopeComposition(this)
232 }
233 return
234 }
235 block?.invoke(composer, 1) ?: error("Invalid restart scope")
236 }
237
238 @ExperimentalComposeRuntimeApi
239 internal fun observe(observer: RecomposeScopeObserver): CompositionObserverHandle {
240 synchronized(callbackLock) { this.observer = observer }
241 return object : CompositionObserverHandle {
242 override fun dispose() {
243 synchronized(callbackLock) {
244 if (this@RecomposeScopeImpl.observer == observer) {
245 this@RecomposeScopeImpl.observer = null
246 }
247 }
248 }
249 }
250 }
251
252 /**
253 * Invalidate the group which will cause [owner] to request this scope be recomposed, and an
254 * [InvalidationResult] will be returned.
255 */
256 fun invalidateForResult(value: Any?): InvalidationResult =
257 owner?.invalidate(this, value) ?: InvalidationResult.IGNORED
258
259 /**
260 * Release the recompose scope. This is called when the recompose scope has been removed by the
261 * compostion because the part of the composition it was tracking was removed.
262 */
263 fun release() {
264 owner?.recomposeScopeReleased(this)
265 owner = null
266 trackedInstances = null
267 trackedDependencies = null
268 @OptIn(ExperimentalComposeRuntimeApi::class) observer?.onScopeDisposed(this)
269 }
270
271 /**
272 * Called when the data tracked by this recompose scope moves to a different composition when
273 * for example, the movable content it is part of has moved.
274 */
275 fun adoptedBy(owner: RecomposeScopeOwner) {
276 this.owner = owner
277 }
278
279 /**
280 * Invalidate the group which will cause [owner] to request this scope be recomposed.
281 *
282 * Unlike [invalidateForResult], this method is thread safe and calls the thread safe invalidate
283 * on the composer.
284 */
285 override fun invalidate() {
286 owner?.invalidate(this, null)
287 }
288
289 /**
290 * Update [block]. The scope is returned by [Composer.endRestartGroup] when [used] is true and
291 * implements [ScopeUpdateScope].
292 */
293 override fun updateScope(block: (Composer, Int) -> Unit) {
294 this.block = block
295 }
296
297 private var currentToken = 0
298 private var trackedInstances: MutableObjectIntMap<Any>? = null
299 private var trackedDependencies: MutableScatterMap<DerivedState<*>, Any?>? = null
300 private var rereading: Boolean
301 get() = flags and RereadingFlag != 0
302 set(value) {
303 if (value) {
304 flags = flags or RereadingFlag
305 } else {
306 flags = flags and RereadingFlag.inv()
307 }
308 }
309
310 /**
311 * Used to explicitly force recomposition. This is used during live edit to force a recompose
312 * scope that doesn't have a restart callback to recompose as its parent (or some parent above
313 * it) was invalidated and the path to this scope has also been forced.
314 */
315 var forcedRecompose: Boolean
316 get() = flags and ForcedRecomposeFlag != 0
317 set(value) {
318 if (value) {
319 flags = flags or ForcedRecomposeFlag
320 } else {
321 flags = flags and ForcedRecomposeFlag.inv()
322 }
323 }
324
325 /** Indicates whether the scope was skipped (e.g. [scopeSkipped] was called. */
326 internal var skipped: Boolean
327 get() = flags and SkippedFlag != 0
328 private set(value) {
329 if (value) {
330 flags = flags or SkippedFlag
331 } else {
332 flags = flags and SkippedFlag.inv()
333 }
334 }
335
336 /**
337 * Called when composition start composing into this scope. The [token] is a value that is
338 * unique everytime this is called. This is currently the snapshot id but that shouldn't be
339 * relied on.
340 */
341 fun start(token: Int) {
342 currentToken = token
343 skipped = false
344 }
345
346 fun scopeSkipped() {
347 if (!reusing) {
348 skipped = true
349 }
350 }
351
352 /**
353 * Track instances that were read in scope.
354 *
355 * @return whether the value was already read in scope during current pass
356 */
357 fun recordRead(instance: Any): Boolean {
358 if (rereading) return false // Re-reading should force composition to update its tracking
359
360 val trackedInstances =
361 trackedInstances ?: MutableObjectIntMap<Any>().also { trackedInstances = it }
362
363 val token = trackedInstances.put(instance, currentToken, default = -1)
364 if (token == currentToken) {
365 return true
366 }
367
368 return false
369 }
370
371 fun recordDerivedStateValue(instance: DerivedState<*>, value: Any?) {
372 val trackedDependencies =
373 trackedDependencies
374 ?: MutableScatterMap<DerivedState<*>, Any?>().also { trackedDependencies = it }
375
376 trackedDependencies[instance] = value
377 }
378
379 /**
380 * Returns true if the scope is observing derived state which might make this scope
381 * conditionally invalidated.
382 */
383 val isConditional: Boolean
384 get() = trackedDependencies != null
385
386 /**
387 * Determine if the scope should be considered invalid.
388 *
389 * @param instances The set of objects reported as invalidating this scope.
390 */
391 fun isInvalidFor(instances: Any? /* State | ScatterSet<State> | null */): Boolean {
392 // If a non-empty instances exists and contains only derived state objects with their
393 // default values, then the scope should not be considered invalid. Otherwise the scope
394 // should if it was invalidated by any other kind of instance.
395 if (instances == null) return true
396 val trackedDependencies = trackedDependencies ?: return true
397
398 return when (instances) {
399 is DerivedState<*> -> {
400 instances.checkDerivedStateChanged(trackedDependencies)
401 }
402 is ScatterSet<*> -> {
403 instances.isNotEmpty() &&
404 instances.any {
405 it !is DerivedState<*> || it.checkDerivedStateChanged(trackedDependencies)
406 }
407 }
408 else -> true
409 }
410 }
411
412 private fun DerivedState<*>.checkDerivedStateChanged(
413 dependencies: MutableScatterMap<DerivedState<*>, Any?>
414 ): Boolean {
415 @Suppress("UNCHECKED_CAST")
416 this as DerivedState<Any?>
417 val policy = policy ?: structuralEqualityPolicy()
418 return !policy.equivalent(currentRecord.currentValue, dependencies[this])
419 }
420
421 fun rereadTrackedInstances() {
422 owner?.let { owner ->
423 trackedInstances?.let { trackedInstances ->
424 rereading = true
425 try {
426 trackedInstances.forEach { value, _ -> owner.recordReadOf(value) }
427 } finally {
428 rereading = false
429 }
430 }
431 }
432 }
433
434 /**
435 * Called when composition is completed for this scope. The [token] is the same token passed in
436 * the previous call to [start]. If [end] returns a non-null value the lambda returned will be
437 * called during [ControlledComposition.applyChanges].
438 */
439 fun end(token: Int): ((Composition) -> Unit)? {
440 return trackedInstances?.let { instances ->
441 // If any value previous observed was not read in this current composition
442 // schedule the value to be removed from the observe scope and removed from the
443 // observations tracked by the composition.
444 // [skipped] is true if the scope was skipped. If the scope was skipped we should
445 // leave the observations unmodified.
446 if (!skipped && instances.any { _, instanceToken -> instanceToken != token })
447 { composition ->
448 if (
449 currentToken == token &&
450 instances == trackedInstances &&
451 composition is CompositionImpl
452 ) {
453 instances.removeIf { instance, instanceToken ->
454 val shouldRemove = instanceToken != token
455 if (shouldRemove) {
456 composition.removeObservation(instance, this)
457 if (instance is DerivedState<*>) {
458 composition.removeDerivedStateObservation(instance)
459 trackedDependencies?.remove(instance)
460 }
461 }
462 shouldRemove
463 }
464 }
465 }
466 else null
467 }
468 }
469
470 companion object {
471 internal fun adoptAnchoredScopes(
472 slots: SlotWriter,
473 anchors: List<Anchor>,
474 newOwner: RecomposeScopeOwner
475 ) {
476 if (anchors.isNotEmpty()) {
477 anchors.fastForEach { anchor ->
478 // The recompose scope is always at slot 0 of a restart group.
479 val recomposeScope = slots.slot(anchor, 0) as? RecomposeScopeImpl
480 // Check for null as the anchor might not be for a recompose scope
481 recomposeScope?.adoptedBy(newOwner)
482 }
483 }
484 }
485
486 internal fun hasAnchoredRecomposeScopes(slots: SlotTable, anchors: List<Anchor>) =
487 anchors.isNotEmpty() &&
488 anchors.fastAny {
489 slots.ownsAnchor(it) &&
490 slots.slot(slots.anchorIndex(it), 0) is RecomposeScopeImpl
491 }
492 }
493 }
494