androidx.compose.foundation.lazy.layout

Interfaces

IntervalList

The list consisting of multiple intervals.

Cmn
LazyLayoutCacheWindow

Represents an out of viewport area of a Lazy Layout where items should be cached.

Cmn
LazyLayoutIntervalContent.Interval

Common content of individual intervals in item DSL of lazy layouts.

Cmn
LazyLayoutItemProvider

Provides all the needed info about the items which could be later composed and displayed as children or LazyLayout.

Cmn
LazyLayoutMeasurePolicy
Cmn
LazyLayoutMeasureScope

The receiver scope of a LazyLayout's measure lambda.

Cmn
LazyLayoutPinnedItemList.PinnedItem

Item pinned in a lazy layout.

Cmn
LazyLayoutPrefetchState.PrefetchHandle

A handle to control some aspects of the prefetch request.

Cmn
LazyLayoutPrefetchState.PrefetchResultScope

A scope for schedulePrecompositionAndPremeasure callbacks.

Cmn
LazyLayoutScrollScope

A ScrollScope to allow customization of scroll sessions in LazyLayouts.

Cmn
NestedPrefetchScope

A scope which allows nested prefetches to be requested for the precomposition of a LazyLayout.

Cmn
PrefetchRequest

This interface is deprecated. Customization of PrefetchScheduler is no longer supported.

Cmn
PrefetchRequestScope

This interface is deprecated. Customization of PrefetchScheduler is no longer supported.

Cmn
PrefetchScheduler

This interface is deprecated. Customization of PrefetchScheduler is no longer supported.

Cmn

Classes

IntervalList.Interval

The interval holder.

Cmn
LazyLayoutIntervalContent

Common parts backing the interval-based content of lazy layout defined through item DSL.

Cmn
LazyLayoutPinnedItemList

Read-only list of pinned items in a lazy layout.

Cmn
LazyLayoutPrefetchState

State for lazy items prefetching, used by lazy layouts to instruct the prefetcher.

Cmn
MutableIntervalList

Mutable version of IntervalList.

Cmn

Top-level functions summary

Unit
@Composable
LazyLayout(
    itemProvider: () -> LazyLayoutItemProvider,
    modifier: Modifier,
    prefetchState: LazyLayoutPrefetchState?,
    measurePolicy: LazyLayoutMeasurePolicy
)

A layout that only composes and lays out currently needed items.

Cmn
LazyLayoutCacheWindow

A Dp based LazyLayoutCacheWindow.

Cmn
LazyLayoutCacheWindow
@ExperimentalFoundationApi
LazyLayoutCacheWindow(
    aheadFraction: @FloatRange(from = 0.0) Float,
    behindFraction: @FloatRange(from = 0.0) Float
)

Creates a LazyLayoutCacheWindow based off a fraction of the viewport.

Cmn
Unit
@Composable
LazyLayoutPinnableItem(
    key: Any?,
    index: Int,
    pinnedItemList: LazyLayoutPinnedItemList,
    content: @Composable () -> Unit
)

Wrapper supporting PinnableContainer in lazy layout items.

Cmn
Any

This creates an object meeting following requirements:

Cmn
android

Top-level functions

@Composable
fun LazyLayout(
    itemProvider: () -> LazyLayoutItemProvider,
    modifier: Modifier = Modifier,
    prefetchState: LazyLayoutPrefetchState? = null,
    measurePolicy: LazyLayoutMeasurePolicy
): Unit

A layout that only composes and lays out currently needed items. Can be used to build efficient complex layouts. Currently needed items depend on the LazyLayout implementation, that is, on how the LazyLayoutMeasurePolicy is implemented. Composing items during the measure pass is the signal to indicate which items are "currently needed". In general, only visible items are considered needed, but additional items may be requested by calling LazyLayoutMeasureScope.compose.

This is a low level API for building efficient complex layouts, for a ready-to-use linearly scrollable lazy layout implementation see androidx.compose.foundation.lazy.LazyRow and androidx.compose.foundation.lazy.LazyRow. For a grid-like scrollable lazy layout, see androidx.compose.foundation.lazy.grid.LazyVerticalGrid and androidx.compose.foundation.lazy.grid.LazyHorizontalGrid. For a pager-like lazy layout, see androidx.compose.foundation.pager.VerticalPager and androidx.compose.foundation.pager.HorizontalPager

