Refactor internal LazyLayout out of lazy lists
Relnote: n/a
Bug: 166591700
Test: ran tests in foundation.lazy
Change-Id: Ia8ac4d18a9a33a41a96f9ca63a4747d787ec7d3e
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index f41809d..110a916 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,6 +1,10 @@
// Baseline format: 1.0
RemovedClass: androidx.compose.foundation.gestures.RelativeVelocityTrackerKt:
Removed class androidx.compose.foundation.gestures.RelativeVelocityTrackerKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemContentFactoryKt:
+ Removed class androidx.compose.foundation.lazy.LazyListItemContentFactoryKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListPrefetcher_androidKt:
+ Removed class androidx.compose.foundation.lazy.LazyListPrefetcher_androidKt
RemovedMethod: androidx.compose.foundation.ImageKt#Image(androidx.compose.ui.graphics.ImageBitmap, String, androidx.compose.ui.Modifier, androidx.compose.ui.Alignment, androidx.compose.ui.layout.ContentScale, float, androidx.compose.ui.graphics.ColorFilter):
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 8234736..ab96527 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -367,9 +367,6 @@
public final class LazyListHeadersKt {
}
- public final class LazyListItemContentFactoryKt {
- }
-
public interface LazyListItemInfo {
method public int getIndex();
method public Object getKey();
@@ -398,9 +395,6 @@
public final class LazyListMeasureKt {
}
- public final class LazyListPrefetcher_androidKt {
- }
-
@androidx.compose.foundation.lazy.LazyScopeMarker public interface LazyListScope {
method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
@@ -448,6 +442,25 @@
}
+package androidx.compose.foundation.lazy.layout {
+
+ public final class LazyLayoutItemContentFactoryKt {
+ }
+
+ public final class LazyLayoutKt {
+ }
+
+ public final class LazyLayoutPrefetchPolicyKt {
+ }
+
+ public final class LazyLayoutPrefetcher_androidKt {
+ }
+
+ public final class LazyLayoutStateKt {
+ }
+
+}
+
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 3f38d0a..d5b6b12 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -412,9 +412,6 @@
public final class LazyListHeadersKt {
}
- public final class LazyListItemContentFactoryKt {
- }
-
public interface LazyListItemInfo {
method public int getIndex();
method public Object getKey();
@@ -443,9 +440,6 @@
public final class LazyListMeasureKt {
}
- public final class LazyListPrefetcher_androidKt {
- }
-
@androidx.compose.foundation.lazy.LazyScopeMarker public interface LazyListScope {
method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
@@ -494,6 +488,25 @@
}
+package androidx.compose.foundation.lazy.layout {
+
+ public final class LazyLayoutItemContentFactoryKt {
+ }
+
+ public final class LazyLayoutKt {
+ }
+
+ public final class LazyLayoutPrefetchPolicyKt {
+ }
+
+ public final class LazyLayoutPrefetcher_androidKt {
+ }
+
+ public final class LazyLayoutStateKt {
+ }
+
+}
+
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index f41809d..110a916 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,6 +1,10 @@
// Baseline format: 1.0
RemovedClass: androidx.compose.foundation.gestures.RelativeVelocityTrackerKt:
Removed class androidx.compose.foundation.gestures.RelativeVelocityTrackerKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemContentFactoryKt:
+ Removed class androidx.compose.foundation.lazy.LazyListItemContentFactoryKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListPrefetcher_androidKt:
+ Removed class androidx.compose.foundation.lazy.LazyListPrefetcher_androidKt
RemovedMethod: androidx.compose.foundation.ImageKt#Image(androidx.compose.ui.graphics.ImageBitmap, String, androidx.compose.ui.Modifier, androidx.compose.ui.Alignment, androidx.compose.ui.layout.ContentScale, float, androidx.compose.ui.graphics.ColorFilter):
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 8234736..ab96527 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -367,9 +367,6 @@
public final class LazyListHeadersKt {
}
- public final class LazyListItemContentFactoryKt {
- }
-
public interface LazyListItemInfo {
method public int getIndex();
method public Object getKey();
@@ -398,9 +395,6 @@
public final class LazyListMeasureKt {
}
- public final class LazyListPrefetcher_androidKt {
- }
-
@androidx.compose.foundation.lazy.LazyScopeMarker public interface LazyListScope {
method public void item(optional Object? key, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.LazyItemScope,kotlin.Unit> content);
method public void items(int count, optional kotlin.jvm.functions.Function1<? super java.lang.Integer,?>? key, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.LazyItemScope,? super java.lang.Integer,kotlin.Unit> itemContent);
@@ -448,6 +442,25 @@
}
+package androidx.compose.foundation.lazy.layout {
+
+ public final class LazyLayoutItemContentFactoryKt {
+ }
+
+ public final class LazyLayoutKt {
+ }
+
+ public final class LazyLayoutPrefetchPolicyKt {
+ }
+
+ public final class LazyLayoutPrefetcher_androidKt {
+ }
+
+ public final class LazyLayoutStateKt {
+ }
+
+}
+
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
index c0dfc19..4673083 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.lazy
import android.os.Build
+import androidx.compose.foundation.AutoTestFrameClock
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
@@ -202,6 +203,33 @@
}
@Test
+ fun changeItemsCountAndScrollImmediately() {
+ lateinit var state: LazyListState
+ var count by mutableStateOf(100)
+ val composedIndexes = mutableListOf<Int>()
+ rule.setContent {
+ state = rememberLazyListState()
+ LazyColumn(Modifier.fillMaxWidth().height(10.dp), state) {
+ items(count) { index ->
+ composedIndexes.add(index)
+ Box(Modifier.size(20.dp))
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ composedIndexes.clear()
+ count = 10
+ runBlocking(AutoTestFrameClock()) {
+ state.scrollToItem(50)
+ }
+ composedIndexes.forEach {
+ assertThat(it).isLessThan(count)
+ }
+ assertThat(state.firstVisibleItemIndex).isEqualTo(9)
+ }
+ }
+ @Test
fun changingDataTest() {
val dataLists = listOf(
(1..3).toList(),
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetcher.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
similarity index 74%
rename from compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetcher.android.kt
rename to compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
index 6e219f3..4d2f45d 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/LazyListPrefetcher.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
@@ -14,40 +14,36 @@
* limitations under the License.
*/
-package androidx.compose.foundation.lazy
+package androidx.compose.foundation.lazy.layout
import android.view.Choreographer
import android.view.Display
import android.view.View
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
-import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.layout.SubcomposeLayoutState
import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.trace
import java.util.concurrent.TimeUnit
-@OptIn(ExperimentalFoundationApi::class)
@Composable
-internal actual fun LazyListPrefetcher(
- lazyListState: LazyListState,
- stateOfItemsProvider: State<LazyListItemsProvider>,
- itemContentFactory: LazyListItemContentFactory,
+internal actual fun LazyLayoutPrefetcher(
+ prefetchPolicy: LazyLayoutPrefetchPolicy,
+ state: LazyLayoutState,
+ itemContentFactory: LazyLayoutItemContentFactory,
subcomposeLayoutState: SubcomposeLayoutState
) {
val view = LocalView.current
- remember(subcomposeLayoutState, lazyListState, view) {
- LazyListPrefetcher(
+ remember(subcomposeLayoutState, prefetchPolicy, view) {
+ LazyLayoutPrefetcher(
+ prefetchPolicy,
+ state,
subcomposeLayoutState,
- lazyListState,
- stateOfItemsProvider,
itemContentFactory,
view
)
@@ -109,29 +105,24 @@
* so critical given that we don't need to calculate the deadline.
* Tracking bug: 187393922
*/
-private class LazyListPrefetcher(
+internal class LazyLayoutPrefetcher(
+ private val prefetchPolicy: LazyLayoutPrefetchPolicy,
+ private val state: LazyLayoutState,
private val subcomposeLayoutState: SubcomposeLayoutState,
- private val lazyListState: LazyListState,
- private val stateOfItemsProvider: State<LazyListItemsProvider>,
- private val itemContentFactory: LazyListItemContentFactory,
+ private val itemContentFactory: LazyLayoutItemContentFactory,
private val view: View
) : RememberObserver,
- LazyListOnScrolledListener,
- LazyListOnPostMeasureListener,
+ LazyLayoutOnPostMeasureListener,
+ LazyLayoutPrefetchPolicy.Subscriber,
Runnable,
Choreographer.FrameCallback {
/**
- * Keeps the scrolling direction during the previous calculation in order to be able to
- * detect the scrolling direction change.
- */
- private var wasScrollingForward: Boolean = false
-
- /**
* The index scheduled to be prefetched (or the last prefetched index if the prefetch is
- * done, in this case [precomposedSlotHandle] is not null and associated with this index.
+ * done, in this case [precomposedSlotHandle] is not null and associated with this index).
+ * TODO(popam): probably this should be a queue rather than one element
*/
- private var indexToPrefetch: Int = -1
+ private var indexToPrefetch = -1
/**
* Non-null when the item with [indexToPrefetch] index was prefetched.
@@ -175,11 +166,11 @@
val beforeNs = System.nanoTime()
if (beforeNs > nextFrameNs || beforeNs + averagePrecomposeTimeNs < nextFrameNs) {
val index = indexToPrefetch
- val itemProvider = stateOfItemsProvider.value
+ val itemsProvider = state.itemsProvider()
if (view.windowVisibility == View.VISIBLE &&
- index in 0 until itemProvider.itemsCount
+ index in 0 until itemsProvider.itemsCount
) {
- precomposedSlotHandle = precompose(itemProvider, index)
+ precomposedSlotHandle = precompose(itemsProvider, index)
averagePrecomposeTimeNs = calculateAverageTime(
System.nanoTime() - beforeNs,
averagePrecomposeTimeNs
@@ -204,7 +195,7 @@
if (beforeNs > nextFrameNs || beforeNs + averagePremeasureTimeNs < nextFrameNs) {
if (view.windowVisibility == View.VISIBLE) {
premeasuringIsNeeded = true
- lazyListState.remeasurement.forceRemeasure()
+ state.remeasure()
averagePremeasureTimeNs = calculateAverageTime(
System.nanoTime() - beforeNs,
averagePremeasureTimeNs
@@ -232,10 +223,10 @@
}
private fun precompose(
- itemProvider: LazyListItemsProvider,
+ itemsProvider: LazyLayoutItemsProvider,
index: Int
): PrecomposedSlotHandle {
- val key = itemProvider.getKey(index)
+ val key = itemsProvider.getKey(index)
val content = itemContentFactory.getContent(index, key)
return subcomposeLayoutState.precompose(key, content)
}
@@ -251,67 +242,40 @@
}
}
- /**
- * The callback to be executed on every scroll.
- */
- override fun onScrolled(delta: Float) {
- if (!lazyListState.prefetchingEnabled) {
- return
- }
- val info = lazyListState.layoutInfo
- if (info.visibleItemsInfo.isNotEmpty()) {
- check(isActive)
- val scrollingForward = delta < 0
- val indexToPrefetch = if (scrollingForward) {
- info.visibleItemsInfo.last().index + 1
- } else {
- info.visibleItemsInfo.first().index - 1
- }
- if (indexToPrefetch != this.indexToPrefetch &&
- indexToPrefetch in 0 until info.totalItemsCount
- ) {
- val precomposedSlot = precomposedSlotHandle
- if (precomposedSlot != null) {
- if (wasScrollingForward != scrollingForward) {
- // the scrolling direction has been changed which means the last prefetched
- // is not going to be reached anytime soon so it is safer to dispose it.
- // if this item is already visible it is safe to call the method anyway
- // as it will be no-op
- precomposedSlot.dispose()
- }
- }
- this.wasScrollingForward = scrollingForward
- this.indexToPrefetch = indexToPrefetch
- this.precomposedSlotHandle = null
- premeasuringIsNeeded = false
- if (!prefetchScheduled) {
- prefetchScheduled = true
- // schedule the prefetching
- view.post(this)
- }
- }
+ override fun scheduleForPrefetch(index: Int) {
+ indexToPrefetch = index
+ precomposedSlotHandle = null
+ premeasuringIsNeeded = false
+ if (!prefetchScheduled) {
+ prefetchScheduled = true
+ // schedule the prefetching
+ view.post(this)
}
}
- override fun SubcomposeMeasureScope.onPostMeasure(
- childConstraints: Constraints,
- result: LazyListMeasureResult
- ) {
+ override fun removeFromPrefetch(index: Int) {
+ if (index == indexToPrefetch) {
+ precomposedSlotHandle?.dispose()
+ indexToPrefetch = -1
+ }
+ }
+
+ override fun SubcomposeMeasureScope.onPostMeasure(result: LazyLayoutMeasureResult) {
val index = indexToPrefetch
if (premeasuringIsNeeded && index != -1) {
check(isActive)
- val itemProvider = stateOfItemsProvider.value
- if (index < itemProvider.itemsCount) {
+ val itemsProvider = state.itemsProvider()
+ if (index < itemsProvider.itemsCount) {
val isVisibleAlready = result.visibleItemsInfo.fastAny { it.index == index }
- val composedButNotVisible = result.composedButNotVisibleItems != null &&
- result.composedButNotVisibleItems.fastAny { it.index == index }
+ val composedButNotVisible = result.composedButNotVisibleItemsIndices != null &&
+ result.composedButNotVisibleItemsIndices!!.fastAny { it == index }
if (isVisibleAlready || composedButNotVisible) {
premeasuringIsNeeded = false
} else {
- val key = itemProvider.getKey(index)
+ val key = itemsProvider.getKey(index)
val content = itemContentFactory.getContent(index, key)
subcompose(key, content).fastForEach {
- it.measure(childConstraints)
+ it.measure(prefetchPolicy.constraints)
}
}
}
@@ -319,15 +283,15 @@
}
override fun onRemembered() {
- lazyListState.>
- lazyListState.>
+ prefetchPolicy.prefetcher = this
+ state.>
isActive = true
}
override fun onForgotten() {
isActive = false
- lazyListState.>
- lazyListState.>
+ prefetchPolicy.prefetcher = null
+ state.>
view.removeCallbacks(this)
choreographer.removeFrameCallback(this)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt
index c037ca7..aa3c299 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/DataIndex.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@
package androidx.compose.foundation.lazy
/**
- * Represents an index in the list of items of lazy list.
+ * Represents an index in the list of items of lazy layout.
*/
@Suppress("NOTHING_TO_INLINE", "INLINE_CLASS_DEPRECATED", "EXPERIMENTAL_FEATURE_WARNING")
internal inline class DataIndex(val value: Int) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/IntervalList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/IntervalList.kt
index f09bc56..eedce14 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/IntervalList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/IntervalList.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
index 9eea202..8603749 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt
@@ -22,10 +22,6 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -164,73 +160,6 @@
itemContent(it, items[it])
}
-private class IntervalContent(
- val key: ((index: Int) -> Any)?,
- val content: LazyItemScope.(index: Int) -> @Composable() () -> Unit
-)
-
-private class LazyListScopeImpl : LazyListScope, LazyListItemsProvider {
- private val intervals = IntervalList<IntervalContent>()
- override val itemsCount get() = intervals.totalSize
- private var _headerIndexes: MutableList<Int>? = null
- override val headerIndexes: List<Int> get() = _headerIndexes ?: emptyList()
-
- override fun getKey(index: Int): Any {
- val interval = intervals.intervalForIndex(index)
- val localIntervalIndex = index - interval.startIndex
- val key = interval.content.key?.invoke(localIntervalIndex)
- return key ?: getDefaultLazyKeyFor(index)
- }
-
- override fun getContent(index: Int, scope: LazyItemScope): @Composable () -> Unit {
- val interval = intervals.intervalForIndex(index)
- val localIntervalIndex = index - interval.startIndex
- return interval.content.content.invoke(scope, localIntervalIndex)
- }
-
- override fun items(
- count: Int,
- key: ((index: Int) -> Any)?,
- itemContent: @Composable LazyItemScope.(index: Int) -> Unit
- ) {
- intervals.add(
- count,
- IntervalContent(
- key = key,
- content = { index -> @Composable { itemContent(index) } }
- )
- )
- }
-
- override fun item(key: Any?, content: @Composable LazyItemScope.() -> Unit) {
- intervals.add(
- 1,
- IntervalContent(
- key = if (key != null) { _: Int -> key } else null,
- content = { @Composable { content() } }
- )
- )
- }
-
- @ExperimentalFoundationApi
- override fun stickyHeader(key: Any?, content: @Composable LazyItemScope.() -> Unit) {
- val headersIndexes = _headerIndexes ?: mutableListOf<Int>().also {
- _headerIndexes = it
- }
- headersIndexes.add(itemsCount)
-
- item(key, content)
- }
-}
-
-/**
- * This should create an object meeting following requirements:
- * 1) objects created for the same index are equals and never equals for different indexes
- * 2) this class is saveable via a default SaveableStateRegistry on the platform
- * 3) this objects can't be equals to any object which could be provided by a user as a custom key
- */
-internal expect fun getDefaultLazyKeyFor(index: Int): Any
-
/**
* The horizontally scrolling list that only composes and lays out the currently visible items.
* The [content] block defines a DSL which allows you to emit items of different types. For
@@ -269,7 +198,6 @@
content: LazyListScope.() -> Unit
) {
LazyList(
- stateOfItemsProvider = rememberStateOfItemsProvider(content),
modifier = modifier,
state = state,
contentPadding = contentPadding,
@@ -277,7 +205,8 @@
horizontalArrangement = horizontalArrangement,
isVertical = false,
flingBehavior = flingBehavior,
- reverseLayout = reverseLayout
+ reverseLayout = reverseLayout,
+ content = content
)
}
@@ -319,7 +248,6 @@
content: LazyListScope.() -> Unit
) {
LazyList(
- stateOfItemsProvider = rememberStateOfItemsProvider(content),
modifier = modifier,
state = state,
contentPadding = contentPadding,
@@ -327,16 +255,15 @@
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArrangement,
isVertical = true,
- reverseLayout = reverseLayout
+ reverseLayout = reverseLayout,
+ content = content
)
}
-@Composable
-private fun rememberStateOfItemsProvider(
- content: LazyListScope.() -> Unit
-): State<LazyListItemsProvider> {
- val latestContent = rememberUpdatedState(content)
- return remember {
- derivedStateOf { LazyListScopeImpl().apply(latestContent.value) }
- }
-}
+/**
+ * This should create an object meeting following requirements:
+ * 1) objects created for the same index are equals and never equals for different indexes
+ * 2) this class is saveable via a default SaveableStateRegistry on the platform
+ * 3) this objects can't be equals to any object which could be provided by a user as a custom key
+ */
+internal expect fun getDefaultLazyKeyFor(index: Int): Any
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
index 9ca44a3..4874764 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyGrid.kt
@@ -242,9 +242,10 @@
intervals.add(1) { @Composable { content() } }
}
- override fun items(count: Int, itemContent: @Composable LazyItemScope.(index: Int) -> Unit) {
- intervals.add(count) {
- @Composable { itemContent(it) }
- }
+ override fun items(
+ count: Int,
+ itemContent: @Composable LazyItemScope.(index: Int) -> Unit
+ ) {
+ intervals.add(count) { @Composable { itemContent(it) } }
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt
index 39ce194..17fd69a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyItemScope.kt
@@ -16,9 +16,14 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
/**
* Receiver scope being used by the item content parameter of LazyColumn/Row.
@@ -28,8 +33,8 @@
interface LazyItemScope {
/**
* Have the content fill the [Constraints.maxWidth] and [Constraints.maxHeight] of the parent
- * measurement constraints by setting the [minimum width][Constraints.minWidth] to be equal to the
- * [maximum width][Constraints.maxWidth] multiplied by [fraction] and the [minimum
+ * measurement constraints by setting the [minimum width][Constraints.minWidth] to be equal to
+ * the [maximum width][Constraints.maxWidth] multiplied by [fraction] and the [minimum
* height][Constraints.minHeight] to be equal to the [maximum height][Constraints.maxHeight]
* multiplied by [fraction]. Note that, by default, the [fraction] is 1, so the modifier will
* make the content fill the whole available space. [fraction] must be between `0` and `1`.
@@ -72,3 +77,22 @@
fraction: Float = 1f
): Modifier
}
+
+internal data class LazyItemScopeImpl(
+ val density: Density,
+ val constraints: Constraints
+) : LazyItemScope {
+ private val maxWidth: Dp = with(density) { constraints.maxWidth.toDp() }
+ private val maxHeight: Dp = with(density) { constraints.maxHeight.toDp() }
+
+ override fun Modifier.fillParentMaxSize(fraction: Float) = size(
+ maxWidth * fraction,
+ maxHeight * fraction
+ )
+
+ override fun Modifier.fillParentMaxWidth(fraction: Float) =
+ width(maxWidth * fraction)
+
+ override fun Modifier.fillParentMaxHeight(fraction: Float) =
+ height(maxHeight * fraction)
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 92c623b..c54624d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.assertNotNestingScrollableContainers
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.FlingBehavior
@@ -28,24 +29,28 @@
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.lazy.layout.LazyMeasurePolicy
+import androidx.compose.foundation.lazy.layout.rememberLazyLayoutPrefetchPolicy
+import androidx.compose.foundation.lazy.layout.rememberLazyLayoutState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.IntrinsicMeasureScope
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.SubcomposeLayoutState
+import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@Composable
internal fun LazyList(
- /** State object containing the latest item provider */
- stateOfItemsProvider: State<LazyListItemsProvider>,
/** Modifier to be applied for the inner layout */
modifier: Modifier,
/** State controlling the scroll position */
@@ -65,23 +70,41 @@
/** The alignment to align items vertically. Required when isVertical is false */
verticalAlignment: Alignment.Vertical? = null,
/** The horizontal arrangement for items. Required when isVertical is false */
- horizontalArrangement: Arrangement.Horizontal? = null
+ horizontalArrangement: Arrangement.Horizontal? = null,
+ /** The content of the list */
+ content: LazyListScope.() -> Unit
) {
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- // reverse scroll by default, to have "natural" gesture that goes reversed to layout
- // if rtl and horizontal, do not reverse to make it right-to-left
- val reverseScrollDirection = if (!isVertical && isRtl) reverseLayout else !reverseLayout
-
- val itemContentFactory = rememberItemContentFactory(stateOfItemsProvider, state)
-
- val subcomposeLayoutState = remember { SubcomposeLayoutState(MaxItemsToRetainForReuse) }
- LazyListPrefetcher(state, stateOfItemsProvider, itemContentFactory, subcomposeLayoutState)
-
val overScrollController = rememberOverScrollController()
- SubcomposeLayout(
- subcomposeLayoutState,
- modifier
+ val itemScope: Ref<LazyItemScopeImpl> = remember { Ref() }
+
+ val stateOfItemsProvider = rememberStateOfItemsProvider(content, itemScope)
+
+ val measurePolicy = rememberLazyListMeasurePolicy(
+ stateOfItemsProvider,
+ itemScope,
+ state,
+ overScrollController,
+ contentPadding,
+ reverseLayout,
+ isVertical,
+ horizontalAlignment,
+ verticalArrangement,
+ verticalAlignment,
+ horizontalArrangement
+ )
+
+ state.prefetchPolicy = rememberLazyLayoutPrefetchPolicy()
+ state.innerState = rememberLazyLayoutState()
+
+ val itemsProvider = stateOfItemsProvider.value
+ if (itemsProvider.itemsCount > 0) {
+ state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
+ }
+
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ LazyLayout(
+ modifier = modifier
.lazyListSemantics(
stateOfItemsProvider = stateOfItemsProvider,
state = state,
@@ -91,25 +114,128 @@
.clipScrollableContainer(isVertical)
.scrollable(
orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
- reverseDirection = reverseScrollDirection,
+ reverseDirection = if (!isVertical && isRtl) reverseLayout else !reverseLayout,
interactionSource = state.internalInteractionSource,
flingBehavior = flingBehavior,
state = state,
overScrollController = overScrollController
)
- .padding(contentPadding)
- .then(state.remeasurementModifier)
- ) { constraints ->
+ .padding(contentPadding),
+ state = state.innerState,
+ prefetchPolicy = state.prefetchPolicy,
+ measurePolicy = measurePolicy,
+ itemsProvider = { stateOfItemsProvider.value }
+ )
+}
+
+@Composable
+private fun rememberStateOfItemsProvider(
+ content: LazyListScope.() -> Unit,
+ itemScope: Ref<LazyItemScopeImpl>
+): State<LazyListItemsProvider> {
+ val latestContent = rememberUpdatedState(content)
+ return remember {
+ derivedStateOf { LazyListScopeImpl(itemScope).apply(latestContent.value) }
+ }
+}
+
+internal class LazyListScopeImpl(
+ private val itemScope: Ref<LazyItemScopeImpl>
+) : LazyListScope, LazyListItemsProvider {
+ private val intervals = IntervalList<IntervalContent>()
+ override val itemsCount get() = intervals.totalSize
+ private var _headerIndexes: MutableList<Int>? = null
+ override val headerIndexes: List<Int> get() = _headerIndexes ?: emptyList()
+
+ override fun getKey(index: Int): Any {
+ val interval = intervals.intervalForIndex(index)
+ val localIntervalIndex = index - interval.startIndex
+ val key = interval.content.key?.invoke(localIntervalIndex)
+ return key ?: getDefaultLazyKeyFor(index)
+ }
+
+ override fun getContent(index: Int): @Composable () -> Unit {
+ val interval = intervals.intervalForIndex(index)
+ val localIntervalIndex = index - interval.startIndex
+ return interval.content.content.invoke(itemScope.value!!, localIntervalIndex)
+ }
+
+ override fun items(
+ count: Int,
+ key: ((index: Int) -> Any)?,
+ itemContent: @Composable LazyItemScope.(index: Int) -> Unit
+ ) {
+ intervals.add(
+ count,
+ IntervalContent(
+ key = key,
+ content = { index -> @Composable { itemContent(index) } }
+ )
+ )
+ }
+
+ override fun item(key: Any?, content: @Composable LazyItemScope.() -> Unit) {
+ intervals.add(
+ 1,
+ IntervalContent(
+ key = if (key != null) { _: Int -> key } else null,
+ content = { @Composable { content() } }
+ )
+ )
+ }
+
+ @ExperimentalFoundationApi
+ override fun stickyHeader(key: Any?, content: @Composable LazyItemScope.() -> Unit) {
+ val headersIndexes = _headerIndexes ?: mutableListOf<Int>().also {
+ _headerIndexes = it
+ }
+ headersIndexes.add(itemsCount)
+
+ item(key, content)
+ }
+}
+
+internal class IntervalContent(
+ val key: ((index: Int) -> Any)?,
+ val content: LazyItemScope.(index: Int) -> @Composable () -> Unit
+)
+
+@Composable
+private fun rememberLazyListMeasurePolicy(
+ /** State containing the items provider of the list. */
+ stateOfItemsProvider: State<LazyListItemsProvider>,
+ /** Value holder for the item scope used to compose items. */
+ itemScope: Ref<LazyItemScopeImpl>,
+ /** The state of the list. */
+ state: LazyListState,
+ /** The overscroll controller. */
+ overScrollController: OverScrollController,
+ /** The inner padding to be added for the whole content(nor for each individual item) */
+ contentPadding: PaddingValues,
+ /** reverse the direction of scrolling and layout */
+ reverseLayout: Boolean,
+ /** The layout orientation of the list */
+ isVertical: Boolean,
+ /** The alignment to align items horizontally. Required when isVertical is true */
+ horizontalAlignment: Alignment.Horizontal? = null,
+ /** The vertical arrangement for items. Required when isVertical is true */
+ verticalArrangement: Arrangement.Vertical? = null,
+ /** The alignment to align items vertically. Required when isVertical is false */
+ verticalAlignment: Alignment.Vertical? = null,
+ /** The horizontal arrangement for items. Required when isVertical is false */
+ horizontalArrangement: Arrangement.Horizontal? = null
+) = remember {
+ LazyMeasurePolicy { measurables, constraints ->
constraints.assertNotNestingScrollableContainers(isVertical)
val itemsProvider = stateOfItemsProvider.value
state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
// Update the state's cached Density
- state.density = Density(density, fontScale)
+ state.density = this
// this will update the scope object if the constrains have been changed
- itemContentFactory.updateItemScope(this, constraints)
+ itemScope.update(this, constraints)
val startContentPadding = if (isVertical) {
contentPadding.calculateTopPadding()
@@ -134,9 +260,8 @@
val itemProvider = LazyMeasuredItemProvider(
constraints,
isVertical,
- this,
itemsProvider,
- itemContentFactory
+ measurables
) { index, key, placeables ->
// we add spaceBetweenItems as an extra spacing for all items apart from the last one so
// the lazy list measuring logic will take it into account.
@@ -155,8 +280,9 @@
key = key
)
}
+ state.prefetchPolicy?.constraints = itemProvider.childConstraints
- val measureResult = measureLazyList(
+ measureLazyList(
itemsCount = itemsCount,
itemProvider = itemProvider,
mainAxisMaxSize = mainAxisMaxSize,
@@ -172,38 +298,21 @@
horizontalArrangement = horizontalArrangement,
reverseLayout = reverseLayout,
density = this,
- layoutDirection = layoutDirection
- )
-
- state.applyMeasureResult(measureResult)
-
- refreshOverScrollInfo(overScrollController, measureResult, contentPadding)
-
- state.onPostMeasureListener?.apply {
- onPostMeasure(itemProvider.childConstraints, measureResult)
- }
-
- layout(
- width = measureResult.layoutWidth,
- height = measureResult.layoutHeight,
- placementBlock = measureResult.placementBlock
- )
+ layoutDirection = layoutDirection,
+ layout = { width, height, placement -> layout(width, height, emptyMap(), placement) }
+ ).also {
+ state.applyMeasureResult(it)
+ refreshOverScrollInfo(overScrollController, it, contentPadding)
+ }.lazyLayoutMeasureResult
}
}
-private const val MaxItemsToRetainForReuse = 2
-
-/**
- * Platform specific implementation of lazy list prefetching - precomposing next items in
- * advance during the scrolling.
- */
-@Composable
-internal expect fun LazyListPrefetcher(
- lazyListState: LazyListState,
- stateOfItemsProvider: State<LazyListItemsProvider>,
- itemContentFactory: LazyListItemContentFactory,
- subcomposeLayoutState: SubcomposeLayoutState
-)
+private fun Ref<LazyItemScopeImpl>.update(density: Density, constraints: Constraints) {
+ val value = value
+ if (value == null || value.density != density || value.constraints != constraints) {
+ this.value = LazyItemScopeImpl(density, constraints)
+ }
+}
private fun IntrinsicMeasureScope.refreshOverScrollInfo(
overScrollController: OverScrollController,
@@ -224,8 +333,8 @@
overScrollController.refreshContainerInfo(
Size(
- result.layoutWidth.toFloat() + horizontalPadding.roundToPx(),
- result.layoutHeight.toFloat() + verticalPadding.roundToPx()
+ result.width.toFloat() + horizontalPadding.roundToPx(),
+ result.height.toFloat() + verticalPadding.roundToPx()
),
canScrollForward || canScrollBackward
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemContentFactory.kt
deleted file mode 100644
index 38d63f0..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemContentFactory.kt
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.lazy
-
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.SaveableStateHolder
-import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-
-@Composable
-internal fun rememberItemContentFactory(
- stateOfItemsProvider: State<LazyListItemsProvider>,
- state: LazyListState
-): LazyListItemContentFactory {
- val saveableStateHolder = rememberSaveableStateHolder()
- val factory = remember(stateOfItemsProvider) {
- LazyListItemContentFactory(saveableStateHolder, stateOfItemsProvider)
- }
- factory.updateKeyIndexMappingForVisibleItems(state)
- return factory
-}
-
-/**
- * This class:
- * 1) Caches the lambdas being produced by [itemsProvider]. This allows us to perform less
- * recompositions as the compose runtime can skip the whole composition if we subcompose with the
- * same instance of the content lambda.
- * 2) Updates the mapping between keys and indexes when we have a new factory
- * 3) Creates an [itemScope] to be used with [itemsProvider]
- * 4) Adds state restoration on top of the composable returned by [itemsProvider] with help of
- * [saveableStateHolder].
- */
-internal class LazyListItemContentFactory(
- private val saveableStateHolder: SaveableStateHolder,
- private var itemsProvider: State<LazyListItemsProvider>,
-) {
- /**
- * Contains the cached lambdas produced by the [itemsProvider].
- */
- private val lambdasCache = mutableMapOf<Any, CachedItemContent>()
-
- /**
- * We iterate through the currently composed keys and update the associated indexes so we can
- * smartly handle reorderings. If we will not do it and just wait for the next remeasure the
- * item could be recomposed before it and switch to start displaying the wrong item.
- */
- fun updateKeyIndexMappingForVisibleItems(state: LazyListState) {
- val itemsProvider = itemsProvider.value
- val itemsCount = itemsProvider.itemsCount
- if (itemsCount > 0) {
- state.updateScrollPositionIfTheFirstItemWasMoved(itemsProvider)
- val firstVisible = state.firstVisibleItemIndexNonObservable.value
- val count = state.visibleItemsCount
- for (i in firstVisible until minOf(itemsCount, firstVisible + count)) {
- val key = itemsProvider.getKey(i)
- lambdasCache[key]?.index = i
- }
- }
- }
-
- /**
- * Return cached item content lambda or creates a new lambda and puts it in the cache.
- */
- fun getContent(index: Int, key: Any): @Composable () -> Unit {
- val cachedContent = lambdasCache[key]
- return if (cachedContent != null && cachedContent.index == index) {
- cachedContent.content
- } else {
- val newContent = CachedItemContent(index, itemScope, key)
- lambdasCache[key] = newContent
- newContent.content
- }
- }
-
- private inner class CachedItemContent(
- initialIndex: Int,
- private val scope: LazyItemScopeImpl,
- val key: Any
- ) {
- var index by mutableStateOf(initialIndex)
-
- val content: @Composable () -> Unit = @Composable {
- val itemsProvider = itemsProvider.value
- if (index < itemsProvider.itemsCount) {
- val key = itemsProvider.getKey(index)
- if (key == this.key) {
- val content = itemsProvider.getContent(index, scope)
- saveableStateHolder.SaveableStateProvider(key, content)
- }
- }
- }
- }
-
- /**
- * The cached instance of the scope to be used for composing items.
- */
- private var itemScope = InitialLazyItemsScopeImpl
-
- /**
- * Updates the [itemScope] with the last [constraints] we got from the parent.
- */
- fun updateItemScope(density: Density, constraints: Constraints) {
- if (itemScope.density != density || itemScope.constraints != constraints) {
- itemScope = LazyItemScopeImpl(density, constraints)
- lambdasCache.clear()
- }
- }
-}
-
-/**
- * Pre-allocated initial value for [LazyItemScopeImpl] to not have it nullable and avoid using
- * late init.
- */
-private val InitialLazyItemsScopeImpl = LazyItemScopeImpl(Density(0f, 0f), Constraints())
-
-private data class LazyItemScopeImpl(
- val density: Density,
- val constraints: Constraints
-) : LazyItemScope {
- private val maxWidth: Dp = with(density) { constraints.maxWidth.toDp() }
- private val maxHeight: Dp = with(density) { constraints.maxHeight.toDp() }
-
- override fun Modifier.fillParentMaxSize(fraction: Float) = size(
- maxWidth * fraction,
- maxHeight * fraction
- )
-
- override fun Modifier.fillParentMaxWidth(fraction: Float) =
- width(maxWidth * fraction)
-
- override fun Modifier.fillParentMaxHeight(fraction: Float) =
- height(maxHeight * fraction)
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt
index 20afb64..d5e4c5d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemInfo.kt
@@ -17,7 +17,8 @@
package androidx.compose.foundation.lazy
/**
- * Contains useful information about an individual item in lazy lists like [LazyColumn] or [LazyRow].
+ * Contains useful information about an individual item in lazy lists like [LazyColumn]
+ * or [LazyRow].
*
* @see LazyListLayoutInfo
*/
@@ -28,6 +29,11 @@
val index: Int
/**
+ * The key of the item which was passed to the item() or items() function.
+ */
+ val key: Any
+
+ /**
* The main axis offset of the item. It is relative to the start of the lazy list container.
*/
val offset: Int
@@ -37,9 +43,4 @@
* slot for the item then this size will be calculated as the sum of their sizes.
*/
val size: Int
-
- /**
- * The key of the item which was passed to the item() or items() function.
- */
- val key: Any
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProvider.kt
index 85cf980..d721bff 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemsProvider.kt
@@ -16,18 +16,9 @@
package androidx.compose.foundation.lazy
-import androidx.compose.runtime.Composable
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemsProvider
-internal interface LazyListItemsProvider {
- /** The total size of the list */
- val itemsCount: Int
-
+internal interface LazyListItemsProvider : LazyLayoutItemsProvider {
/** The list of indexes of the sticky header items */
val headerIndexes: List<Int>
-
- /** Returns the key for the item on this index */
- fun getKey(index: Int): Any
-
- /** Returns the content lambda for the given index and scope object */
- fun getContent(index: Int, scope: LazyItemScope): @Composable() () -> Unit
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 6eb219c..e121f8b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -17,6 +17,8 @@
package androidx.compose.foundation.lazy
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@@ -47,7 +49,8 @@
horizontalArrangement: Arrangement.Horizontal?,
reverseLayout: Boolean,
density: Density,
- layoutDirection: LayoutDirection
+ layoutDirection: LayoutDirection,
+ layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
): LazyListMeasureResult {
require(startContentPadding >= 0)
require(endContentPadding >= 0)
@@ -59,9 +62,7 @@
canScrollForward = false,
consumedScroll = 0f,
composedButNotVisibleItems = null,
- layoutWidth = constraints.minWidth,
- layoutHeight = constraints.minHeight,
- placementBlock = {},
+ measureResult = layout(constraints.minWidth, constraints.minHeight) {},
visibleItemsInfo = emptyList(),
viewportStartOffset = -startContentPadding,
viewportEndOffset = endContentPadding,
@@ -258,9 +259,7 @@
canScrollForward = mainAxisUsed > maxOffset,
consumedScroll = consumedScroll,
composedButNotVisibleItems = notUsedButComposedItems,
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- placementBlock = {
+ measureResult = layout(layoutWidth, layoutHeight) {
visibleItems.fastForEach {
if (it !== headerItem) {
it.place(this, layoutWidth, layoutHeight)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
index 773c0f7..9378177 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasureResult.kt
@@ -16,7 +16,10 @@
package androidx.compose.foundation.lazy
-import androidx.compose.ui.layout.Placeable
+import androidx.compose.foundation.lazy.layout.LazyLayoutItemInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureResult
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.util.fastMap
/**
* The result of the measure pass for lazy list layout.
@@ -33,13 +36,8 @@
val consumedScroll: Float,
/** List of items which were composed, but are not a part of [visibleItemsInfo].*/
val composedButNotVisibleItems: List<LazyMeasuredItem>?,
- // properties to be used by the Layout's measure result
- /** The calculated layout width */
- val layoutWidth: Int,
- /** The calculated layout height */
- val layoutHeight: Int,
- /** The placement block */
- val placementBlock: Placeable.PlacementScope.() -> Unit,
+ // MeasureResult defining the layout
+ val measureResult: MeasureResult,
// properties representing the info needed for LazyListLayoutInfo
/** see [LazyListLayoutInfo.visibleItemsInfo] */
override val visibleItemsInfo: List<LazyListItemInfo>,
@@ -49,4 +47,17 @@
override val viewportEndOffset: Int,
/** see [LazyListLayoutInfo.totalItemsCount] */
override val totalItemsCount: Int,
-) : LazyListLayoutInfo
+) : LazyListLayoutInfo, MeasureResult by measureResult {
+ val lazyLayoutMeasureResult: LazyLayoutMeasureResult get() =
+ object : LazyLayoutMeasureResult, MeasureResult by measureResult {
+ override val visibleItemsInfo: List<LazyLayoutItemInfo>
+ get() = this@LazyListMeasureResult.visibleItemsInfo.fastMap {
+ object : LazyLayoutItemInfo {
+ override val index: Int get() = it.index
+ override val key: Any get() = it.key
+ }
+ }
+ override val composedButNotVisibleItemsIndices: List<Int>?
+ get() = composedButNotVisibleItems?.fastMap { it.index }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 51f0da8..c019af8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -22,15 +22,14 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchPolicy
+import androidx.compose.foundation.lazy.layout.LazyLayoutState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.layout.Remeasurement
-import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import kotlin.math.abs
@@ -59,7 +58,7 @@
}
/**
- * A state object that can be hoisted to control and observe scrolling
+ * A state object that can be hoisted to control and observe scrolling.
*
* In most cases, this will be created via [rememberLazyListState].
*
@@ -141,12 +140,6 @@
private val scrollableState = ScrollableState { -onScroll(-it) }
/**
- * The [Remeasurement] object associated with our layout. It allows us to remeasure
- * synchronously during scroll.
- */
- internal lateinit var remeasurement: Remeasurement
-
- /**
* Only used for testing to confirm that we're not making too many measure passes
*/
/*@VisibleForTesting*/
@@ -160,13 +153,20 @@
internal var prefetchingEnabled: Boolean = true
/**
- * The modifier which provides [remeasurement].
+ * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done).
*/
- internal val remeasurementModifier = object : RemeasurementModifier {
- override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
- this@LazyListState.remeasurement = remeasurement
- }
- }
+ private var indexToPrefetch = -1
+
+ /**
+ * Keeps the scrolling direction during the previous calculation in order to be able to
+ * detect the scrolling direction change.
+ */
+ private var wasScrollingForward = false
+
+ /**
+ * The state of the inner LazyLayout.
+ */
+ internal lateinit var innerState: LazyLayoutState
/**
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
@@ -185,13 +185,15 @@
index: Int,
/*@IntRange(from = 0)*/
scrollOffset: Int = 0
- ) = scrollableState.scroll {
- snapToItemIndexInternal(index, scrollOffset)
+ ) {
+ return scrollableState.scroll {
+ snapToItemIndexInternal(index, scrollOffset)
+ }
}
internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int) {
scrollPosition.requestPosition(DataIndex(index), scrollOffset)
- remeasurement.forceRemeasure()
+ innerState.remeasure()
}
/**
@@ -217,9 +219,6 @@
override val isScrollInProgress: Boolean
get() = scrollableState.isScrollInProgress
- internal var onScrolledListener: LazyListOnScrolledListener? = null
- internal var onPostMeasureListener: LazyListOnPostMeasureListener? = null
-
private var canScrollBackward: Boolean = false
private var canScrollForward: Boolean = false
@@ -240,8 +239,10 @@
// we have less than 0.5 pixels
if (abs(scrollToBeConsumed) > 0.5f) {
val preScrollToBeConsumed = scrollToBeConsumed
- remeasurement.forceRemeasure()
- onScrolledListener?.onScrolled(preScrollToBeConsumed - scrollToBeConsumed)
+ innerState.remeasure()
+ if (prefetchingEnabled && prefetchPolicy != null) {
+ notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+ }
}
// here scrollToBeConsumed is already consumed during the forceRemeasure invocation
@@ -258,6 +259,38 @@
}
}
+ private fun notifyPrefetch(delta: Float) {
+ if (!prefetchingEnabled) {
+ return
+ }
+ val info = layoutInfo
+ if (info.visibleItemsInfo.isNotEmpty()) {
+ // check(isActive)
+ val scrollingForward = delta < 0
+ val indexToPrefetch = if (scrollingForward) {
+ info.visibleItemsInfo.last().index + 1
+ } else {
+ info.visibleItemsInfo.first().index - 1
+ }
+ if (indexToPrefetch != this.indexToPrefetch &&
+ indexToPrefetch in 0 until info.totalItemsCount
+ ) {
+ if (wasScrollingForward != scrollingForward) {
+ // the scrolling direction has been changed which means the last prefetched
+ // is not going to be reached anytime soon so it is safer to dispose it.
+ // if this item is already visible it is safe to call the method anyway
+ // as it will be no-op
+ prefetchPolicy?.removeFromPrefetch(this.indexToPrefetch)
+ }
+ this.wasScrollingForward = scrollingForward
+ this.indexToPrefetch = indexToPrefetch
+ prefetchPolicy?.scheduleForPrefetch(indexToPrefetch)
+ }
+ }
+ }
+
+ internal var prefetchPolicy: LazyLayoutPrefetchPolicy? = null
+
/**
* Animate (smooth scroll) to the given item.
*
@@ -314,6 +347,12 @@
)
}
)
+
+ /**
+ * Pre-allocated initial value for [LazyItemScopeImpl] to not have it nullable and
+ * avoid using late init.
+ */
+ private val InitialLazyItemsScopeImpl = LazyItemScopeImpl(Density(0f, 0f), Constraints())
}
}
@@ -323,14 +362,3 @@
override val viewportEndOffset = 0
override val totalItemsCount = 0
}
-
-internal interface LazyListOnScrolledListener {
- fun onScrolled(delta: Float)
-}
-
-internal interface LazyListOnPostMeasureListener {
- fun SubcomposeMeasureScope.onPostMeasure(
- childConstraints: Constraints,
- result: LazyListMeasureResult
- )
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
index 8d4d0dd..ebecce6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyMeasuredItemProvider.kt
@@ -16,8 +16,8 @@
package androidx.compose.foundation.lazy
+import androidx.compose.foundation.lazy.layout.LazyMeasurablesProvider
import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
/**
@@ -26,9 +26,8 @@
internal class LazyMeasuredItemProvider(
constraints: Constraints,
isVertical: Boolean,
- private val scope: SubcomposeMeasureScope,
private val itemsProvider: LazyListItemsProvider,
- private val itemContentFactory: LazyListItemContentFactory,
+ private val measurables: LazyMeasurablesProvider,
private val measuredItemFactory: MeasuredItemFactory
) {
// the constraints we will measure child with. the main axis is not restricted
@@ -44,8 +43,7 @@
*/
fun getAndMeasure(index: DataIndex): LazyMeasuredItem {
val key = itemsProvider.getKey(index.value)
- val content = itemContentFactory.getContent(index.value, key)
- val measurables = scope.subcompose(key, content)
+ val measurables = measurables[index.value]
val placeables = Array(measurables.size) {
measurables[it].measure(childConstraints)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
index a0606a7..f5ef66d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
@@ -18,7 +18,6 @@
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.CollectionInfo
@@ -76,9 +75,9 @@
}
scrollToIndex { index ->
- require(index >= 0 && index < stateOfItemsProvider.value.itemsCount) {
- "Can't scroll to index $index, it is out of bounds [0, ${stateOfItemsProvider
- .value.itemsCount})"
+ require(index >= 0 && index < state.layoutInfo.totalItemsCount) {
+ "Can't scroll to index $index, it is out of " +
+ "bounds [0, ${state.layoutInfo.totalItemsCount})"
}
coroutineScope.launch {
state.scrollToItem(index)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
new file mode 100644
index 0000000..0eb750b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.SubcomposeLayoutState
+
+/**
+ * A layout that only composes and lays out currently visible items. Can be used to build
+ * efficient scrollable layouts.
+ */
+@Composable
+internal fun LazyLayout(
+ itemsProvider: () -> LazyLayoutItemsProvider,
+ modifier: Modifier = Modifier,
+ state: LazyLayoutState = rememberLazyLayoutState(),
+ prefetchPolicy: LazyLayoutPrefetchPolicy? = null,
+ measurePolicy: LazyMeasurePolicy
+) {
+ state.itemsProvider = itemsProvider
+ val itemContentFactory = rememberItemContentFactory(state)
+ val subcomposeLayoutState = remember { SubcomposeLayoutState(MaxItemsToRetainForReuse) }
+ prefetchPolicy?.let {
+ LazyLayoutPrefetcher(prefetchPolicy, state, itemContentFactory, subcomposeLayoutState)
+ }
+
+ SubcomposeLayout(
+ subcomposeLayoutState,
+ modifier.then(state.remeasurementModifier)
+ ) { constraints ->
+ itemContentFactory.onBeforeMeasure(this, constraints)
+
+ val measurables = LazyMeasurablesProvider(
+ state.itemsProvider(),
+ itemContentFactory,
+ this
+ )
+ val measureResult = with(measurePolicy) { measure(measurables, constraints) }
+
+ state.onPostMeasureListener?.apply { onPostMeasure(measureResult) }
+ state.layoutInfoState.value = measureResult
+ state.layoutInfoNonObservable = measureResult
+
+ measureResult
+ }
+}
+
+private const val MaxItemsToRetainForReuse = 2
+
+/**
+ * Platform specific implementation of lazy layout items prefetching - precomposing next items in
+ * advance during the scrolling.
+ */
+@Composable
+internal expect fun LazyLayoutPrefetcher(
+ prefetchPolicy: LazyLayoutPrefetchPolicy,
+ state: LazyLayoutState,
+ itemContentFactory: LazyLayoutItemContentFactory,
+ subcomposeLayoutState: SubcomposeLayoutState
+)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
new file mode 100644
index 0000000..2e23171
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastForEach
+
+@Composable
+internal fun rememberItemContentFactory(state: LazyLayoutState): LazyLayoutItemContentFactory {
+ val saveableStateHolder = rememberSaveableStateHolder()
+ val itemsProvider = state.itemsProvider
+ val factory = remember(itemsProvider) {
+ LazyLayoutItemContentFactory(saveableStateHolder, itemsProvider)
+ }
+ factory.updateKeyIndexMappingForVisibleItems(state)
+ return factory
+}
+
+/**
+ * This class:
+ * 1) Caches the lambdas being produced by [itemsProvider]. This allows us to perform less
+ * recompositions as the compose runtime can skip the whole composition if we subcompose with the
+ * same instance of the content lambda.
+ * 2) Updates the mapping between keys and indexes when we have a new factory
+ * 3) Adds state restoration on top of the composable returned by [itemsProvider] with help of
+ * [saveableStateHolder].
+ */
+internal class LazyLayoutItemContentFactory(
+ private val saveableStateHolder: SaveableStateHolder,
+ private val itemsProvider: () -> LazyLayoutItemsProvider,
+) {
+ /** Contains the cached lambdas produced by the [itemsProvider]. */
+ private val lambdasCache = mutableMapOf<Any, CachedItemContent>()
+
+ /** Density used to obtain the cached lambdas. */
+ private var densityOfCachedLambdas = Density(0f, 0f)
+
+ /** Constraints used to obtain the cached lambdas. */
+ private var constraintsOfCachedLambdas = Constraints()
+
+ /**
+ * We iterate through the currently composed keys and update the associated indexes so we can
+ * smartly handle reorderings. If we will not do it and just wait for the next remeasure the
+ * item could be recomposed before it and switch to start displaying the wrong item.
+ */
+ fun updateKeyIndexMappingForVisibleItems(state: LazyLayoutState) {
+ val itemsProvider = itemsProvider()
+ val itemsCount = itemsProvider.itemsCount
+ if (itemsCount > 0) {
+ state.layoutInfoNonObservable.visibleItemsInfo.fastForEach {
+ if (it.index < itemsCount) {
+ val key = itemsProvider.getKey(it.index)
+ lambdasCache[key]?.index = it.index
+ }
+ }
+ }
+ }
+
+ /**
+ * Invalidate the cached lambas if the density or constraints have changed.
+ * TODO(popam): probably LazyLayoutState should provide an invalidate() method instead.
+ */
+ fun onBeforeMeasure(density: Density, constraints: Constraints) {
+ if (density != densityOfCachedLambdas || constraints != constraintsOfCachedLambdas) {
+ densityOfCachedLambdas = density
+ constraintsOfCachedLambdas = constraints
+ lambdasCache.clear()
+ }
+ }
+
+ /**
+ * Return cached item content lambda or creates a new lambda and puts it in the cache.
+ */
+ fun getContent(index: Int, key: Any): @Composable () -> Unit {
+ val cachedContent = lambdasCache[key]
+ return if (cachedContent != null && cachedContent.index == index) {
+ cachedContent.content
+ } else {
+ val newContent = CachedItemContent(index, key)
+ lambdasCache[key] = newContent
+ newContent.content
+ }
+ }
+
+ private inner class CachedItemContent(
+ initialIndex: Int,
+ val key: Any
+ ) {
+ var index by mutableStateOf(initialIndex)
+
+ val content: @Composable () -> Unit = @Composable {
+ val itemsProvider = itemsProvider()
+ if (index < itemsProvider.itemsCount) {
+ val key = itemsProvider.getKey(index)
+ if (key == this.key) {
+ val content = itemsProvider.getContent(index)
+ saveableStateHolder.SaveableStateProvider(key, content)
+ }
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemInfo.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemInfo.kt
new file mode 100644
index 0000000..d1e049c
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemInfo.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+/**
+ * Contains useful information about an individual item in lazy layouts.
+ *
+ * @see LazyLayoutInfo
+ */
+internal interface LazyLayoutItemInfo {
+ /**
+ * The index of the item in the list.
+ */
+ val index: Int
+
+ /**
+ * The key of the item which was passed to the item() or items() function.
+ */
+ val key: Any
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemsProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemsProvider.kt
new file mode 100644
index 0000000..fe7a75e7
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemsProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.Composable
+
+internal interface LazyLayoutItemsProvider {
+ /** Returns the content lambda for the given index and scope object */
+ fun getContent(index: Int): @Composable () -> Unit
+
+ /** The total number of items in the lazy layout (visible or not). */
+ val itemsCount: Int
+
+ /** Returns the key for the item on this index */
+ fun getKey(index: Int): Any
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureResult.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureResult.kt
new file mode 100644
index 0000000..5391b0d
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureResult.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.ui.layout.MeasureResult
+
+internal interface LazyLayoutMeasureResult : MeasureResult, LazyLayoutInfo {
+ /**
+ * The list of [LazyLayoutItemInfo] representing all the currently visible items.
+ */
+ override val visibleItemsInfo: List<LazyLayoutItemInfo>
+
+ // TODO(popam): this should really be removed / derived implicitly from the placement block.
+ val composedButNotVisibleItemsIndices: List<Int>?
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchPolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchPolicy.kt
new file mode 100644
index 0000000..44dd216
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchPolicy.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.unit.Constraints
+
+/** Creates a [LazyLayoutPrefetchPolicy]. */
+@Composable
+internal fun rememberLazyLayoutPrefetchPolicy(): LazyLayoutPrefetchPolicy = remember {
+ LazyLayoutPrefetchPolicy()
+}
+
+/**
+ * Controller for lazy items prefetching, used by lazy layouts to instruct the prefetcher.
+ * TODO: This is currently supporting just one item, but it should rather be a queue.
+ */
+@Stable
+internal class LazyLayoutPrefetchPolicy {
+ internal var prefetcher: Subscriber? = null
+
+ /** Schedules a new item to prefetch, specified by [index]. */
+ fun scheduleForPrefetch(index: Int) = prefetcher?.scheduleForPrefetch(index)
+
+ /** Notifies the prefetcher that item with [index] is no longer likely to be needed. */
+ fun removeFromPrefetch(index: Int) = prefetcher?.removeFromPrefetch(index)
+
+ /** The constraints to be used for premeasuring the precomposed items. */
+ var constraints = Constraints()
+
+ interface Subscriber {
+ fun scheduleForPrefetch(index: Int)
+ fun removeFromPrefetch(index: Int)
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutState.kt
new file mode 100644
index 0000000..2ed35e0
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutState.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
+import androidx.compose.ui.layout.SubcomposeMeasureScope
+
+/**
+ * Creates a [LazyLayoutState] that is remembered across recompositions.
+ */
+@Composable
+internal fun rememberLazyLayoutState(): LazyLayoutState {
+ return remember { LazyLayoutState() }
+}
+
+/**
+ * A state object that can be hoisted to interact and observe the state of the [LazyLayout].
+ *
+ * In most cases, this will be created via [rememberLazyLayoutState].
+*/
+@Stable
+internal class LazyLayoutState internal constructor() {
+ /**
+ * Information about the layout of the lazy layout, calculated during the latest layout pass.
+ */
+ val layoutInfo: LazyLayoutInfo get() = layoutInfoState.value
+
+ /** Backing state for [layoutInfo] */
+ internal val layoutInfoState = mutableStateOf<LazyLayoutInfo>(EmptyLazyLayoutInfo)
+
+ internal var layoutInfoNonObservable: LazyLayoutInfo = EmptyLazyLayoutInfo
+
+ /**
+ * Remeasures the lazy list now. This can be used, for example, in reaction to scrolling.
+ */
+ fun remeasure() = remeasurement?.forceRemeasure()
+
+ /**
+ * The [Remeasurement] object associated with our layout. It allows us to remeasure
+ * synchronously during scroll.
+ */
+ private var remeasurement: Remeasurement? = null
+
+ /**
+ * The modifier which provides [remeasurement].
+ */
+ internal val remeasurementModifier = object : RemeasurementModifier {
+ override fun onRemeasurementAvailable(remeasurement: Remeasurement) {
+ this@LazyLayoutState.remeasurement = remeasurement
+ }
+ }
+
+ /**
+ * The items provider of the lazy layout.
+ */
+ internal var itemsProvider: () -> LazyLayoutItemsProvider = { NoItemsProvider }
+
+ /**
+ * Listener to be notified after measurement - the prefetcher.
+ */
+ internal var onPostMeasureListener: LazyLayoutOnPostMeasureListener? = null
+}
+
+internal interface LazyLayoutInfo {
+ /** The items currently participating in the layout of the lazy layout. */
+ val visibleItemsInfo: List<LazyLayoutItemInfo>
+}
+
+private object EmptyLazyLayoutInfo : LazyLayoutInfo {
+ override val visibleItemsInfo = emptyList<LazyLayoutItemInfo>()
+}
+
+internal interface LazyLayoutOnPostMeasureListener {
+ fun SubcomposeMeasureScope.onPostMeasure(result: LazyLayoutMeasureResult)
+}
+
+private object NoItemsProvider : LazyLayoutItemsProvider {
+ override fun getContent(index: Int): () -> Unit = error("No items")
+
+ override val itemsCount = 0
+
+ override fun getKey(index: Int): Any = error("No items")
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyMeasurePolicy.kt
new file mode 100644
index 0000000..115410b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyMeasurePolicy.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.SubcomposeMeasureScope
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Defines the measure and layout behaviour of a [LazyLayout].
+ */
+@Stable
+internal fun interface LazyMeasurePolicy {
+ fun MeasureScope.measure(
+ measurables: LazyMeasurablesProvider,
+ constraints: Constraints
+ ): LazyLayoutMeasureResult
+}
+
+/** A lazily evaluated "list" of [Measurable]s. */
+@Stable
+internal class LazyMeasurablesProvider internal constructor(
+ private val itemsProvider: LazyLayoutItemsProvider,
+ private val itemContentFactory: LazyLayoutItemContentFactory,
+ private val subcomposeMeasureScope: SubcomposeMeasureScope
+) {
+ operator fun get(index: Int): List<Measurable> {
+ val key = itemsProvider.getKey(index)
+ val itemContent = itemContentFactory.getContent(index, key)
+ return subcomposeMeasureScope.subcompose(key, itemContent)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/Lazy.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/Lazy.desktop.kt
index 20e25b8..8f78d81 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/Lazy.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/Lazy.desktop.kt
@@ -16,20 +16,6 @@
package androidx.compose.foundation.lazy
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.ui.layout.SubcomposeLayoutState
-
internal actual fun getDefaultLazyKeyFor(index: Int): Any = DefaultLazyKey(index)
private data class DefaultLazyKey(private val index: Int)
-
-@Composable
-internal actual fun LazyListPrefetcher(
- lazyListState: LazyListState,
- stateOfItemsProvider: State<LazyListItemsProvider>,
- itemContentFactory: LazyListItemContentFactory,
- subcomposeLayoutState: SubcomposeLayoutState
-) {
- // there is no prefetch implementation on desktop yet
-}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt
new file mode 100644
index 0000000..37ab057
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.desktop.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.layout.SubcomposeLayoutState
+
+@Composable
+internal actual fun LazyLayoutPrefetcher(
+ prefetchPolicy: LazyLayoutPrefetchPolicy,
+ state: LazyLayoutState,
+ itemContentFactory: LazyLayoutItemContentFactory,
+ subcomposeLayoutState: SubcomposeLayoutState
+) {
+ // there is no prefetch implementation on desktop yet
+}