1 /*
<lambda>null2  * Copyright 2020 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.ui.layout
18 
19 import androidx.collection.MutableOrderedScatterSet
20 import androidx.collection.mutableIntSetOf
21 import androidx.collection.mutableOrderedScatterSetOf
22 import androidx.collection.mutableScatterMapOf
23 import androidx.compose.runtime.Applier
24 import androidx.compose.runtime.Composable
25 import androidx.compose.runtime.ComposeNodeLifecycleCallback
26 import androidx.compose.runtime.CompositionContext
27 import androidx.compose.runtime.PausableComposition
28 import androidx.compose.runtime.PausedComposition
29 import androidx.compose.runtime.ReusableComposeNode
30 import androidx.compose.runtime.ReusableComposition
31 import androidx.compose.runtime.ReusableContentHost
32 import androidx.compose.runtime.ShouldPauseCallback
33 import androidx.compose.runtime.SideEffect
34 import androidx.compose.runtime.collection.mutableVectorOf
35 import androidx.compose.runtime.currentComposer
36 import androidx.compose.runtime.currentCompositeKeyHashCode
37 import androidx.compose.runtime.mutableStateOf
38 import androidx.compose.runtime.remember
39 import androidx.compose.runtime.rememberCompositionContext
40 import androidx.compose.runtime.snapshots.Snapshot
41 import androidx.compose.ui.ComposeUiFlags
42 import androidx.compose.ui.ExperimentalComposeUiApi
43 import androidx.compose.ui.Modifier
44 import androidx.compose.ui.UiComposable
45 import androidx.compose.ui.internal.checkPrecondition
46 import androidx.compose.ui.internal.requirePrecondition
47 import androidx.compose.ui.internal.throwIllegalStateExceptionForNullCheck
48 import androidx.compose.ui.internal.throwIndexOutOfBoundsException
49 import androidx.compose.ui.layout.SubcomposeLayoutState.PausedPrecomposition
50 import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
51 import androidx.compose.ui.materialize
52 import androidx.compose.ui.node.ComposeUiNode.Companion.SetCompositeKeyHash
53 import androidx.compose.ui.node.ComposeUiNode.Companion.SetModifier
54 import androidx.compose.ui.node.ComposeUiNode.Companion.SetResolvedCompositionLocals
55 import androidx.compose.ui.node.LayoutNode
56 import androidx.compose.ui.node.LayoutNode.LayoutState
57 import androidx.compose.ui.node.LayoutNode.UsageByParent
58 import androidx.compose.ui.node.OutOfFrameExecutor
59 import androidx.compose.ui.node.TraversableNode
60 import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction
61 import androidx.compose.ui.node.checkMeasuredSize
62 import androidx.compose.ui.node.requireOwner
63 import androidx.compose.ui.node.traverseDescendants
64 import androidx.compose.ui.platform.createPausableSubcomposition
65 import androidx.compose.ui.platform.createSubcomposition
66 import androidx.compose.ui.unit.Constraints
67 import androidx.compose.ui.unit.IntSize
68 import androidx.compose.ui.unit.LayoutDirection
69 import androidx.compose.ui.util.fastForEach
70 
71 /**
72  * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage for
73  * example to use the values calculated during the measurement as params for the composition of the
74  * children.
75  *
76  * Possible use cases:
77  * * You need to know the constraints passed by the parent during the composition and can't solve
78  *   your use case with just custom [Layout] or [LayoutModifier]. See
79  *   [androidx.compose.foundation.layout.BoxWithConstraints].
80  * * You want to use the size of one child during the composition of the second child.
81  * * You want to compose your items lazily based on the available size. For example you have a list
82  *   of 100 items and instead of composing all of them you only compose the ones which are currently
83  *   visible(say 5 of them) and compose next items when the component is scrolled.
84  *
85  * @sample androidx.compose.ui.samples.SubcomposeLayoutSample
86  * @param modifier [Modifier] to apply for the layout.
87  * @param measurePolicy Measure policy which provides ability to subcompose during the measuring.
88  */
89 @Composable
90 fun SubcomposeLayout(
91     modifier: Modifier = Modifier,
92     measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
93 ) {
94     SubcomposeLayout(
95         state = remember { SubcomposeLayoutState() },
96         modifier = modifier,
97         measurePolicy = measurePolicy
98     )
99 }
100 
101 /**
102  * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage for
103  * example to use the values calculated during the measurement as params for the composition of the
104  * children.
105  *
106  * Possible use cases:
107  * * You need to know the constraints passed by the parent during the composition and can't solve
108  *   your use case with just custom [Layout] or [LayoutModifier]. See
109  *   [androidx.compose.foundation.layout.BoxWithConstraints].
110  * * You want to use the size of one child during the composition of the second child.
111  * * You want to compose your items lazily based on the available size. For example you have a list
112  *   of 100 items and instead of composing all of them you only compose the ones which are currently
113  *   visible(say 5 of them) and compose next items when the component is scrolled.
114  *
115  * @sample androidx.compose.ui.samples.SubcomposeLayoutSample
116  * @param state the state object to be used by the layout.
117  * @param modifier [Modifier] to apply for the layout.
118  * @param measurePolicy Measure policy which provides ability to subcompose during the measuring.
119  */
120 @Composable
121 @UiComposable
SubcomposeLayoutnull122 fun SubcomposeLayout(
123     state: SubcomposeLayoutState,
124     modifier: Modifier = Modifier,
125     measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
126 ) {
127     val compositeKeyHash = currentCompositeKeyHashCode.hashCode()
128     val compositionContext = rememberCompositionContext()
129     val materialized = currentComposer.materialize(modifier)
130     val localMap = currentComposer.currentCompositionLocalMap
131     ReusableComposeNode<LayoutNode, Applier<Any>>(
132         factory = LayoutNode.Constructor,
133         update = {
134             set(state, state.setRoot)
135             set(compositionContext, state.setCompositionContext)
136             set(measurePolicy, state.setMeasurePolicy)
137             set(localMap, SetResolvedCompositionLocals)
138             set(materialized, SetModifier)
139             set(compositeKeyHash, SetCompositeKeyHash)
140         }
141     )
142     if (!currentComposer.skipping) {
143         SideEffect { state.forceRecomposeChildren() }
144     }
145 }
146 
147 /**
148  * The receiver scope of a [SubcomposeLayout]'s measure lambda which adds ability to dynamically
149  * subcompose a content during the measuring on top of the features provided by [MeasureScope].
150  */
151 interface SubcomposeMeasureScope : MeasureScope {
152     /**
153      * Performs subcomposition of the provided [content] with given [slotId].
154      *
155      * @param slotId unique id which represents the slot we are composing into. If you have fixed
156      *   amount or slots you can use enums as slot ids, or if you have a list of items maybe an
157      *   index in the list or some other unique key can work. To be able to correctly match the
158      *   content between remeasures you should provide the object which is equals to the one you
159      *   used during the previous measuring.
160      * @param content the composable content which defines the slot. It could emit multiple layouts,
161      *   in this case the returned list of [Measurable]s will have multiple elements. **Note:** When
162      *   a [SubcomposeLayout] is in a [LookaheadScope], the subcomposition only happens during the
163      *   lookahead pass. In the post-lookahead/main pass, [subcompose] will return the list of
164      *   [Measurable]s that were subcomposed during the lookahead pass. If the structure of the
165      *   subtree emitted from [content] is dependent on incoming constraints, consider using
166      *   constraints received from the lookahead pass for both passes.
167      */
subcomposenull168     fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable>
169 }
170 
171 /**
172  * State used by [SubcomposeLayout].
173  *
174  * [slotReusePolicy] the policy defining what slots should be retained to be reused later.
175  */
176 class SubcomposeLayoutState(private val slotReusePolicy: SubcomposeSlotReusePolicy) {
177     /** State used by [SubcomposeLayout]. */
178     constructor() : this(NoOpSubcomposeSlotReusePolicy)
179 
180     /**
181      * State used by [SubcomposeLayout].
182      *
183      * @param maxSlotsToRetainForReuse when non-zero the layout will keep active up to this count
184      *   slots which we were used but not used anymore instead of disposing them. Later when you try
185      *   to compose a new slot instead of creating a completely new slot the layout would reuse the
186      *   previous slot which allows to do less work especially if the slot contents are similar.
187      */
188     @Deprecated(
189         "This constructor is deprecated",
190         ReplaceWith(
191             "SubcomposeLayoutState(SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse))",
192             "androidx.compose.ui.layout.SubcomposeSlotReusePolicy"
193         )
194     )
195     constructor(
196         maxSlotsToRetainForReuse: Int
197     ) : this(SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse))
198 
199     private var _state: LayoutNodeSubcompositionsState? = null
200     private val state: LayoutNodeSubcompositionsState
201         get() =
202             requireNotNull(_state) { "SubcomposeLayoutState is not attached to SubcomposeLayout" }
203 
204     // Pre-allocated lambdas to update LayoutNode
205     internal val setRoot: LayoutNode.(SubcomposeLayoutState) -> Unit = {
206         _state =
207             subcompositionsState
208                 ?: LayoutNodeSubcompositionsState(this, slotReusePolicy).also {
209                     subcompositionsState = it
210                 }
211         state.makeSureStateIsConsistent()
212         state.slotReusePolicy = slotReusePolicy
213     }
214     internal val setCompositionContext: LayoutNode.(CompositionContext) -> Unit = {
215         state.compositionContext = it
216     }
217     internal val setMeasurePolicy:
218         LayoutNode.((SubcomposeMeasureScope.(Constraints) -> MeasureResult)) -> Unit =
219         {
220             measurePolicy = state.createMeasurePolicy(it)
221         }
222 
223     /**
224      * Composes the content for the given [slotId]. This makes the next scope.subcompose(slotId)
225      * call during the measure pass faster as the content is already composed.
226      *
227      * If the [slotId] was precomposed already but after the future calculations ended up to not be
228      * needed anymore (meaning this slotId is not going to be used during the measure pass anytime
229      * soon) you can use [PrecomposedSlotHandle.dispose] on a returned object to dispose the
230      * content.
231      *
232      * @param slotId unique id which represents the slot to compose into.
233      * @param content the composable content which defines the slot.
234      * @return [PrecomposedSlotHandle] instance which allows you to dispose the content.
235      */
236     fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle =
237         state.precompose(slotId, content)
238 
239     /**
240      * @param slotId unique id which represents the slot to compose into.
241      * @param content the composable content which defines the slot.]
242      * @return [PausedPrecomposition] for the given [slotId]. It allows to perform the composition
243      *   in an incremental manner. Performing full or partial precomposition makes the next
244      *   scope.subcompose(slotId) call during the measure pass faster as the content is already
245      *   composed.
246      */
247     fun createPausedPrecomposition(
248         slotId: Any?,
249         content: @Composable () -> Unit
250     ): PausedPrecomposition = state.precomposePaused(slotId, content)
251 
252     internal fun forceRecomposeChildren() = state.forceRecomposeChildren()
253 
254     /**
255      * A [PausedPrecomposition] is a subcomposition that can be composed incrementally as it
256      * supports being paused and resumed.
257      *
258      * Pausable subcomposition can be used between frames to prepare a subcomposition before it is
259      * required by the main composition. For example, this is used in lazy lists to prepare list
260      * items in between frames to that are likely to be scrolled in. The composition is paused when
261      * the start of the next frame is near, allowing composition to be spread across multiple frames
262      * without delaying the production of the next frame.
263      *
264      * @see [PausedComposition]
265      */
266     sealed interface PausedPrecomposition {
267 
268         /**
269          * Returns `true` when the [PausedPrecomposition] is complete. [isComplete] matches the last
270          * value returned from [resume]. Once a [PausedPrecomposition] is [isComplete] the [apply]
271          * method should be called. If the [apply] method is not called synchronously and
272          * immediately after [resume] returns `true` then this [isComplete] can return `false` as
273          * any state changes read by the paused composition while it is paused will cause the
274          * composition to require the paused composition to need to be resumed before it is used.
275          */
276         val isComplete: Boolean
277 
278         /**
279          * Resume the composition that has been paused. This method should be called until [resume]
280          * returns `true` or [isComplete] is `true` which has the same result as the last result of
281          * calling [resume]. The [shouldPause] parameter is a lambda that returns whether the
282          * composition should be paused. For example, in lazy lists this returns `false` until just
283          * prior to the next frame starting in which it returns `true`
284          *
285          * Calling [resume] after it returns `true` or when `isComplete` is true will throw an
286          * exception.
287          *
288          * @param shouldPause A lambda that is used to determine if the composition should be
289          *   paused. This lambda is called often so should be a very simple calculation. Returning
290          *   `true` does not guarantee the composition will pause, it should only be considered a
291          *   request to pause the composition. Not all composable functions are pausable and only
292          *   pausable composition functions will pause.
293          * @return `true` if the composition is complete and `false` if one or more calls to
294          *   `resume` are required to complete composition.
295          */
296         @Suppress("ExecutorRegistration") fun resume(shouldPause: ShouldPauseCallback): Boolean
297 
298         /**
299          * Apply the composition. This is the last step of a paused composition and is required to
300          * be called prior to the composition is usable.
301          *
302          * Calling [apply] should always be proceeded with a check of [isComplete] before it is
303          * called and potentially calling [resume] in a loop until [isComplete] returns `true`. This
304          * can happen if [resume] returned `true` but [apply] was not synchronously called
305          * immediately afterwords. Any state that was read that changed between when [resume] being
306          * called and [apply] being called may require the paused composition to be resumed before
307          * applied.
308          *
309          * @return [PrecomposedSlotHandle] you can use to premeasure the slot as well, or to dispose
310          *   the composed content.
311          */
312         fun apply(): PrecomposedSlotHandle
313 
314         /**
315          * Cancels the paused composition. This should only be used if the composition is going to
316          * be disposed and the entire composition is not going to be used.
317          */
318         fun cancel()
319     }
320 
321     /** Instance of this interface is returned by [precompose] function. */
322     interface PrecomposedSlotHandle {
323 
324         /**
325          * This function allows to dispose the content for the slot which was precomposed previously
326          * via [precompose].
327          *
328          * If this slot was already used during the regular measure pass via
329          * [SubcomposeMeasureScope.subcompose] this function will do nothing.
330          *
331          * This could be useful if after the future calculations this item is not anymore expected
332          * to be used during the measure pass anytime soon.
333          */
334         fun dispose()
335 
336         /** The amount of placeables composed into this slot. */
337         val placeablesCount: Int
338             get() = 0
339 
340         /**
341          * Performs synchronous measure of the placeable at the given [index].
342          *
343          * @param index the placeable index. Should be smaller than [placeablesCount].
344          * @param constraints Constraints to measure this placeable with.
345          */
346         fun premeasure(index: Int, constraints: Constraints) {}
347 
348         /**
349          * Conditionally executes [block] for each [Modifier.Node] of this Composition that is a
350          * [TraversableNode] with a matching [key].
351          *
352          * See [androidx.compose.ui.node.traverseDescendants] for the complete semantics of this
353          * function.
354          */
355         fun traverseDescendants(key: Any?, block: (TraversableNode) -> TraverseDescendantsAction) {}
356 
357         /**
358          * Retrieves the latest measured size for a given placeable [index]. This will return
359          * [IntSize.Zero] if this is called before [premeasure].
360          */
361         fun getSize(index: Int): IntSize = IntSize.Zero
362     }
363 }
364 
365 /**
366  * This policy allows [SubcomposeLayout] to retain some of slots which we were used but not used
367  * anymore instead of disposing them. Next time when you try to compose a new slot instead of
368  * creating a completely new slot the layout would reuse the kept slot. This allows to do less work
369  * especially if the slot contents are similar.
370  */
371 interface SubcomposeSlotReusePolicy {
372     /**
373      * This function will be called with [slotIds] set populated with the slot ids available to
374      * reuse. In the implementation you can remove slots you don't want to retain.
375      */
getSlotsToRetainnull376     fun getSlotsToRetain(slotIds: SlotIdsSet)
377 
378     /**
379      * Returns true if the content previously composed with [reusableSlotId] is compatible with the
380      * content which is going to be composed for [slotId]. Slots could be considered incompatible if
381      * they display completely different types of the UI.
382      */
383     fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean
384 
385     /**
386      * Set containing slot ids currently available to reuse. Used by [getSlotsToRetain]. The set
387      * retains the insertion order of its elements, guaranteeing stable iteration order.
388      *
389      * This class works exactly as [MutableSet], but doesn't allow to add new items in it.
390      */
391     class SlotIdsSet
392     internal constructor(
393         @PublishedApi
394         internal val set: MutableOrderedScatterSet<Any?> = mutableOrderedScatterSetOf()
395     ) : Collection<Any?> {
396 
397         override val size: Int
398             get() = set.size
399 
400         override fun isEmpty(): Boolean = set.isEmpty()
401 
402         override fun containsAll(elements: Collection<Any?>): Boolean {
403             elements.forEach { element ->
404                 if (element !in set) {
405                     return false
406                 }
407             }
408             return true
409         }
410 
411         override fun contains(element: Any?): Boolean = set.contains(element)
412 
413         internal fun add(slotId: Any?) = set.add(slotId)
414 
415         override fun iterator(): MutableIterator<Any?> = set.asMutableSet().iterator()
416 
417         /**
418          * Removes a [slotId] from this set, if it is present.
419          *
420          * @return `true` if the slot id was removed, `false` if the set was not modified.
421          */
422         fun remove(slotId: Any?): Boolean = set.remove(slotId)
423 
424         /**
425          * Removes all slot ids from [slotIds] that are also contained in this set.
426          *
427          * @return `true` if any slot id was removed, `false` if the set was not modified.
428          */
429         fun removeAll(slotIds: Collection<Any?>): Boolean = set.remove(slotIds)
430 
431         /**
432          * Removes all slot ids that match the given [predicate].
433          *
434          * @return `true` if any slot id was removed, `false` if the set was not modified.
435          */
436         fun removeAll(predicate: (Any?) -> Boolean): Boolean {
437             val size = set.size
438             set.removeIf(predicate)
439             return size != set.size
440         }
441 
442         /**
443          * Retains only the slot ids that are contained in [slotIds].
444          *
445          * @return `true` if any slot id was removed, `false` if the set was not modified.
446          */
447         fun retainAll(slotIds: Collection<Any?>): Boolean = set.retainAll(slotIds)
448 
449         /**
450          * Retains only slotIds that match the given [predicate].
451          *
452          * @return `true` if any slot id was removed, `false` if the set was not modified.
453          */
454         fun retainAll(predicate: (Any?) -> Boolean): Boolean = set.retainAll(predicate)
455 
456         /** Removes all slot ids from this set. */
457         fun clear() = set.clear()
458 
459         /**
460          * Remove entries until [size] equals [maxSlotsToRetainForReuse]. Entries inserted last are
461          * removed first.
462          */
463         fun trimToSize(maxSlotsToRetainForReuse: Int) = set.trimToSize(maxSlotsToRetainForReuse)
464 
465         /**
466          * Iterates over every element stored in this set by invoking the specified [block] lambda.
467          * The iteration order is the same as the insertion order. It is safe to remove the element
468          * passed to [block] during iteration.
469          *
470          * NOTE: This method is obscured by `Collection<T>.forEach` since it is marked with
471          *
472          * @HidesMember, which means in practice this will never get called. Please use
473          *   [fastForEach] instead.
474          */
475         fun forEach(block: (Any?) -> Unit) = set.forEach(block)
476 
477         /**
478          * Iterates over every element stored in this set by invoking the specified [block] lambda.
479          * The iteration order is the same as the insertion order. It is safe to remove the element
480          * passed to [block] during iteration.
481          *
482          * NOTE: this method was added in order to allow for a more performant forEach method. It is
483          * necessary because [forEach] is obscured by `Collection<T>.forEach` since it is marked
484          * with @HidesMember.
485          */
486         inline fun fastForEach(block: (Any?) -> Unit) = set.forEach(block)
487     }
488 }
489 
490 /**
491  * Creates [SubcomposeSlotReusePolicy] which retains the fixed amount of slots.
492  *
493  * @param maxSlotsToRetainForReuse the [SubcomposeLayout] will retain up to this amount of slots.
494  */
SubcomposeSlotReusePolicynull495 fun SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse: Int): SubcomposeSlotReusePolicy =
496     FixedCountSubcomposeSlotReusePolicy(maxSlotsToRetainForReuse)
497 
498 /**
499  * The inner state containing all the information about active slots and their compositions. It is
500  * stored inside LayoutNode object as in fact we need to keep 1-1 mapping between this state and the
501  * node: when we compose a slot we first create a virtual LayoutNode child to this node and then
502  * save the extra information inside this state. Keeping this state inside LayoutNode also helps us
503  * to retain the pool of reusable slots even when a new SubcomposeLayoutState is applied to
504  * SubcomposeLayout and even when the SubcomposeLayout's LayoutNode is reused via the
505  * ReusableComposeNode mechanism.
506  */
507 @OptIn(ExperimentalComposeUiApi::class)
508 internal class LayoutNodeSubcompositionsState(
509     private val root: LayoutNode,
510     slotReusePolicy: SubcomposeSlotReusePolicy
511 ) : ComposeNodeLifecycleCallback {
512     var compositionContext: CompositionContext? = null
513 
514     var slotReusePolicy: SubcomposeSlotReusePolicy = slotReusePolicy
515         set(value) {
516             if (field !== value) {
517                 field = value
518                 // the new policy will be applied after measure
519                 markActiveNodesAsReused(deactivate = false)
520                 root.requestRemeasure()
521             }
522         }
523 
524     private var currentIndex = 0
525     private var currentApproachIndex = 0
526     private val nodeToNodeState = mutableScatterMapOf<LayoutNode, NodeState>()
527 
528     // this map contains active slotIds (without precomposed or reusable nodes)
529     private val slotIdToNode = mutableScatterMapOf<Any?, LayoutNode>()
530     private val scope = Scope()
531     private val approachMeasureScope = ApproachMeasureScopeImpl()
532 
533     private val precomposeMap = mutableScatterMapOf<Any?, LayoutNode>()
534     private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet()
535 
536     // SlotHandles precomposed in the post-lookahead pass.
537     private val approachPrecomposeSlotHandleMap = mutableScatterMapOf<Any?, PrecomposedSlotHandle>()
538 
539     // Slot ids _composed_ in post-lookahead. The valid slot ids are stored between 0 and
540     // currentApproachIndex - 1, beyond index currentApproachIndex are obsolete ids.
541     private val approachComposedSlotIds = mutableVectorOf<Any?>()
542 
543     /**
544      * `root.foldedChildren` list consist of:
545      * 1) all the active children (used during the last measure pass)
546      * 2) `reusableCount` nodes in the middle of the list which were active and stopped being used.
547      *    now we keep them (up to `maxCountOfSlotsToReuse`) in order to reuse next time we will need
548      *    to compose a new item
549      * 4) `precomposedCount` nodes in the end of the list which were precomposed and are waiting to
550      *    be used during the next measure passes.
551      */
552     private var reusableCount = 0
553     private var precomposedCount = 0
554 
555     override fun onReuse() {
556         markActiveNodesAsReused(deactivate = false)
557     }
558 
559     override fun onDeactivate() {
560         markActiveNodesAsReused(deactivate = true)
561     }
562 
563     override fun onRelease() {
564         disposeCurrentNodes()
565     }
566 
567     fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> {
568         makeSureStateIsConsistent()
569         val layoutState = root.layoutState
570         checkPrecondition(
571             layoutState == LayoutState.Measuring ||
572                 layoutState == LayoutState.LayingOut ||
573                 layoutState == LayoutState.LookaheadMeasuring ||
574                 layoutState == LayoutState.LookaheadLayingOut
575         ) {
576             "subcompose can only be used inside the measure or layout blocks"
577         }
578 
579         val node =
580             slotIdToNode.getOrPut(slotId) {
581                 val precomposed = precomposeMap.remove(slotId)
582                 if (precomposed != null) {
583                     @Suppress("ExceptionMessage") checkPrecondition(precomposedCount > 0)
584                     precomposedCount--
585                     precomposed
586                 } else {
587                     takeNodeFromReusables(slotId) ?: createNodeAt(currentIndex)
588                 }
589             }
590 
591         if (root.foldedChildren.getOrNull(currentIndex) !== node) {
592             // the node has a new index in the list
593             val itemIndex = root.foldedChildren.indexOf(node)
594             requirePrecondition(itemIndex >= currentIndex) {
595                 "Key \"$slotId\" was already used. If you are using LazyColumn/Row please make " +
596                     "sure you provide a unique key for each item."
597             }
598             if (currentIndex != itemIndex) {
599                 move(itemIndex, currentIndex)
600             }
601         }
602         currentIndex++
603 
604         subcompose(node, slotId, pausable = false, content)
605 
606         return if (layoutState == LayoutState.Measuring || layoutState == LayoutState.LayingOut) {
607             node.childMeasurables
608         } else {
609             node.childLookaheadMeasurables
610         }
611     }
612 
613     // This may be called in approach pass, if a node is only emitted in the approach pass, but
614     // not in the lookahead pass.
615     private fun subcompose(
616         node: LayoutNode,
617         slotId: Any?,
618         pausable: Boolean,
619         content: @Composable () -> Unit
620     ) {
621         val nodeState = nodeToNodeState.getOrPut(node) { NodeState(slotId, {}) }
622         val contentChanged = nodeState.content !== content
623         if (nodeState.pausedComposition != null) {
624             if (contentChanged) {
625                 // content did change so it is not safe to apply the current paused composition.
626                 nodeState.cancelPausedPrecomposition()
627             } else if (pausable) {
628                 // the paused composition is initialized and the content didn't change
629                 return
630             } else {
631                 // we can apply as we are still composing the same content.
632                 nodeState.applyPausedPrecomposition(shouldComplete = true)
633             }
634         }
635         val hasPendingChanges = nodeState.composition?.hasInvalidations ?: true
636         if (contentChanged || hasPendingChanges || nodeState.forceRecompose) {
637             nodeState.content = content
638             subcompose(node, nodeState, pausable)
639             nodeState.forceRecompose = false
640         }
641     }
642 
643     private val outOfFrameExecutor: OutOfFrameExecutor?
644         get() =
645             if (ComposeUiFlags.isOutOfFrameDeactivationEnabled) {
646                 root.requireOwner().outOfFrameExecutor
647             } else {
648                 null
649             }
650 
651     private fun subcompose(node: LayoutNode, nodeState: NodeState, pausable: Boolean) {
652         requirePrecondition(nodeState.pausedComposition == null) {
653             "new subcompose call while paused composition is still active"
654         }
655         Snapshot.withoutReadObservation {
656             ignoreRemeasureRequests {
657                 val existing = nodeState.composition
658                 val parentComposition =
659                     compositionContext
660                         ?: throwIllegalStateExceptionForNullCheck(
661                             "parent composition reference not set"
662                         )
663                 val composition =
664                     if (existing == null || existing.isDisposed) {
665                         if (pausable) {
666                             createPausableSubcomposition(node, parentComposition)
667                         } else {
668                             createSubcomposition(node, parentComposition)
669                         }
670                     } else {
671                         existing
672                     }
673                 nodeState.composition = composition
674                 val content = nodeState.content
675                 val composable: @Composable () -> Unit =
676                     if (outOfFrameExecutor != null) {
677                         nodeState.composedWithReusableContentHost = false
678                         content
679                     } else {
680                         nodeState.composedWithReusableContentHost = true
681                         { ReusableContentHost(nodeState.active, content) }
682                     }
683                 if (pausable) {
684                     composition as PausableComposition
685                     if (nodeState.forceReuse) {
686                         nodeState.pausedComposition =
687                             composition.setPausableContentWithReuse(composable)
688                     } else {
689                         nodeState.pausedComposition = composition.setPausableContent(composable)
690                     }
691                 } else {
692                     if (nodeState.forceReuse) {
693                         composition.setContentWithReuse(composable)
694                     } else {
695                         composition.setContent(composable)
696                     }
697                 }
698                 nodeState.forceReuse = false
699             }
700         }
701     }
702 
703     private fun getSlotIdAtIndex(foldedChildren: List<LayoutNode>, index: Int): Any? {
704         val node = foldedChildren[index]
705         return nodeToNodeState[node]!!.slotId
706     }
707 
708     fun disposeOrReuseStartingFromIndex(startIndex: Int) {
709         reusableCount = 0
710         val foldedChildren = root.foldedChildren
711         val lastReusableIndex = foldedChildren.size - precomposedCount - 1
712         var needApplyNotification = false
713         if (startIndex <= lastReusableIndex) {
714             // construct the set of available slot ids
715             reusableSlotIdsSet.clear()
716             for (i in startIndex..lastReusableIndex) {
717                 val slotId = getSlotIdAtIndex(foldedChildren, i)
718                 reusableSlotIdsSet.add(slotId)
719             }
720 
721             slotReusePolicy.getSlotsToRetain(reusableSlotIdsSet)
722             // iterating backwards so it is easier to remove items
723             var i = lastReusableIndex
724             val outOfFrameExecutor = outOfFrameExecutor
725             Snapshot.withoutReadObservation {
726                 while (i >= startIndex) {
727                     val node = foldedChildren[i]
728                     val nodeState = nodeToNodeState[node]!!
729                     val slotId = nodeState.slotId
730                     if (slotId in reusableSlotIdsSet) {
731                         reusableCount++
732                         if (nodeState.active) {
733                             node.resetLayoutState()
734                             if (outOfFrameExecutor != null) {
735                                 nodeState.deactivateOutOfFrame(outOfFrameExecutor)
736                             } else {
737                                 nodeState.active = false
738                                 if (nodeState.composedWithReusableContentHost) {
739                                     needApplyNotification = true
740                                 } else {
741                                     nodeState.composition?.deactivate()
742                                 }
743                             }
744                         }
745                     } else {
746                         ignoreRemeasureRequests {
747                             nodeToNodeState.remove(node)
748                             nodeState.composition?.dispose()
749                             root.removeAt(i, 1)
750                         }
751                     }
752                     // remove it from slotIdToNode so it is not considered active
753                     slotIdToNode.remove(slotId)
754                     i--
755                 }
756             }
757         }
758 
759         if (needApplyNotification) {
760             Snapshot.sendApplyNotifications()
761         }
762 
763         makeSureStateIsConsistent()
764     }
765 
766     private fun NodeState.deactivateOutOfFrame(executor: OutOfFrameExecutor) {
767         active = false
768         executor.schedule {
769             if (!active) {
770                 composition?.deactivate()
771             }
772         }
773     }
774 
775     private fun markActiveNodesAsReused(deactivate: Boolean) {
776         precomposedCount = 0
777         precomposeMap.clear()
778 
779         val foldedChildren = root.foldedChildren
780         val childCount = foldedChildren.size
781         if (reusableCount != childCount) {
782             reusableCount = childCount
783             val outOfFrameExecutor = outOfFrameExecutor
784             Snapshot.withoutReadObservation {
785                 for (i in 0 until childCount) {
786                     val node = foldedChildren[i]
787                     val nodeState = nodeToNodeState[node]
788                     if (nodeState != null && nodeState.active) {
789                         node.resetLayoutState()
790                         if (deactivate) {
791                             nodeState.composition?.deactivate()
792                             nodeState.activeState = mutableStateOf(false)
793                         } else {
794                             if (outOfFrameExecutor != null) {
795                                 nodeState.deactivateOutOfFrame(outOfFrameExecutor)
796                             } else {
797                                 nodeState.active = false
798                                 if (!nodeState.composedWithReusableContentHost) {
799                                     nodeState.composition?.deactivate()
800                                 }
801                             }
802                         }
803                         // create a new instance to avoid change notifications
804                         nodeState.slotId = ReusedSlotId
805                     }
806                 }
807             }
808             slotIdToNode.clear()
809         }
810 
811         makeSureStateIsConsistent()
812     }
813 
814     private fun disposeCurrentNodes() {
815         root.ignoreRemeasureRequests {
816             nodeToNodeState.forEachValue { it.composition?.dispose() }
817             root.removeAll()
818         }
819 
820         nodeToNodeState.clear()
821         slotIdToNode.clear()
822         precomposedCount = 0
823         reusableCount = 0
824         precomposeMap.clear()
825 
826         makeSureStateIsConsistent()
827     }
828 
829     fun makeSureStateIsConsistent() {
830         val childrenCount = root.foldedChildren.size
831         requirePrecondition(nodeToNodeState.size == childrenCount) {
832             "Inconsistency between the count of nodes tracked by the state " +
833                 "(${nodeToNodeState.size}) and the children count on the SubcomposeLayout" +
834                 " ($childrenCount). Are you trying to use the state of the" +
835                 " disposed SubcomposeLayout?"
836         }
837         requirePrecondition(childrenCount - reusableCount - precomposedCount >= 0) {
838             "Incorrect state. Total children $childrenCount. Reusable children " +
839                 "$reusableCount. Precomposed children $precomposedCount"
840         }
841         requirePrecondition(precomposeMap.size == precomposedCount) {
842             "Incorrect state. Precomposed children $precomposedCount. Map size " +
843                 "${precomposeMap.size}"
844         }
845     }
846 
847     private fun LayoutNode.resetLayoutState() {
848         measurePassDelegate.measuredByParent = UsageByParent.NotUsed
849         lookaheadPassDelegate?.let { it.measuredByParent = UsageByParent.NotUsed }
850     }
851 
852     private fun takeNodeFromReusables(slotId: Any?): LayoutNode? {
853         if (reusableCount == 0) {
854             return null
855         }
856         val foldedChildren = root.foldedChildren
857         val reusableNodesSectionEnd = foldedChildren.size - precomposedCount
858         val reusableNodesSectionStart = reusableNodesSectionEnd - reusableCount
859         var index = reusableNodesSectionEnd - 1
860         var chosenIndex = -1
861         // first try to find a node with exactly the same slotId
862         while (index >= reusableNodesSectionStart) {
863             if (getSlotIdAtIndex(foldedChildren, index) == slotId) {
864                 // we have a node with the same slotId
865                 chosenIndex = index
866                 break
867             } else {
868                 index--
869             }
870         }
871         if (chosenIndex == -1) {
872             // try to find a first compatible slotId from the end of the section
873             index = reusableNodesSectionEnd - 1
874             while (index >= reusableNodesSectionStart) {
875                 val node = foldedChildren[index]
876                 val nodeState = nodeToNodeState[node]!!
877                 if (
878                     nodeState.slotId === ReusedSlotId ||
879                         slotReusePolicy.areCompatible(slotId, nodeState.slotId)
880                 ) {
881                     nodeState.slotId = slotId
882                     chosenIndex = index
883                     break
884                 }
885                 index--
886             }
887         }
888         return if (chosenIndex == -1) {
889             // no compatible nodes found
890             null
891         } else {
892             if (index != reusableNodesSectionStart) {
893                 // we need to rearrange the items
894                 move(index, reusableNodesSectionStart, 1)
895             }
896             reusableCount--
897             val node = foldedChildren[reusableNodesSectionStart]
898             val nodeState = nodeToNodeState[node]!!
899             // create a new instance to avoid change notifications
900             nodeState.activeState = mutableStateOf(true)
901             nodeState.forceReuse = true
902             nodeState.forceRecompose = true
903             node
904         }
905     }
906 
907     fun createMeasurePolicy(
908         block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
909     ): MeasurePolicy {
910         return object : LayoutNode.NoIntrinsicsMeasurePolicy(error = NoIntrinsicsMessage) {
911             override fun MeasureScope.measure(
912                 measurables: List<Measurable>,
913                 constraints: Constraints
914             ): MeasureResult {
915                 scope.layoutDirection = layoutDirection
916                 scope.density = density
917                 scope.fontScale = fontScale
918                 if (!isLookingAhead && root.lookaheadRoot != null) {
919                     // Approach pass
920                     currentApproachIndex = 0
921                     val result = approachMeasureScope.block(constraints)
922                     val indexAfterMeasure = currentApproachIndex
923                     return createMeasureResult(result) {
924                         currentApproachIndex = indexAfterMeasure
925                         result.placeChildren()
926                         // dispose
927                         disposeUnusedSlotsInApproach()
928                     }
929                 } else {
930                     // Lookahead pass, or the main pass if not in a lookahead scope.
931                     currentIndex = 0
932                     val result = scope.block(constraints)
933                     val indexAfterMeasure = currentIndex
934                     return createMeasureResult(result) {
935                         currentIndex = indexAfterMeasure
936                         result.placeChildren()
937                         disposeOrReuseStartingFromIndex(currentIndex)
938                     }
939                 }
940             }
941         }
942     }
943 
944     private fun disposeUnusedSlotsInApproach() {
945         approachPrecomposeSlotHandleMap.removeIf { slotId, handle ->
946             val id = approachComposedSlotIds.indexOf(slotId)
947             if (id < 0 || id >= currentApproachIndex) {
948                 // Slot was not used in the latest pass of post-lookahead.
949                 handle.dispose()
950                 true
951             } else {
952                 false
953             }
954         }
955     }
956 
957     private inline fun createMeasureResult(
958         result: MeasureResult,
959         crossinline placeChildrenBlock: () -> Unit
960     ) =
961         object : MeasureResult by result {
962             override fun placeChildren() {
963                 placeChildrenBlock()
964             }
965         }
966 
967     private val NoIntrinsicsMessage =
968         "Asking for intrinsic measurements of SubcomposeLayout " +
969             "layouts is not supported. This includes components that are built on top of " +
970             "SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc. To mitigate " +
971             "this:\n" +
972             "- if intrinsic measurements are used to achieve 'match parent' sizing, consider " +
973             "replacing the parent of the component with a custom layout which controls the order in " +
974             "which children are measured, making intrinsic measurement not needed\n" +
975             "- adding a size modifier to the component, in order to fast return the queried " +
976             "intrinsic measurement."
977 
978     fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle {
979         precompose(slotId, content, pausable = false)
980         return createPrecomposedSlotHandle(slotId)
981     }
982 
983     private fun precompose(slotId: Any?, content: @Composable () -> Unit, pausable: Boolean) {
984         if (!root.isAttached) {
985             return
986         }
987         makeSureStateIsConsistent()
988         if (!slotIdToNode.containsKey(slotId)) {
989             // Yield ownership of PrecomposedHandle from approach to the caller of precompose
990             approachPrecomposeSlotHandleMap.remove(slotId)
991             val node =
992                 precomposeMap.getOrPut(slotId) {
993                     val reusedNode = takeNodeFromReusables(slotId)
994                     if (reusedNode != null) {
995                         // now move this node to the end where we keep precomposed items
996                         val nodeIndex = root.foldedChildren.indexOf(reusedNode)
997                         move(nodeIndex, root.foldedChildren.size, 1)
998                         precomposedCount++
999                         reusedNode
1000                     } else {
1001                         createNodeAt(root.foldedChildren.size).also { precomposedCount++ }
1002                     }
1003                 }
1004             subcompose(node, slotId, pausable = pausable, content)
1005         }
1006     }
1007 
1008     private fun NodeState.cancelPausedPrecomposition() {
1009         pausedComposition?.let {
1010             it.cancel()
1011             pausedComposition = null
1012             composition?.dispose()
1013             composition = null
1014         }
1015     }
1016 
1017     private fun disposePrecomposedSlot(slotId: Any?) {
1018         makeSureStateIsConsistent()
1019         val node = precomposeMap.remove(slotId)
1020         if (node != null) {
1021             checkPrecondition(precomposedCount > 0) { "No pre-composed items to dispose" }
1022             val itemIndex = root.foldedChildren.indexOf(node)
1023             checkPrecondition(itemIndex >= root.foldedChildren.size - precomposedCount) {
1024                 "Item is not in pre-composed item range"
1025             }
1026             // move this item into the reusable section
1027             reusableCount++
1028             precomposedCount--
1029 
1030             nodeToNodeState[node]?.cancelPausedPrecomposition()
1031 
1032             val reusableStart = root.foldedChildren.size - precomposedCount - reusableCount
1033             move(itemIndex, reusableStart, 1)
1034             disposeOrReuseStartingFromIndex(reusableStart)
1035         }
1036     }
1037 
1038     private fun createPrecomposedSlotHandle(slotId: Any?): PrecomposedSlotHandle {
1039         if (!root.isAttached) {
1040             return object : PrecomposedSlotHandle {
1041                 override fun dispose() {}
1042             }
1043         }
1044         return object : PrecomposedSlotHandle {
1045             // Saves indices of placeables that have been premeasured in this handle
1046             val hasPremeasured = mutableIntSetOf()
1047 
1048             override fun dispose() {
1049                 disposePrecomposedSlot(slotId)
1050             }
1051 
1052             override val placeablesCount: Int
1053                 get() = precomposeMap[slotId]?.children?.size ?: 0
1054 
1055             override fun premeasure(index: Int, constraints: Constraints) {
1056                 val node = precomposeMap[slotId]
1057                 if (node != null && node.isAttached) {
1058                     val size = node.children.size
1059                     if (index < 0 || index >= size) {
1060                         throwIndexOutOfBoundsException(
1061                             "Index ($index) is out of bound of [0, $size)"
1062                         )
1063                     }
1064                     requirePrecondition(!node.isPlaced) {
1065                         "Pre-measure called on node that is not placed"
1066                     }
1067                     root.ignoreRemeasureRequests {
1068                         node.requireOwner().measureAndLayout(node.children[index], constraints)
1069                     }
1070                     hasPremeasured.add(index)
1071                 }
1072             }
1073 
1074             override fun traverseDescendants(
1075                 key: Any?,
1076                 block: (TraversableNode) -> TraverseDescendantsAction
1077             ) {
1078                 precomposeMap[slotId]?.nodes?.head?.traverseDescendants(key, block)
1079             }
1080 
1081             override fun getSize(index: Int): IntSize {
1082                 val node = precomposeMap[slotId]
1083                 if (node != null && node.isAttached) {
1084                     val size = node.children.size
1085                     if (index < 0 || index >= size) {
1086                         throwIndexOutOfBoundsException(
1087                             "Index ($index) is out of bound of [0, $size)"
1088                         )
1089                     }
1090 
1091                     if (hasPremeasured.contains(index)) {
1092                         return IntSize(node.children[index].width, node.children[index].height)
1093                     }
1094                 }
1095                 return IntSize.Zero
1096             }
1097         }
1098     }
1099 
1100     fun precomposePaused(slotId: Any?, content: @Composable () -> Unit): PausedPrecomposition {
1101         if (!root.isAttached) {
1102             return object : PausedPrecompositionImpl {
1103                 override val isComplete: Boolean = true
1104 
1105                 override fun resume(shouldPause: ShouldPauseCallback) = true
1106 
1107                 override fun apply() = createPrecomposedSlotHandle(slotId)
1108 
1109                 override fun cancel() {}
1110             }
1111         }
1112         precompose(slotId, content, pausable = true)
1113         return object : PausedPrecompositionImpl {
1114             override fun cancel() {
1115                 if (nodeState?.pausedComposition != null) {
1116                     // only dispose if the paused composition is still waiting to be applied
1117                     disposePrecomposedSlot(slotId)
1118                 }
1119             }
1120 
1121             private val nodeState: NodeState?
1122                 get() = precomposeMap[slotId]?.let { nodeToNodeState[it] }
1123 
1124             override val isComplete: Boolean
1125                 get() = nodeState?.pausedComposition?.isComplete ?: true
1126 
1127             override fun resume(shouldPause: ShouldPauseCallback): Boolean {
1128                 val pausedComposition = nodeState?.pausedComposition
1129                 return if (pausedComposition != null && !pausedComposition.isComplete) {
1130                     Snapshot.withoutReadObservation {
1131                         ignoreRemeasureRequests { pausedComposition.resume(shouldPause) }
1132                     }
1133                 } else {
1134                     true
1135                 }
1136             }
1137 
1138             override fun apply(): PrecomposedSlotHandle {
1139                 nodeState?.applyPausedPrecomposition(shouldComplete = false)
1140                 return createPrecomposedSlotHandle(slotId)
1141             }
1142         }
1143     }
1144 
1145     fun forceRecomposeChildren() {
1146         val childCount = root.foldedChildren.size
1147         if (reusableCount != childCount) {
1148             // only invalidate children if there are any non-reused ones
1149             // in other cases, all of them are going to be invalidated later anyways
1150             nodeToNodeState.forEachValue { nodeState -> nodeState.forceRecompose = true }
1151 
1152             if (root.lookaheadRoot != null) {
1153                 // If the SubcomposeLayout is in a LookaheadScope, request for a lookahead measure
1154                 // so that lookahead gets triggered again to recompose children.
1155                 if (!root.lookaheadMeasurePending) {
1156                     root.requestLookaheadRemeasure()
1157                 }
1158             } else {
1159                 if (!root.measurePending) {
1160                     root.requestRemeasure()
1161                 }
1162             }
1163         }
1164     }
1165 
1166     private fun createNodeAt(index: Int) =
1167         LayoutNode(
1168                 isVirtual = true,
1169             )
1170             .also { node -> ignoreRemeasureRequests { root.insertAt(index, node) } }
1171 
1172     private fun move(from: Int, to: Int, count: Int = 1) {
1173         ignoreRemeasureRequests { root.move(from, to, count) }
1174     }
1175 
1176     private inline fun <T> ignoreRemeasureRequests(block: () -> T): T =
1177         root.ignoreRemeasureRequests(block)
1178 
1179     private fun NodeState.applyPausedPrecomposition(shouldComplete: Boolean) {
1180         val pausedComposition = pausedComposition
1181         if (pausedComposition != null) {
1182             Snapshot.withoutReadObservation {
1183                 ignoreRemeasureRequests {
1184                     if (shouldComplete) {
1185                         while (!pausedComposition.isComplete) {
1186                             pausedComposition.resume { false }
1187                         }
1188                     }
1189                     pausedComposition.apply()
1190                     this.pausedComposition = null
1191                 }
1192             }
1193         }
1194     }
1195 
1196     private class NodeState(
1197         var slotId: Any?,
1198         var content: @Composable () -> Unit,
1199         var composition: ReusableComposition? = null
1200     ) {
1201         var forceRecompose = false
1202         var forceReuse = false
1203         var pausedComposition: PausedComposition? = null
1204         var activeState = mutableStateOf(true)
1205         var composedWithReusableContentHost = false
1206         var active: Boolean
1207             get() = activeState.value
1208             set(value) {
1209                 activeState.value = value
1210             }
1211     }
1212 
1213     private inner class Scope : SubcomposeMeasureScope {
1214         // MeasureScope delegation
1215         override var layoutDirection: LayoutDirection = LayoutDirection.Rtl
1216         override var density: Float = 0f
1217         override var fontScale: Float = 0f
1218         override val isLookingAhead: Boolean
1219             get() =
1220                 root.layoutState == LayoutState.LookaheadLayingOut ||
1221                     root.layoutState == LayoutState.LookaheadMeasuring
1222 
1223         override fun subcompose(slotId: Any?, content: @Composable () -> Unit) =
1224             this@LayoutNodeSubcompositionsState.subcompose(slotId, content)
1225 
1226         override fun layout(
1227             width: Int,
1228             height: Int,
1229             alignmentLines: Map<AlignmentLine, Int>,
1230             rulers: (RulerScope.() -> Unit)?,
1231             placementBlock: Placeable.PlacementScope.() -> Unit
1232         ): MeasureResult {
1233             checkMeasuredSize(width, height)
1234             return object : MeasureResult {
1235                 override val width: Int
1236                     get() = width
1237 
1238                 override val height: Int
1239                     get() = height
1240 
1241                 override val alignmentLines: Map<AlignmentLine, Int>
1242                     get() = alignmentLines
1243 
1244                 override val rulers: (RulerScope.() -> Unit)?
1245                     get() = rulers
1246 
1247                 override fun placeChildren() {
1248                     if (isLookingAhead) {
1249                         val delegate = root.innerCoordinator.lookaheadDelegate
1250                         if (delegate != null) {
1251                             delegate.placementScope.placementBlock()
1252                             return
1253                         }
1254                     }
1255                     root.innerCoordinator.placementScope.placementBlock()
1256                 }
1257             }
1258         }
1259     }
1260 
1261     private inner class ApproachMeasureScopeImpl : SubcomposeMeasureScope, MeasureScope by scope {
1262         /**
1263          * This function retrieves [Measurable]s created for [slotId] based on the subcomposition
1264          * that happened in the lookahead pass. If [slotId] was not subcomposed in the lookahead
1265          * pass, [subcompose] will return an [emptyList].
1266          */
1267         override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> {
1268             val nodeInSlot = slotIdToNode[slotId]
1269             if (nodeInSlot != null && root.foldedChildren.indexOf(nodeInSlot) < currentIndex) {
1270                 // Check that the node has been composed in lookahead. Otherwise, we need to
1271                 // compose the node in approach pass via approachSubcompose.
1272                 return nodeInSlot.childMeasurables
1273             } else {
1274                 return approachSubcompose(slotId, content)
1275             }
1276         }
1277     }
1278 
1279     private fun approachSubcompose(
1280         slotId: Any?,
1281         content: @Composable () -> Unit
1282     ): List<Measurable> {
1283         requirePrecondition(approachComposedSlotIds.size >= currentApproachIndex) {
1284             "Error: currentApproachIndex cannot be greater than the size of the" +
1285                 "approachComposedSlotIds list."
1286         }
1287         if (approachComposedSlotIds.size == currentApproachIndex) {
1288             approachComposedSlotIds.add(slotId)
1289         } else {
1290             approachComposedSlotIds[currentApproachIndex] = slotId
1291         }
1292         currentApproachIndex++
1293         if (!precomposeMap.contains(slotId)) {
1294             // Not composed yet
1295             precompose(slotId, content).also { approachPrecomposeSlotHandleMap[slotId] = it }
1296             if (root.layoutState == LayoutState.LayingOut) {
1297                 root.requestLookaheadRelayout(true)
1298             } else {
1299                 root.requestLookaheadRemeasure(true)
1300             }
1301         } else {
1302             // Re-subcompose if needed based on forceRecompose
1303             val node = precomposeMap[slotId]
1304             val nodeState = node?.let { nodeToNodeState[it] }
1305             if (nodeState?.forceRecompose == true) {
1306                 subcompose(node, slotId, pausable = false, content)
1307             }
1308         }
1309 
1310         return precomposeMap[slotId]?.run {
1311             measurePassDelegate.childDelegates.also {
1312                 it.fastForEach { delegate -> delegate.markDetachedFromParentLookaheadPass() }
1313             }
1314         } ?: emptyList()
1315     }
1316 }
1317 
1318 private val ReusedSlotId =
1319     object {
toStringnull1320         override fun toString(): String = "ReusedSlotId"
1321     }
1322 
1323 private class FixedCountSubcomposeSlotReusePolicy(private val maxSlotsToRetainForReuse: Int) :
1324     SubcomposeSlotReusePolicy {
1325 
1326     override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) {
1327         if (slotIds.size > maxSlotsToRetainForReuse) {
1328             slotIds.trimToSize(maxSlotsToRetainForReuse)
1329         }
1330     }
1331 
1332     override fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean = true
1333 }
1334 
1335 private object NoOpSubcomposeSlotReusePolicy : SubcomposeSlotReusePolicy {
getSlotsToRetainnull1336     override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) {
1337         slotIds.clear()
1338     }
1339 
areCompatiblenull1340     override fun areCompatible(slotId: Any?, reusableSlotId: Any?) = false
1341 }
1342 
1343 private interface PausedPrecompositionImpl : PausedPrecomposition
1344