For a basic lazy layout sample, see:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.layout.LazyLayout
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp

val items = remember { (0..100).toList().map { it.toString() } }

// Create an item provider
val itemProvider = remember {
    {
        object : LazyLayoutItemProvider {
            override val itemCount: Int
                get() = 100

            @Composable
            override fun Item(index: Int, key: Any) {
                Box(
                    modifier =
                        Modifier.width(100.dp)
                            .height(100.dp)
                            .background(color = if (index % 2 == 0) Color.Red else Color.Green)
                ) {
                    Text(text = items[index])
                }
            }
        }
    }
}

LazyLayout(modifier = Modifier.size(500.dp), itemProvider = itemProvider) { constraints ->
    // plug the measure policy, this is how we create and layout items.
    val placeablesCache = mutableListOf<Pair<Placeable, Int>>()
    fun Placeable.mainAxisSize() = width
    fun Placeable.crossAxisSize() = height

    val childConstraints =
        Constraints(maxWidth = Constraints.Infinity, maxHeight = constraints.maxHeight)

    var currentItemIndex = 0
    var crossAxisSize = 0
    var mainAxisSize = 0

    // measure items until we either fill in the space or run out of items.
    while (mainAxisSize < constraints.maxWidth && currentItemIndex < items.size) {
        val itemPlaceables = compose(currentItemIndex).map { it.measure(childConstraints) }
        for (item in itemPlaceables) {
            // save placeable to be placed later.
            placeablesCache.add(item to mainAxisSize)

            mainAxisSize += item.mainAxisSize() // item size contributes to main axis size
            // cross axis size will the size of tallest/widest item
            crossAxisSize = maxOf(crossAxisSize, item.crossAxisSize())
        }
        currentItemIndex++
    }

    val layoutWidth = minOf(mainAxisSize, constraints.maxHeight)
    val layoutHeight = crossAxisSize

    layout(layoutWidth, layoutHeight) {
        // since this is a linear list all items are placed on the same cross-axis position
        for ((placeable, position) in placeablesCache) {
            placeable.place(position, 0)
        }
    }
}

For a scrollable lazy layout, see:

import androidx.collection.IntObjectMap
import androidx.collection.MutableIntObjectMap
import androidx.collection.mutableIntObjectMapOf
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.layout.LazyLayout
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.constrainHeight
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastRoundToInt

val items = remember { (0..100).toList().map { it.toString() } }

/** Saves the deltas from the scroll gesture. */
var scrollAmount by remember { mutableFloatStateOf(0f) }

/**
 * Lazy Layout measurement starts at a known position identified by the first item's position.
 */
var firstVisibleItemIndex by remember { mutableIntStateOf(0) }
var firstVisibleItemOffset by remember { mutableIntStateOf(0) }

// A scrollable state is needed for scroll support
val scrollableState = rememberScrollableState { delta ->
    scrollAmount += delta
    delta // assume we consumed everything.
}

// Create an item provider
val itemProvider = {
    object : LazyLayoutItemProvider {
        override val itemCount: Int
            get() = items.size

        @Composable
        override fun Item(index: Int, key: Any) {
            Box(
                modifier =
                    Modifier.width(100.dp)
                        .height(100.dp)
                        .background(color = if (index % 2 == 0) Color.Red else Color.Green)
            ) {
                Text(text = items[index])
            }
        }
    }
}

fun LazyLayoutMeasureScope.createItem(
    index: Int,
    constraints: Constraints,
    placeablesCache: IntObjectMap<Placeable>,
): Placeable {

    val cachedPlaceable = placeablesCache[index]
    return if (cachedPlaceable == null) {
        val measurables = compose(index)
        require(measurables.size == 1) { "Only one composable item emission is supported." }
        measurables[0].measure(constraints)
    } else {
        cachedPlaceable
    }
}

