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