Add precomposing logic for SubcomposeLayout

This will allow to add the prefetching logic during the scroll of LazyColumn/Row

Relnote: Introduces ability to hoist the SubcomposeLayout state which allows you to precompose the content into a requires slotId which would make the next measure pass faster as once we try to subcompose with the given slotId next time there will be no composition needed.
Bug: 184940225
Bug: 185784787
Test: new tests in SubcomposeLayoutTest
Change-Id: I425806be5ec3e36337e04558e6621fbe515b7cd8
diff --git a/compose/ui/ui/api/1.0.0-beta07.txt b/compose/ui/ui/api/1.0.0-beta07.txt
index ac74859..e353e8d 100644
--- a/compose/ui/ui/api/1.0.0-beta07.txt
+++ b/compose/ui/ui/api/1.0.0-beta07.txt
@@ -1734,6 +1734,16 @@
 
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+  }
+
+  public final class SubcomposeLayoutState {
+    ctor public SubcomposeLayoutState();
+    method public androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle precompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public static interface SubcomposeLayoutState.PrecomposedSlotHandle {
+    method public void dispose();
   }
 
   public interface SubcomposeMeasureScope extends androidx.compose.ui.layout.MeasureScope {
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index ac74859..e353e8d 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1734,6 +1734,16 @@
 
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+  }
+
+  public final class SubcomposeLayoutState {
+    ctor public SubcomposeLayoutState();
+    method public androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle precompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public static interface SubcomposeLayoutState.PrecomposedSlotHandle {
+    method public void dispose();
   }
 
   public interface SubcomposeMeasureScope extends androidx.compose.ui.layout.MeasureScope {
diff --git a/compose/ui/ui/api/public_plus_experimental_1.0.0-beta07.txt b/compose/ui/ui/api/public_plus_experimental_1.0.0-beta07.txt
index 86f417b..73106b6c 100644
--- a/compose/ui/ui/api/public_plus_experimental_1.0.0-beta07.txt
+++ b/compose/ui/ui/api/public_plus_experimental_1.0.0-beta07.txt
@@ -1838,6 +1838,16 @@
 
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+  }
+
+  public final class SubcomposeLayoutState {
+    ctor public SubcomposeLayoutState();
+    method public androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle precompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public static interface SubcomposeLayoutState.PrecomposedSlotHandle {
+    method public void dispose();
   }
 
   public interface SubcomposeMeasureScope extends androidx.compose.ui.layout.MeasureScope {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 86f417b..73106b6c 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1838,6 +1838,16 @@
 
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+  }
+
+  public final class SubcomposeLayoutState {
+    ctor public SubcomposeLayoutState();
+    method public androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle precompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public static interface SubcomposeLayoutState.PrecomposedSlotHandle {
+    method public void dispose();
   }
 
   public interface SubcomposeMeasureScope extends androidx.compose.ui.layout.MeasureScope {
diff --git a/compose/ui/ui/api/restricted_1.0.0-beta07.txt b/compose/ui/ui/api/restricted_1.0.0-beta07.txt
index d741df2..a99eda6 100644
--- a/compose/ui/ui/api/restricted_1.0.0-beta07.txt
+++ b/compose/ui/ui/api/restricted_1.0.0-beta07.txt
@@ -1735,6 +1735,16 @@
 
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+  }
+
+  public final class SubcomposeLayoutState {
+    ctor public SubcomposeLayoutState();
+    method public androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle precompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public static interface SubcomposeLayoutState.PrecomposedSlotHandle {
+    method public void dispose();
   }
 
   public interface SubcomposeMeasureScope extends androidx.compose.ui.layout.MeasureScope {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index d741df2..a99eda6 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1735,6 +1735,16 @@
 
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+  }
+
+  public final class SubcomposeLayoutState {
+    ctor public SubcomposeLayoutState();
+    method public androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle precompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public static interface SubcomposeLayoutState.PrecomposedSlotHandle {
+    method public void dispose();
   }
 
   public interface SubcomposeMeasureScope extends androidx.compose.ui.layout.MeasureScope {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index 22e4869..e7e9b98 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -20,6 +20,7 @@
 import android.widget.FrameLayout
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -66,6 +67,7 @@
 
     @get:Rule
     val rule = createAndroidComposeRule<TestActivity>()
+
     @get:Rule
     val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
 
@@ -555,6 +557,271 @@
             stateUsedLatch.await(1, TimeUnit.SECONDS)
         )
     }
+
+    @Test
+    fun precompose() {
+        val addSlot = mutableStateOf(false)
+        var composingCounter = 0
+        var composedDuringMeasure = false
+        val state = SubcomposeLayoutState()
+        val content: @Composable () -> Unit = {
+            composingCounter++
+        }
+
+        rule.setContent {
+            SubcomposeLayout(state) {
+                if (addSlot.value) {
+                    composedDuringMeasure = true
+                    subcompose(Unit, content)
+                }
+                layout(10, 10) {}
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composingCounter).isEqualTo(0)
+            state.precompose(Unit, content)
+        }
+
+        rule.runOnIdle {
+            assertThat(composingCounter).isEqualTo(1)
+
+            assertThat(composedDuringMeasure).isFalse()
+            addSlot.value = true
+        }
+
+        rule.runOnIdle {
+            assertThat(composedDuringMeasure).isTrue()
+            assertThat(composingCounter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun disposePrecomposedItem() {
+        var composed = false
+        var disposed = false
+        val state = SubcomposeLayoutState()
+
+        rule.setContent {
+            SubcomposeLayout(state) {
+                layout(10, 10) {}
+            }
+        }
+
+        val slot = rule.runOnIdle {
+            state.precompose(Unit) {
+                DisposableEffect(Unit) {
+                    composed = true
+                    onDispose {
+                        disposed = true
+                    }
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composed).isTrue()
+            assertThat(disposed).isFalse()
+
+            slot.dispose()
+        }
+
+        rule.runOnIdle {
+            assertThat(disposed).isTrue()
+        }
+    }
+
+    @Test
+    fun composeItemRegularlyAfterDisposingPrecomposedItem() {
+        val addSlot = mutableStateOf(false)
+        var composingCounter = 0
+        var enterCounter = 0
+        var exitCounter = 0
+        val state = SubcomposeLayoutState()
+        val content: @Composable () -> Unit = @Composable {
+            composingCounter++
+            DisposableEffect(Unit) {
+                enterCounter++
+                onDispose {
+                    exitCounter++
+                }
+            }
+        }
+
+        rule.setContent {
+            SubcomposeLayout(state) {
+                if (addSlot.value) {
+                    subcompose(Unit, content)
+                }
+                layout(10, 10) {}
+            }
+        }
+
+        val slot = rule.runOnIdle {
+            state.precompose(Unit, content)
+        }
+
+        rule.runOnIdle {
+            slot.dispose()
+        }
+
+        rule.runOnIdle {
+            assertThat(composingCounter).isEqualTo(1)
+            assertThat(enterCounter).isEqualTo(1)
+            assertThat(exitCounter).isEqualTo(1)
+
+            addSlot.value = true
+        }
+
+        rule.runOnIdle {
+            assertThat(composingCounter).isEqualTo(2)
+            assertThat(enterCounter).isEqualTo(2)
+            assertThat(exitCounter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun precomposeTwoItems() {
+        val addSlots = mutableStateOf(false)
+        var composing1Counter = 0
+        var composing2Counter = 0
+        val state = SubcomposeLayoutState()
+        val content1: @Composable () -> Unit = {
+            composing1Counter++
+        }
+        val content2: @Composable () -> Unit = {
+            composing2Counter++
+        }
+
+        rule.setContent {
+            SubcomposeLayout(state) {
+                subcompose(0) { }
+                if (addSlots.value) {
+                    subcompose(1, content1)
+                    subcompose(2, content2)
+                }
+                subcompose(3) { }
+                layout(10, 10) {}
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(composing1Counter).isEqualTo(0)
+            assertThat(composing2Counter).isEqualTo(0)
+            state.precompose(1, content1)
+            state.precompose(2, content2)
+        }
+
+        rule.runOnIdle {
+            assertThat(composing1Counter).isEqualTo(1)
+            assertThat(composing2Counter).isEqualTo(1)
+            addSlots.value = true
+        }
+
+        rule.runOnIdle {
+            assertThat(composing1Counter).isEqualTo(1)
+            assertThat(composing2Counter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun precomposedItemDisposedWhenSubcomposeLayoutIsDisposed() {
+        val emitLayout = mutableStateOf(true)
+        var enterCounter = 0
+        var exitCounter = 0
+        val state = SubcomposeLayoutState()
+        val content: @Composable () -> Unit = @Composable {
+            DisposableEffect(Unit) {
+                enterCounter++
+                onDispose {
+                    exitCounter++
+                }
+            }
+        }
+
+        rule.setContent {
+            if (emitLayout.value) {
+                SubcomposeLayout(state) {
+                    layout(10, 10) {}
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            state.precompose(Unit, content)
+        }
+
+        rule.runOnIdle {
+            assertThat(enterCounter).isEqualTo(1)
+            assertThat(exitCounter).isEqualTo(0)
+            emitLayout.value = false
+        }
+
+        rule.runOnIdle {
+            assertThat(exitCounter).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun precomposeIsNotTriggeringParentRemeasure() {
+        val state = SubcomposeLayoutState()
+
+        var measureCount = 0
+        var layoutCount = 0
+
+        rule.setContent {
+            SubcomposeLayout(state) {
+                measureCount++
+                layout(10, 10) {
+                    layoutCount++
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(measureCount).isEqualTo(1)
+            assertThat(layoutCount).isEqualTo(1)
+            state.precompose(Unit) {
+                Box(Modifier.fillMaxSize())
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(measureCount).isEqualTo(1)
+            assertThat(layoutCount).isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun precomposedItemDisposalIsNotTriggeringParentRemeasure() {
+        val state = SubcomposeLayoutState()
+
+        var measureCount = 0
+        var layoutCount = 0
+
+        rule.setContent {
+            SubcomposeLayout(state) {
+                measureCount++
+                layout(10, 10) {
+                    layoutCount++
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(measureCount).isEqualTo(1)
+            assertThat(layoutCount).isEqualTo(1)
+            val handle = state.precompose(Unit) {
+                Box(Modifier.fillMaxSize())
+            }
+            handle.dispose()
+        }
+
+        rule.runOnIdle {
+            assertThat(measureCount).isEqualTo(1)
+            assertThat(layoutCount).isEqualTo(1)
+        }
+    }
 }
 
 fun ImageBitmap.assertCenterPixelColor(expectedColor: Color) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 6c6820c1..2977afe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -60,7 +60,39 @@
     modifier: Modifier = Modifier,
     measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
 ) {
-    val state = remember { SubcomposeLayoutState() }
+    SubcomposeLayout(
+        state = remember { SubcomposeLayoutState() },
+        modifier = modifier,
+        measurePolicy = measurePolicy
+    )
+}
+
+/**
+ * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
+ * for example to use the values calculated during the measurement as params for the composition
+ * of the children.
+ *
+ * Possible use cases:
+ * * You need to know the constraints passed by the parent during the composition and can't solve
+ * your use case with just custom [Layout] or [LayoutModifier].
+ * See [androidx.compose.foundation.layout.BoxWithConstraints].
+ * * You want to use the size of one child during the composition of the second child.
+ * * You want to compose your items lazily based on the available size. For example you have a
+ * list of 100 items and instead of composing all of them you only compose the ones which are
+ * currently visible(say 5 of them) and compose next items when the component is scrolled.
+ *
+ * @sample androidx.compose.ui.samples.SubcomposeLayoutSample
+ *
+ * @param state the state object to be used by the layout.
+ * @param modifier [Modifier] to apply for the layout.
+ * @param measurePolicy Measure policy which provides ability to subcompose during the measuring.
+ */
+@Composable
+fun SubcomposeLayout(
+    state: SubcomposeLayoutState,
+    modifier: Modifier = Modifier,
+    measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
+) {
     state.compositionContext = rememberCompositionContext()
     DisposableEffect(state) {
         onDispose {
@@ -105,32 +137,45 @@
 /**
  * Contains the state used by [SubcomposeLayout].
  */
-internal class SubcomposeLayoutState {
+class SubcomposeLayoutState {
     internal var compositionContext: CompositionContext? = null
 
     // Pre-allocated lambdas to update LayoutNode
-    internal val setRoot: LayoutNode.() -> Unit = { root = this }
+    internal val setRoot: LayoutNode.() -> Unit = { _root = this }
     internal val setMeasurePolicy:
         LayoutNode.(SubcomposeMeasureScope.(Constraints) -> MeasureResult) -> Unit =
             { measurePolicy = createMeasurePolicy(it) }
 
     // inner state
-    private var root: LayoutNode? = null
+    private var _root: LayoutNode? = null
+    private val root: LayoutNode get() = requireNotNull(_root)
     private var currentIndex = 0
     private val nodeToNodeState = mutableMapOf<LayoutNode, NodeState>()
     private val slodIdToNode = mutableMapOf<Any?, LayoutNode>()
     private val scope = Scope()
+    private val precomposeMap = mutableMapOf<Any?, LayoutNode>()
+
+    /**
+     * `root.foldedChildren` list contains all the active children (used during the last measure
+     * pass) plus n = `precomposedCount` nodes in the end of the list which were precomposed and
+     * are waiting to be used during the next measure passes.
+     */
+    private var precomposedCount = 0
 
     internal fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> {
-        val root = root!!
         val layoutState = root.layoutState
         check(layoutState == LayoutState.Measuring || layoutState == LayoutState.LayingOut) {
             "subcompose can only be used inside the measure or layout blocks"
         }
 
         val node = slodIdToNode.getOrPut(slotId) {
-            LayoutNode(isVirtual = true).also {
-                root.insertAt(currentIndex, it)
+            val precomposed = precomposeMap.remove(slotId)
+            if (precomposed != null) {
+                check(precomposedCount > 0)
+                precomposedCount--
+                precomposed
+            } else {
+                createNodeAt(currentIndex)
             }
         }
 
@@ -141,10 +186,15 @@
             )
         }
         if (currentIndex != itemIndex) {
-            root.move(itemIndex, currentIndex, 1)
+            move(itemIndex, currentIndex)
         }
         currentIndex++
 
+        subcompose(node, slotId, content)
+        return node.children
+    }
+
+    private fun subcompose(node: LayoutNode, slotId: Any?, content: @Composable () -> Unit) {
         val nodeState = nodeToNodeState.getOrPut(node) {
             NodeState(slotId, {})
         }
@@ -153,21 +203,22 @@
             nodeState.content = content
             subcompose(node, nodeState)
         }
-        return node.children
     }
 
     private fun subcompose(node: LayoutNode, nodeState: NodeState) {
         node.withNoSnapshotReadObservation {
-            val content = nodeState.content
-            nodeState.composition = subcomposeInto(
-                existing = nodeState.composition,
-                container = node,
-                parent = compositionContext ?: error("parent composition reference not set"),
-                // Do not optimize this by passing nodeState.content directly; the additional
-                // composable function call from the lambda expression affects the scope of
-                // recomposition and recomposition of siblings.
-                composable = { content() }
-            )
+            ignoreRemeasureRequests {
+                val content = nodeState.content
+                nodeState.composition = subcomposeInto(
+                    existing = nodeState.composition,
+                    container = node,
+                    parent = compositionContext ?: error("parent composition reference not set"),
+                    // Do not optimize this by passing nodeState.content directly; the additional
+                    // composable function call from the lambda expression affects the scope of
+                    // recomposition and recomposition of siblings.
+                    composable = { content() }
+                )
+            }
         }
     }
 
@@ -188,14 +239,20 @@
     }
 
     private fun disposeAfterIndex(currentIndex: Int) {
-        val root = root!!
-        for (i in currentIndex until root.foldedChildren.size) {
-            val node = root.foldedChildren[i]
-            val nodeState = nodeToNodeState.remove(node)!!
-            nodeState.composition!!.dispose()
-            slodIdToNode.remove(nodeState.slotId)
+        // this size is not including precomposed items in the end of the list
+        val activeChildrenSize = root.foldedChildren.size - precomposedCount
+        ignoreRemeasureRequests {
+            for (i in currentIndex until activeChildrenSize) {
+                disposeNode(root.foldedChildren[i])
+            }
+            root.removeAt(currentIndex, activeChildrenSize - currentIndex)
         }
-        root.removeAt(currentIndex, root.foldedChildren.size - currentIndex)
+    }
+
+    private fun disposeNode(node: LayoutNode) {
+        val nodeState = nodeToNodeState.remove(node)!!
+        nodeState.composition!!.dispose()
+        slodIdToNode.remove(nodeState.slotId)
     }
 
     private fun createMeasurePolicy(
@@ -238,6 +295,60 @@
         slodIdToNode.clear()
     }
 
+    /**
+     * Composes the content for the given [slotId]. This makes the next scope.subcompose(slotId)
+     * call during the measure pass faster as the content is already composed.
+     *
+     * If the [slotId] was precomposed already but after the future calculations ended up to not be
+     * needed anymore (meaning this slotId is not going to be used during the measure pass
+     * anytime soon) you can use [PrecomposedSlotHandle.dispose] on a returned object to dispose the
+     * content.
+     *
+     * @param slotId unique id which represents the slot we are composing into.
+     * @param content the composable content which defines the slot.
+     * @return [PrecomposedSlotHandle] instance which allows you to dispose the content.
+     */
+    fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle {
+        if (!slodIdToNode.containsKey(slotId)) {
+            val node = precomposeMap.getOrPut(slotId) {
+                createNodeAt(root.foldedChildren.size).also {
+                    precomposedCount++
+                }
+            }
+            subcompose(node, slotId, content)
+        }
+        return object : PrecomposedSlotHandle {
+            override fun dispose() {
+                val node = precomposeMap.remove(slotId)
+                if (node != null) {
+                    ignoreRemeasureRequests {
+                        disposeNode(node)
+                        val itemIndex = root.foldedChildren.indexOf(node)
+                        check(itemIndex != -1)
+                        root.removeAt(itemIndex, 1)
+                        check(precomposedCount > 0)
+                        precomposedCount--
+                    }
+                }
+            }
+        }
+    }
+
+    private fun createNodeAt(index: Int) = LayoutNode(isVirtual = true).also {
+        ignoreRemeasureRequests {
+            root.insertAt(index, it)
+        }
+    }
+
+    private fun move(from: Int, to: Int, count: Int = 1) {
+        ignoreRemeasureRequests {
+            root.move(from, to, count)
+        }
+    }
+
+    private inline fun ignoreRemeasureRequests(block: () -> Unit) =
+        root.ignoreRemeasureRequests(block)
+
     private class NodeState(
         val slotId: Any?,
         var content: @Composable () -> Unit,
@@ -253,4 +364,22 @@
         override fun subcompose(slotId: Any?, content: @Composable () -> Unit) =
             [email protected](slotId, content)
     }
+
+    /**
+     * Instance of this interface is returned by [precompose] function.
+     */
+    interface PrecomposedSlotHandle {
+
+        /**
+         * This function allows to dispose the content for the slot which was precomposed
+         * previously via [precompose].
+         *
+         * If this slot was already used during the regular measure pass via
+         * [SubcomposeMeasureScope.subcompose] this function will do nothing.
+         *
+         * This could be useful if after the future calculations this item is not anymore expected to
+         * be used during the measure pass anytime soon.
+         */
+        fun dispose()
+    }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index c381939..ce522152 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -145,7 +145,7 @@
 
     /**
      * The parent node in the LayoutNode hierarchy. This is `null` when the [LayoutNode]
-     * is not attached attached to a hierarchy or is the root of the hierarchy.
+     * is not attached to a hierarchy or is the root of the hierarchy.
      */
     internal var parent: LayoutNode? = null
         get() {
@@ -185,6 +185,11 @@
     private var wrapperCache = mutableVectorOf<DelegatingLayoutNodeWrapper<*>>()
 
     /**
+     * [requestRemeasure] calls will be ignored while this flag is true.
+     */
+    private var ignoreRemeasureRequests = false
+
+    /**
      * Inserts a child [LayoutNode] at a particular index. If this LayoutNode [owner] is not `null`
      * then [instance] will become [attach]ed also. [instance] must have a `null` [parent].
      */
@@ -1036,7 +1041,15 @@
      * Used to request a new measurement + layout pass from the owner.
      */
     internal fun requestRemeasure() {
-        owner?.onRequestMeasure(this)
+        if (!ignoreRemeasureRequests) {
+            owner?.onRequestMeasure(this)
+        }
+    }
+
+    internal inline fun ignoreRemeasureRequests(block: () -> Unit) {
+        ignoreRemeasureRequests = true
+        block()
+        ignoreRemeasureRequests = false
     }
 
     /**