fun LazyLayoutMeasureScope.measureLayout(
    scrollAmount: Float,
    firstVisibleItemIndex: Int,
    firstVisibleItemOffset: Int,
    itemCount: Int,
    placeablesCache: MutableIntObjectMap<Placeable>,
    containerConstraints: Constraints,
    updatePositions: (Int, Int) -> Unit,
): MeasureResult {
    /** 1) Resolve layout information and constraints. */
    val viewportSize = containerConstraints.maxWidth
    // children are only restricted on the cross axis size.
    val childConstraints =
        Constraints(maxWidth = Constraints.Infinity, maxHeight = viewportSize)

    /** 2) Initialize data holders for the pass. */
    // All items that will be placed in this layout in the layout pass.
    val placeables = mutableListOf<Pair<Placeable, Int>>()
    // The anchor points, we start from a known position, the position of the first item.
    var currentFirstVisibleItemIndex = firstVisibleItemIndex
    var currentFirstVisibleItemOffset = firstVisibleItemOffset
    // represents the real amount of scroll we applied as a result of this measure pass.
    val scrollDelta = scrollAmount.fastRoundToInt()
    // The amount of space available to items.
    val maxOffset = containerConstraints.maxWidth
    // tallest item from the ones we've created in this layout.This will determined the cross
    // axis
    // size of this layout
    var maxCrossAxis = 0

    /** 3) Apply Scroll */
    // applying the whole requested scroll offset.
    currentFirstVisibleItemOffset -= scrollDelta

    // if the current scroll offset is less than minimally possible we reset. Imagine we've
    // reached
    // the bounds at the start of the layout and we tried to scroll back.
    if (currentFirstVisibleItemIndex == 0 && currentFirstVisibleItemOffset < 0) {
        currentFirstVisibleItemOffset = 0
    }

    /** 4) Consider we scrolled back */
    while (currentFirstVisibleItemOffset < 0 && currentFirstVisibleItemIndex > 0) {
        val previous = currentFirstVisibleItemIndex - 1
        val measuredItem = createItem(previous, childConstraints, placeablesCache)
        placeables.add(0, measuredItem to currentFirstVisibleItemOffset)
        maxCrossAxis = maxOf(maxCrossAxis, measuredItem.height)
        currentFirstVisibleItemOffset += measuredItem.width
        currentFirstVisibleItemIndex = previous
    }

    // if we were scrolled backward, but there were not enough items before. this means
    // not the whole scroll was consumed
    if (currentFirstVisibleItemOffset < 0) {
        val notConsumedScrollDelta = -currentFirstVisibleItemOffset
        currentFirstVisibleItemOffset = 0
    }

    /** 5) Compose forward. */
    var index = currentFirstVisibleItemIndex
    var currentMainAxisOffset = -currentFirstVisibleItemOffset

    // first we need to skip items we already composed while composing backward
    var indexInVisibleItems = 0
    while (indexInVisibleItems < placeables.size) {
        if (currentMainAxisOffset >= maxOffset) {
            // this item is out of the bounds and will not be visible.
            placeables.removeAt(indexInVisibleItems)
        } else {
            index++
            currentMainAxisOffset += placeables[indexInVisibleItems].first.width
            indexInVisibleItems++
        }
    }

    // then composing visible items forward until we fill the whole viewport.
    while (
        index < itemCount &&
            (currentMainAxisOffset < maxOffset ||
                currentMainAxisOffset <= 0 ||
                placeables.isEmpty())
    ) {
        val measuredItem = createItem(index, childConstraints, placeablesCache)
        val measuredItemPosition = currentMainAxisOffset

        currentMainAxisOffset += measuredItem.width

        if (currentMainAxisOffset <= 0 && index != itemCount - 1) {
            // this item is offscreen and will not be visible. advance firstVisibleItemIndex
            currentFirstVisibleItemIndex = index + 1
            currentFirstVisibleItemOffset -= measuredItem.width
        } else {
            maxCrossAxis = maxOf(maxCrossAxis, measuredItem.height)
            placeables.add(measuredItem to measuredItemPosition)
        }
        index++
    }

    val layoutWidth = containerConstraints.constrainWidth(currentMainAxisOffset)
    val layoutHeight = containerConstraints.constrainHeight(maxCrossAxis)

    /** 7) Update state information. */
    updatePositions(currentFirstVisibleItemIndex, currentFirstVisibleItemOffset)

    /** 8) Perform layout. */
    return layout(layoutWidth, layoutHeight) {
        // since this is a linear list all items are placed on the same cross-axis position
        for ((placeable, position) in placeables) {
            placeable.place(position, 0)
        }
    }
}

