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
}
/**