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