LazyLayout(
    modifier = Modifier.size(500.dp).scrollable(scrollableState, Orientation.Horizontal),
    itemProvider = itemProvider,
) { constraints ->
    // plug the measure policy, this is how we create and layout items.
    val placeablesCache = mutableIntObjectMapOf<Placeable>()
    measureLayout(
        scrollAmount, // will trigger a re-measure when it changes.
        firstVisibleItemIndex,
        firstVisibleItemOffset,
        items.size,
        placeablesCache,
        constraints,
    ) { newFirstVisibleItemIndex, newFirstVisibleItemOffset ->
        // update the information about the anchor item.
        firstVisibleItemIndex = newFirstVisibleItemIndex
        firstVisibleItemOffset = newFirstVisibleItemOffset

        // resets the scrolling state
        scrollAmount = 0f
    }
}
Parameters
itemProvider: () -> LazyLayoutItemProvider

lambda producing an item provider containing all the needed info about the items which could be used to compose and measure items as part of measurePolicy. This is the bridge between your item data source and the LazyLayout and is implemented as a lambda to promote a performant implementation. State backed implementations of LazyLayoutItemProvider are supported, though it is encouraged to implement this as an immutable entity that will return a new instance in case the dataset updates.

modifier: Modifier = Modifier

to apply on the layout

prefetchState: LazyLayoutPrefetchState? = null

allows to schedule items for prefetching. See LazyLayoutPrefetchState on how to control prefetching. Passing null will disable prefetching.

measurePolicy: LazyLayoutMeasurePolicy

Measure policy which allows to only compose and measure needed items.

LazyLayoutCacheWindow

@ExperimentalFoundationApi
fun LazyLayoutCacheWindow(ahead: Dp = 0.dp, behind: Dp = 0.dp): LazyLayoutCacheWindow

A Dp based LazyLayoutCacheWindow.

Parameters
ahead: Dp = 0.dp

The size of the ahead window to be used as per LazyLayoutCacheWindow.calculateAheadWindow.

behind: Dp = 0.dp

The size of the behind window to be used as per LazyLayoutCacheWindow.calculateBehindWindow.

LazyLayoutCacheWindow

@ExperimentalFoundationApi
fun LazyLayoutCacheWindow(
    aheadFraction: @FloatRange(from = 0.0) Float = 0.0f,
    behindFraction: @FloatRange(from = 0.0) Float = 0.0f
): LazyLayoutCacheWindow

Creates a LazyLayoutCacheWindow based off a fraction of the viewport.

Parameters
aheadFraction: @FloatRange(from = 0.0) Float = 0.0f

The fraction of the viewport to be used for the ahead window.

behindFraction: @FloatRange(from = 0.0) Float = 0.0f

The fraction of the viewport to be used for the behind window.

LazyLayoutPinnableItem

@Composable
fun LazyLayoutPinnableItem(
    key: Any?,
    index: Int,
    pinnedItemList: LazyLayoutPinnedItemList,
    content: @Composable () -> Unit
): Unit

Wrapper supporting PinnableContainer in lazy layout items. Each pinned item is considered important to keep alive even if it would be discarded otherwise.

Parameters
key: Any?

key of the item inside the lazy layout

index: Int

index of the item inside the lazy layout

pinnedItemList: LazyLayoutPinnedItemList

container of currently pinned items

content: @Composable () -> Unit

inner content of this item

Note: this function is a part of LazyLayout harness that allows for building custom lazy layouts. LazyLayout and all corresponding APIs are still under development and are subject to change.

getDefaultLazyLayoutKey

fun getDefaultLazyLayoutKey(index: Int): Any

This creates 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.

Note: this function is a part of LazyLayout harness that allows for building custom lazy layouts. LazyLayout and all corresponding APIs are still under development and are subject to change.