Списки и сетки

Многим приложениям необходимо отображать коллекции элементов. В этом документе объясняется, как можно эффективно сделать это в Jetpack Compose.

Если вы знаете, что ваш вариант использования не требует прокрутки, вы можете использовать простой Column или Row (в зависимости от направления) и выдавать содержимое каждого элемента, перебирая список следующим образом:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

Мы можем сделать Column прокручиваемым с помощью модификатораverticalScroll verticalScroll() .

Ленивые списки

Если вам нужно отобразить большое количество элементов (или список неизвестной длины), использование такого макета, как Column может вызвать проблемы с производительностью, поскольку все элементы будут составлены и размещены независимо от того, видимы они или нет.

Compose предоставляет набор компонентов, которые составляют и размещают только те элементы, которые видны в окне просмотра компонента. К этим компонентам относятся LazyColumn и LazyRow .

Как следует из названия, разница между LazyColumn и LazyRow заключается в ориентации, в которой они размещают свои элементы и прокручивают их. LazyColumn создает список с вертикальной прокруткой, а LazyRow создает список с горизонтальной прокруткой.

Компоненты Lazy отличаются от большинства макетов в Compose. Вместо принятия параметра блока контента @Composable , позволяющего приложениям напрямую создавать составные элементы, компоненты Lazy предоставляют блок LazyListScope.() . Этот блок LazyListScope предлагает DSL, который позволяет приложениям описывать содержимое элемента. Затем компонент Lazy отвечает за добавление содержимого каждого элемента в соответствии с требованиями макета и положения прокрутки.

LazyListScope DSL

DSL LazyListScope предоставляет ряд функций для описания элементов макета. В самом простом случае item() добавляет один элемент, а items(Int) — несколько элементов:

LazyColumn {
    // Add a single item
    item {
        Text(text = "First item")
    }

    // Add 5 items
    items(5) { index ->
        Text(text = "Item: $index")
    }

    // Add another single item
    item {
        Text(text = "Last item")
    }
}

Существует также ряд функций расширения, которые позволяют добавлять коллекции элементов, например List . Эти расширения позволяют нам легко перенести наш пример Column из приведенного выше:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

Существует также вариант функции расширения items() , называемый itemsIndexed() , который предоставляет индекс. Дополнительную информацию см. в справочнике LazyListScope .

Ленивые сетки

Составные элементы LazyVerticalGrid и LazyHorizontalGrid обеспечивают поддержку отображения элементов в сетке. Ленивая вертикальная сетка будет отображать свои элементы в вертикально прокручиваемом контейнере, распределенном по нескольким столбцам, в то время как ленивые горизонтальные сетки будут иметь такое же поведение на горизонтальной оси.

Сетки обладают теми же мощными возможностями API, что и списки, а также используют очень похожий DSL — LazyGridScope.() для описания содержимого.

Скриншот телефона, показывающий сетку фотографий.

Параметр columns в LazyVerticalGrid и параметр rows в LazyHorizontalGrid управляют тем, как ячейки формируются в столбцы или строки. В следующем примере элементы отображаются в сетке, используя GridCells.Adaptive чтобы задать ширину каждого столбца не менее 128.dp :

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid позволяет указать ширину элементов, после чего сетка будет соответствовать как можно большему количеству столбцов. Оставшаяся ширина распределяется поровну между столбцами после расчета количества столбцов. Этот адаптивный способ изменения размера особенно полезен для отображения наборов элементов на экранах разных размеров.

Если вы знаете точное количество используемых столбцов, вместо этого вы можете предоставить экземпляр GridCells.Fixed , содержащий количество необходимых столбцов.

Если ваш дизайн требует, чтобы только определенные элементы имели нестандартные размеры, вы можете использовать поддержку сетки для предоставления настраиваемых интервалов столбцов для элементов. Укажите диапазон столбца с помощью параметра span item LazyGridScope DSL и методов items . maxLineSpan , одно из значений области диапазона, особенно полезно при использовании адаптивного размера, поскольку количество столбцов не фиксировано. В этом примере показано, как предоставить полный диапазон строк:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Ленивая шахматная сетка

LazyVerticalStaggeredGrid и LazyHorizontalStaggeredGrid — это составные элементы, которые позволяют создавать лениво загружаемую шахматную сетку элементов. Ленивая вертикальная шахматная сетка отображает элементы в контейнере с вертикальной прокруткой, который охватывает несколько столбцов и позволяет отдельным элементам иметь разную высоту. Ленивые горизонтальные сетки ведут себя одинаково по горизонтальной оси с элементами разной ширины.

Следующий фрагмент представляет собой базовый пример использования LazyVerticalStaggeredGrid с шириной 200.dp для каждого элемента:

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Adaptive(200.dp),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)

Рисунок 1. Пример вертикальной сетки с ленивым шахматным расположением.

Чтобы установить фиксированное количество столбцов, вы можете использовать StaggeredGridCells.Fixed(columns) вместо StaggeredGridCells.Adaptive . При этом доступная ширина делится на количество столбцов (или строк для горизонтальной сетки) и каждый элемент занимает эту ширину (или высоту для горизонтальной сетки):

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(3),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)
Lazy staggered grid of images in Compose
Рисунок 2. Пример вертикальной сетки с ленивым шахматным расположением и фиксированными столбцами.

Заполнение контента

Иногда вам нужно добавить отступы по краям содержимого. Ленивые компоненты позволяют вам передавать некоторые PaddingValues ​​в параметр contentPadding для поддержки этого:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

В этом примере мы добавляем отступы 16.dp к горизонтальным краям (слева и справа), а затем 8.dp к верху и низу содержимого.

Обратите внимание, что это дополнение применяется к содержимому , а не к самому LazyColumn . В приведенном выше примере первый элемент добавит отступ 8.dp вверху, последний элемент добавит 8.dp внизу, и все элементы будут иметь отступы 16.dp слева и справа.

Расстояние между контентом

Чтобы добавить расстояние между элементами, вы можете использовать Arrangement.spacedBy() . В приведенном ниже примере между каждым элементом добавляется пространство размером 4.dp :

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Аналогично для LazyRow :

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Однако сетки допускают как вертикальное, так и горизонтальное расположение:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Ключи предметов

По умолчанию состояние каждого элемента привязано к положению элемента в списке или сетке. Однако это может вызвать проблемы в случае изменения набора данных, поскольку элементы, меняющие положение, фактически теряют любое запомненное состояние. Если вы представите себе сценарий LazyRow внутри LazyColumn , то если строка изменит положение элемента, пользователь потеряет свою позицию прокрутки внутри строки.

Чтобы бороться с этим, вы можете предоставить стабильный и уникальный ключ для каждого элемента, предоставив блок key параметру. Предоставление стабильного ключа позволяет обеспечить единообразие состояния элемента при изменениях набора данных:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Предоставляя ключи, вы помогаете Compose правильно обрабатывать переупорядочение. Например, если ваш элемент содержит запомненное состояние, установка ключей позволит Compose перемещать это состояние вместе с элементом при изменении его положения.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Однако есть одно ограничение на то, какие типы можно использовать в качестве ключей элементов. Тип ключа должен поддерживаться Bundle — механизмом Android для сохранения состояний при воссоздании действия. Bundle поддерживает такие типы, как примитивы, перечисления или Parcelables.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Ключ должен поддерживаться Bundle , чтобы rememberSaveable внутри составного элемента можно было восстановить при воссоздании действия или даже при прокрутке от этого элемента и прокрутке назад.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Анимация предметов

Если вы использовали виджет RecyclerView, вы знаете, что он автоматически анимирует изменения элементов . Ленивые макеты предоставляют ту же функциональность для изменения порядка элементов. API прост — вам просто нужно установить модификатор animateItemPlacement к содержимому элемента:

LazyColumn {
    // It is important to provide a key to each item to ensure animateItem() works as expected.
    items(books, key = { it.id }) {
        Row(Modifier.animateItem()) {
            // ...
        }
    }
}

Вы даже можете предоставить собственную спецификацию анимации, если вам необходимо:

LazyColumn {
    items(books, key = { it.id }) {
        Row(
            Modifier.animateItem(
                fadeInSpec = tween(durationMillis = 250),
                fadeOutSpec = tween(durationMillis = 100),
                placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
            )
        ) {
            // ...
        }
    }
}

Обязательно укажите ключи для своих элементов, чтобы можно было найти новую позицию для перемещаемого элемента.

Помимо изменения порядка, в настоящее время в разработке находится анимация элементов для добавления и удаления. Отследить прогресс можно в задаче 150812265 .

Прикрепленные заголовки (экспериментально)

Шаблон «прикрепленный заголовок» полезен при отображении списков сгруппированных данных. Ниже вы можете увидеть пример «списка контактов», сгруппированного по инициалам каждого контакта:

Видео, на котором телефон прокручивает список контактов вверх и вниз

Чтобы добиться закрепленного заголовка с помощью LazyColumn , вы можете использовать экспериментальную функцию stickyHeader() , предоставляющую содержимое заголовка:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Чтобы получить список с несколькими заголовками, как в приведенном выше примере «списка контактов», вы можете сделать:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

Реакция на положение прокрутки

Многим приложениям необходимо реагировать и прослушивать изменения положения прокрутки и макета элементов. Компоненты Lazy поддерживают этот вариант использования, поднимая LazyListState :

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

В простых случаях приложениям обычно достаточно знать только информацию о первом видимом элементе. Для этого LazyListState предоставляет свойства firstVisibleItemIndex и firstVisibleItemScrollOffset .

Если мы используем пример отображения и скрытия кнопки в зависимости от того, прокрутил ли пользователь первый элемент:

@Composable
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

Чтение состояния непосредственно в композиции полезно, когда вам нужно обновить другие составные элементы пользовательского интерфейса, но существуют также сценарии, в которых событие не требуется обрабатывать в той же композиции. Типичным примером этого является отправка аналитического события после того, как пользователь прокрутил страницу до определенной точки. Чтобы эффективно справиться с этим, мы можем использовать snapshotFlow() :

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState также предоставляет информацию обо всех элементах, отображаемых в данный момент, и их границах на экране через свойство layoutInfo . Дополнительную информацию см. в классе LazyListLayoutInfo .

Управление положением прокрутки

Помимо реакции на положение прокрутки, приложениям также полезно иметь возможность контролировать положение прокрутки. LazyListState поддерживает это с помощью функции scrollToItem() , которая «немедленно» фиксирует положение прокрутки, и animateScrollToItem() , которая прокручивает с помощью анимации (также известной как плавная прокрутка):

@Composable
fun MessageList(messages: List<Message>) {
    val listState = rememberLazyListState()
    // Remember a CoroutineScope to be able to launch
    val coroutineScope = rememberCoroutineScope()

    LazyColumn(state = listState) {
        // ...
    }

    ScrollToTopButton(
        onClick = {
            coroutineScope.launch {
                // Animate scroll to the first item
                listState.animateScrollToItem(index = 0)
            }
        }
    )
}

Большие наборы данных (пейджинг)

Библиотека подкачки позволяет приложениям поддерживать большие списки элементов, загружая и отображая небольшие фрагменты списка по мере необходимости. Paging 3.0 и более поздние версии обеспечивают поддержку Compose через библиотеку androidx.paging:paging-compose .

Чтобы отобразить список постраничного содержимого, мы можем использовать функцию расширения collectAsLazyPagingItems() , а затем передать возвращенные LazyPagingItems в items() в нашем LazyColumn . Подобно поддержке разбиения на страницы в представлениях, вы можете отображать заполнители во время загрузки данных, проверяя, имеет ли item null :

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val message = lazyPagingItems[index]
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

Советы по использованию ленивых макетов

Есть несколько советов, которые вы можете принять во внимание, чтобы ваши ленивые макеты работали должным образом.

Избегайте использования элементов размером 0 пикселей.

Это может произойти в сценариях, где, например, вы ожидаете асинхронно получить некоторые данные, например изображения, для заполнения элементов вашего списка на более позднем этапе. Это приведет к тому, что ленивый макет скомпонует все свои элементы при первом измерении, поскольку их высота равна 0 пикселей, и они все могут поместиться в области просмотра. После загрузки элементов и увеличения их высоты ленивые макеты отбрасывают все остальные элементы, которые были без необходимости составлены в первый раз, поскольку они фактически не могут поместиться в область просмотра. Чтобы избежать этого, вам следует установить для ваших элементов размеры по умолчанию, чтобы ленивый макет мог правильно рассчитать, сколько элементов фактически может поместиться в области просмотра:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Если вы знаете приблизительный размер ваших элементов после асинхронной загрузки данных, рекомендуется обеспечить одинаковый размер ваших элементов до и после загрузки, например, добавив несколько заполнителей. Это поможет сохранить правильное положение прокрутки.

Избегайте вложения компонентов, прокручиваемых в одном направлении.

Это применимо только к случаям вложения прокручиваемых дочерних элементов без предопределенного размера внутри другого прокручиваемого родительского элемента в том же направлении. Например, попытка вложить дочерний LazyColumn без фиксированной высоты в родительский Column с вертикальной прокруткой:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Вместо этого того же результата можно добиться, обернув все ваши составные элементы внутри одного родительского LazyColumn и используя его DSL для передачи различных типов контента. Это позволяет создавать отдельные элементы, а также несколько элементов списка, все в одном месте:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Имейте в виду, что допускаются случаи вложения макетов в разных направлениях, например прокручиваемый родительский Row и дочерний LazyColumn :

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

А также случаи, когда вы по-прежнему используете макеты того же направления, но также устанавливаете фиксированный размер для вложенных дочерних элементов:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Остерегайтесь размещения нескольких элементов в одном элементе.

В этом примере вторая лямбда элемента выдает 2 элемента в одном блоке:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

Ленивые макеты справятся с этим, как и ожидалось: они будут располагать элементы один за другим, как если бы это были разные элементы. Однако при этом есть несколько проблем.

Когда несколько элементов создаются как часть одного элемента, они обрабатываются как один объект, а это означает, что их больше нельзя составлять по отдельности. Если на экране становится видимым один элемент, то необходимо составить и измерить все элементы, соответствующие этому элементу. Это может снизить производительность при чрезмерном использовании. В крайнем случае размещение всех элементов в одном элементе полностью лишает смысла использование ленивых макетов. Помимо потенциальных проблем с производительностью, размещение большего количества элементов в одном элементе также будет мешать работе scrollToItem() и animateScrollToItem() .

Однако существуют допустимые варианты использования нескольких элементов в одном элементе, например наличие разделителей внутри списка. Вы не хотите, чтобы разделители меняли индексы прокрутки, поскольку их не следует считать независимыми элементами. Кроме того, производительность не пострадает, поскольку разделители небольшие. Разделитель, вероятно, должен быть видимым, когда элемент перед ним виден, чтобы он мог быть частью предыдущего элемента:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Рассмотрите возможность использования индивидуальных аранжировок

Обычно в ленивых списках много элементов, и они занимают размер, превышающий размер контейнера прокрутки. Однако, когда ваш список заполнен небольшим количеством элементов, ваш дизайн может иметь более конкретные требования к тому, как они должны располагаться в области просмотра.

Для этого вы можете использовать собственное вертикальное Arrangement и передать его в LazyColumn . В следующем примере объекту TopWithFooter необходимо реализовать только метод arrange . Во-первых, он будет располагать элементы один за другим. Во-вторых, если общая используемая высота ниже высоты области просмотра, нижний колонтитул будет располагаться внизу:

object TopWithFooter : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

Рассмотрите возможность добавления contentType

Начиная с Compose 1.2, чтобы максимизировать производительность вашего Lazy-макета, рассмотрите возможность добавления contentType в ваши списки или сетки. Это позволяет вам указать тип контента для каждого элемента макета в тех случаях, когда вы составляете список или сетку, состоящую из нескольких различных типов элементов:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

Когда вы предоставляете contentType , Compose может повторно использовать композиции только между элементами одного и того же типа. Поскольку повторное использование более эффективно при составлении элементов схожей структуры, предоставление типов содержимого гарантирует, что Compose не будет пытаться составить элемент типа A поверх совершенно другого элемента типа B. Это помогает максимизировать преимущества повторного использования композиции и производительность вашего ленивого макета.

Измерение производительности

Надежно измерить производительность ленивого макета можно только при работе в режиме выпуска и с включенной оптимизацией R8. В отладочных сборках ленивая прокрутка макета может работать медленнее. Для получения дополнительной информации об этом прочитайте Compose Performance .

{% дословно %} {% дословно %} {% дословно %} {% endverbatim %} ,

Многим приложениям необходимо отображать коллекции элементов. В этом документе объясняется, как можно эффективно сделать это в Jetpack Compose.

Если вы знаете, что ваш вариант использования не требует прокрутки, вы можете использовать простой Column или Row (в зависимости от направления) и выдавать содержимое каждого элемента, перебирая список следующим образом:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

Мы можем сделать Column прокручиваемым с помощью модификатораverticalScroll verticalScroll() .

Ленивые списки

Если вам нужно отобразить большое количество элементов (или список неизвестной длины), использование такого макета, как Column может вызвать проблемы с производительностью, поскольку все элементы будут составлены и размещены независимо от того, видимы они или нет.

Compose предоставляет набор компонентов, которые составляют и размещают только те элементы, которые видны в окне просмотра компонента. К этим компонентам относятся LazyColumn и LazyRow .

Как следует из названия, разница между LazyColumn и LazyRow заключается в ориентации, в которой они размещают свои элементы и прокручивают их. LazyColumn создает список с вертикальной прокруткой, а LazyRow создает список с горизонтальной прокруткой.

Компоненты Lazy отличаются от большинства макетов в Compose. Вместо принятия параметра блока контента @Composable , позволяющего приложениям напрямую создавать составные элементы, компоненты Lazy предоставляют блок LazyListScope.() . Этот блок LazyListScope предлагает DSL, который позволяет приложениям описывать содержимое элемента. Затем компонент Lazy отвечает за добавление содержимого каждого элемента в соответствии с требованиями макета и положения прокрутки.

LazyListScope DSL

DSL LazyListScope предоставляет ряд функций для описания элементов макета. В самом простом случае item() добавляет один элемент, а items(Int) — несколько элементов:

LazyColumn {
    // Add a single item
    item {
        Text(text = "First item")
    }

    // Add 5 items
    items(5) { index ->
        Text(text = "Item: $index")
    }

    // Add another single item
    item {
        Text(text = "Last item")
    }
}

Существует также ряд функций расширения, которые позволяют добавлять коллекции элементов, например List . Эти расширения позволяют нам легко перенести наш пример Column из приведенного выше:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

Существует также вариант функции расширения items() , называемый itemsIndexed() , который предоставляет индекс. Дополнительную информацию см. в справочнике LazyListScope .

Ленивые сетки

Составные элементы LazyVerticalGrid и LazyHorizontalGrid обеспечивают поддержку отображения элементов в сетке. Ленивая вертикальная сетка будет отображать свои элементы в вертикально прокручиваемом контейнере, распределенном по нескольким столбцам, в то время как ленивые горизонтальные сетки будут иметь такое же поведение на горизонтальной оси.

Сетки обладают теми же мощными возможностями API, что и списки, а также используют очень похожий DSL — LazyGridScope.() для описания содержимого.

Скриншот телефона, показывающий сетку фотографий.

Параметр columns в LazyVerticalGrid и параметр rows в LazyHorizontalGrid управляют тем, как ячейки формируются в столбцы или строки. В следующем примере элементы отображаются в сетке, используя GridCells.Adaptive чтобы задать ширину каждого столбца не менее 128.dp :

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid позволяет указать ширину элементов, после чего сетка будет соответствовать как можно большему количеству столбцов. Оставшаяся ширина распределяется поровну между столбцами после расчета количества столбцов. Этот адаптивный способ изменения размера особенно полезен для отображения наборов элементов на экранах разных размеров.

Если вы знаете точное количество используемых столбцов, вместо этого вы можете предоставить экземпляр GridCells.Fixed , содержащий количество необходимых столбцов.

Если ваш дизайн требует, чтобы только определенные элементы имели нестандартные размеры, вы можете использовать поддержку сетки для предоставления настраиваемых интервалов столбцов для элементов. Укажите диапазон столбца с помощью параметра span item LazyGridScope DSL и методов items . maxLineSpan , одно из значений области диапазона, особенно полезно при использовании адаптивного размера, поскольку количество столбцов не фиксировано. В этом примере показано, как предоставить полный диапазон строк:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Ленивая шахматная сетка

LazyVerticalStaggeredGrid и LazyHorizontalStaggeredGrid — это составные элементы, которые позволяют создавать лениво загружаемую шахматную сетку элементов. Ленивая вертикальная шахматная сетка отображает элементы в контейнере с вертикальной прокруткой, который охватывает несколько столбцов и позволяет отдельным элементам иметь разную высоту. Ленивые горизонтальные сетки ведут себя одинаково по горизонтальной оси с элементами разной ширины.

Следующий фрагмент представляет собой базовый пример использования LazyVerticalStaggeredGrid с шириной 200.dp для каждого элемента:

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Adaptive(200.dp),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)

Рисунок 1. Пример вертикальной сетки с ленивым шахматным расположением.

Чтобы установить фиксированное количество столбцов, вы можете использовать StaggeredGridCells.Fixed(columns) вместо StaggeredGridCells.Adaptive . При этом доступная ширина делится на количество столбцов (или строк для горизонтальной сетки), и каждый элемент занимает эту ширину (или высоту для горизонтальной сетки):

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(3),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)
Lazy staggered grid of images in Compose
Рисунок 2. Пример вертикальной сетки с ленивым шахматным расположением и фиксированными столбцами.

Заполнение контента

Иногда вам нужно добавить отступы по краям содержимого. Ленивые компоненты позволяют вам передавать некоторые PaddingValues ​​в параметр contentPadding для поддержки этого:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

В этом примере мы добавляем отступы 16.dp к горизонтальным краям (слева и справа), а затем 8.dp к верху и низу содержимого.

Обратите внимание, что это дополнение применяется к содержимому , а не к самому LazyColumn . В приведенном выше примере первый элемент добавит отступ 8.dp вверху, последний элемент добавит 8.dp внизу, и все элементы будут иметь отступы 16.dp слева и справа.

Расстояние между контентом

Чтобы добавить расстояние между элементами, вы можете использовать Arrangement.spacedBy() . В приведенном ниже примере между каждым элементом добавляется пространство размером 4.dp :

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Аналогично для LazyRow :

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Однако сетки допускают как вертикальное, так и горизонтальное расположение:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Ключи предметов

По умолчанию состояние каждого элемента привязано к положению элемента в списке или сетке. Однако это может вызвать проблемы в случае изменения набора данных, поскольку элементы, меняющие положение, фактически теряют любое запомненное состояние. Если вы представите себе сценарий LazyRow внутри LazyColumn , то если строка изменит положение элемента, пользователь потеряет свою позицию прокрутки внутри строки.

Чтобы бороться с этим, вы можете предоставить стабильный и уникальный ключ для каждого элемента, предоставив блок key параметру. Предоставление стабильного ключа позволяет обеспечить единообразие состояния элемента при изменениях набора данных:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Предоставляя ключи, вы помогаете Compose правильно обрабатывать переупорядочение. Например, если ваш элемент содержит запомненное состояние, установка ключей позволит Compose перемещать это состояние вместе с элементом при изменении его положения.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Однако есть одно ограничение на то, какие типы можно использовать в качестве ключей элементов. Тип ключа должен поддерживаться Bundle — механизмом Android для сохранения состояний при воссоздании действия. Bundle поддерживает такие типы, как примитивы, перечисления или Parcelables.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Ключ должен поддерживаться Bundle , чтобы rememberSaveable внутри составного элемента можно было восстановить при воссоздании действия или даже при прокрутке от этого элемента и прокрутке назад.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Анимация предметов

Если вы использовали виджет RecyclerView, вы знаете, что он автоматически анимирует изменения элементов . Ленивые макеты предоставляют ту же функциональность для изменения порядка элементов. API прост — вам просто нужно установить модификатор animateItemPlacement к содержимому элемента:

LazyColumn {
    // It is important to provide a key to each item to ensure animateItem() works as expected.
    items(books, key = { it.id }) {
        Row(Modifier.animateItem()) {
            // ...
        }
    }
}

Вы даже можете предоставить собственную спецификацию анимации, если вам необходимо:

LazyColumn {
    items(books, key = { it.id }) {
        Row(
            Modifier.animateItem(
                fadeInSpec = tween(durationMillis = 250),
                fadeOutSpec = tween(durationMillis = 100),
                placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
            )
        ) {
            // ...
        }
    }
}

Обязательно укажите ключи для своих элементов, чтобы можно было найти новую позицию для перемещаемого элемента.

Помимо изменения порядка, в настоящее время в разработке находится анимация элементов для добавления и удаления. Отследить прогресс можно в задаче 150812265 .

Прикрепленные заголовки (экспериментально)

Шаблон «прикрепленный заголовок» полезен при отображении списков сгруппированных данных. Ниже вы можете увидеть пример «списка контактов», сгруппированного по инициалам каждого контакта:

Видео, на котором телефон прокручивает список контактов вверх и вниз

Чтобы добиться закрепленного заголовка с помощью LazyColumn , вы можете использовать экспериментальную функцию stickyHeader() , предоставляющую содержимое заголовка:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Чтобы получить список с несколькими заголовками, как в приведенном выше примере «списка контактов», вы можете сделать:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

Реакция на положение прокрутки

Многим приложениям необходимо реагировать и прослушивать изменения положения прокрутки и макета элементов. Компоненты Lazy поддерживают этот вариант использования, поднимая LazyListState :

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

В простых случаях приложениям обычно достаточно знать только информацию о первом видимом элементе. Для этого LazyListState предоставляет свойства firstVisibleItemIndex и firstVisibleItemScrollOffset .

Если мы используем пример отображения и скрытия кнопки в зависимости от того, прокрутил ли пользователь первый элемент:

@Composable
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

Чтение состояния непосредственно в композиции полезно, когда вам нужно обновить другие составные элементы пользовательского интерфейса, но существуют также сценарии, в которых событие не требуется обрабатывать в той же композиции. Типичным примером этого является отправка аналитического события после того, как пользователь прокрутил страницу до определенной точки. Чтобы эффективно справиться с этим, мы можем использовать snapshotFlow() :

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState также предоставляет информацию обо всех элементах, отображаемых в данный момент, и их границах на экране через свойство layoutInfo . Дополнительную информацию см. в классе LazyListLayoutInfo .

Управление положением прокрутки

Помимо реакции на положение прокрутки, приложениям также полезно иметь возможность контролировать положение прокрутки. LazyListState поддерживает это с помощью функции scrollToItem() , которая «немедленно» фиксирует положение прокрутки, и animateScrollToItem() , которая прокручивает с помощью анимации (также известной как плавная прокрутка):

@Composable
fun MessageList(messages: List<Message>) {
    val listState = rememberLazyListState()
    // Remember a CoroutineScope to be able to launch
    val coroutineScope = rememberCoroutineScope()

    LazyColumn(state = listState) {
        // ...
    }

    ScrollToTopButton(
        onClick = {
            coroutineScope.launch {
                // Animate scroll to the first item
                listState.animateScrollToItem(index = 0)
            }
        }
    )
}

Большие наборы данных (пейджинг)

Библиотека подкачки позволяет приложениям поддерживать большие списки элементов, загружая и отображая небольшие фрагменты списка по мере необходимости. Paging 3.0 и более поздние версии обеспечивают поддержку Compose через библиотеку androidx.paging:paging-compose .

Чтобы отобразить список постраничного содержимого, мы можем использовать функцию расширения collectAsLazyPagingItems() , а затем передать возвращенные LazyPagingItems в items() в нашем LazyColumn . Подобно поддержке разбиения на страницы в представлениях, вы можете отображать заполнители во время загрузки данных, проверяя, имеет ли item null :

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val message = lazyPagingItems[index]
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

Советы по использованию ленивых макетов

Есть несколько советов, которые вы можете принять во внимание, чтобы ваши ленивые макеты работали должным образом.

Избегайте использования элементов размером 0 пикселей.

Это может произойти в сценариях, где, например, вы ожидаете асинхронно получить некоторые данные, например изображения, для заполнения элементов вашего списка на более позднем этапе. Это приведет к тому, что ленивый макет будет компоновать все свои элементы при первом измерении, поскольку их высота равна 0 пикселей, и они все могут поместиться в области просмотра. После загрузки элементов и увеличения их высоты ленивые макеты отбрасывают все остальные элементы, которые были без необходимости составлены в первый раз, поскольку они фактически не могут поместиться в область просмотра. Чтобы избежать этого, вам следует установить для ваших элементов размеры по умолчанию, чтобы ленивый макет мог правильно рассчитать, сколько элементов фактически может поместиться в области просмотра:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Если вы знаете приблизительный размер ваших элементов после асинхронной загрузки данных, рекомендуется обеспечить одинаковый размер ваших элементов до и после загрузки, например, добавив несколько заполнителей. Это поможет сохранить правильное положение прокрутки.

Избегайте вложения компонентов, прокручиваемых в одном направлении.

Это применимо только к случаям вложения прокручиваемых дочерних элементов без предопределенного размера внутри другого прокручиваемого родительского элемента в том же направлении. Например, попытка вложить дочерний LazyColumn без фиксированной высоты в родительский Column с вертикальной прокруткой:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Вместо этого того же результата можно добиться, обернув все ваши составные элементы внутри одного родительского LazyColumn и используя его DSL для передачи различных типов контента. Это позволяет создавать отдельные элементы, а также несколько элементов списка, все в одном месте:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Имейте в виду, что допускаются случаи вложения макетов в разных направлениях, например прокручиваемый родительский Row и дочерний LazyColumn :

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

А также случаи, когда вы по-прежнему используете макеты того же направления, но также устанавливаете фиксированный размер для вложенных дочерних элементов:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Остерегайтесь размещения нескольких элементов в одном элементе.

В этом примере вторая лямбда элемента выдает 2 элемента в одном блоке:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

Ленивые макеты справятся с этим, как и ожидалось: они будут располагать элементы один за другим, как если бы это были разные элементы. Однако при этом есть несколько проблем.

Когда несколько элементов создаются как часть одного элемента, они обрабатываются как один объект, а это означает, что их больше нельзя составлять по отдельности. Если на экране становится видимым один элемент, то необходимо составить и измерить все элементы, соответствующие этому элементу. Это может снизить производительность при чрезмерном использовании. В крайнем случае размещение всех элементов в одном элементе полностью лишает смысла использование ленивых макетов. Помимо потенциальных проблем с производительностью, размещение большего количества элементов в одном элементе также будет мешать работе scrollToItem() и animateScrollToItem() .

Однако существуют допустимые варианты использования нескольких элементов в одном элементе, например наличие разделителей внутри списка. Вы не хотите, чтобы разделители меняли индексы прокрутки, поскольку их не следует считать независимыми элементами. Кроме того, производительность не пострадает, поскольку разделители небольшие. Разделитель, вероятно, должен быть видимым, когда элемент перед ним виден, чтобы он мог быть частью предыдущего элемента:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Рассмотрите возможность использования индивидуальных аранжировок

Обычно в ленивых списках много элементов, и они занимают размер, превышающий размер контейнера прокрутки. Однако, когда ваш список заполнен небольшим количеством элементов, ваш дизайн может иметь более конкретные требования к тому, как они должны располагаться в области просмотра.

Чтобы достичь этого, вы можете использовать пользовательское вертикальное Arrangement и передать его в LazyColumn . В следующем примере объект TopWithFooter должен только реализовать метод arrange . Во -первых, он будет позиционировать предметы один за другим. Во -вторых, если общая используемая высота ниже высоты видового порта, он поместит нижний колонтитул внизу:

object TopWithFooter : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

Подумайте о добавлении contentType

Начиная с Compose 1.2, чтобы максимизировать производительность вашего ленивого макета, рассмотрите возможность добавления contentType в ваши списки или сетки. Это позволяет вам указать тип контента для каждого элемента макета, в тех случаях, когда вы сочиняете список или сетку, состоящую из нескольких различных типов элементов:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

Когда вы предоставляете contentType , Compose может повторно использовать композиции только между элементами того же типа. Поскольку повторное использование является более эффективным, когда вы сочиняете элементы аналогичной структуры, при условии, что типы контента гарантируют, что композиция не пытается составить элемент типа A в дополнение к совершенно другому элементу типа B. Это помогает максимизировать преимущества повторного использования композиции и Ваше ленивое выступление макета.

Измерение производительности

Вы можете надежно измерить производительность ленивого макета при запуске в режиме выпуска и с включенной оптимизацией R8. При сборке отладки ленивая прокрутка макета может показаться медленной. Для получения дополнительной информации об этом прочитайте через Compose Performance .

{% дословно %} {% дословно %} {% дословно %} {% endverbatim %} ,

Многие приложения должны отображать коллекции элементов. Этот документ объясняет, как вы можете эффективно сделать это в JetPack Compose.

Если вы знаете, что ваш вариант использования не требует какого -либо прокрутки, вы можете использовать простой Column или Row (в зависимости от направления) и излучить содержание каждого элемента, итерация над списком следующим образом:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

Мы можем заставить Column прокрутить, используя модификатор verticalScroll() .

Ленивые списки

Если вам нужно отобразить большое количество элементов (или список неизвестной длины), использование такого макета, как Column может вызвать проблемы с производительностью, поскольку все элементы будут составлены и изложены, независимо от того, видны ли они.

Compose предоставляет набор компонентов, которые составляют только элементы, которые видны в просмотре компонента. Эти компоненты включают LazyColumn и LazyRow .

Как следует из названия, разница между LazyColumn и LazyRow заключается в ориентации, в которой они излагают свои предметы и свиток. LazyColumn создает вертикальный список прокрутки, а LazyRow составляет горизонтальный список прокрутки.

Ленивые компоненты отличаются от большинства макетов в Compose. Вместо того, чтобы принимать параметр блока содержимого @Composable , позволяя приложениям напрямую излучать композиции, ленивые компоненты обеспечивают блок LazyListScope.() . Этот блок LazyListScope предлагает DSL, который позволяет приложениям описывать содержимое элемента. Ленивый компонент затем отвечает за добавление содержимого каждого элемента, как того требует макет и положение прокрутки.

LazyListScope DSL

DSL LazyListScope предоставляет ряд функций для описания элементов в макете. В самом простом, item() добавляет один элемент, а items(Int) добавляет несколько элементов:

LazyColumn {
    // Add a single item
    item {
        Text(text = "First item")
    }

    // Add 5 items
    items(5) { index ->
        Text(text = "Item: $index")
    }

    // Add another single item
    item {
        Text(text = "Last item")
    }
}

Существует также ряд функций расширения, которые позволяют добавлять коллекции элементов, такие как List . Эти расширения позволяют нам легко перенести пример нашего Column сверху:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

Существует также вариант функции расширения items() с именем itemsIndexed() , которая предоставляет индекс. Пожалуйста, смотрите ссылку LazyListScope для получения более подробной информации.

Ленивые сетки

Композиции LazyVerticalGrid и LazyHorizontalGrid обеспечивают поддержку отображения элементов в сетке. Ленивая вертикальная сетка будет отображать свои элементы в вертикально прокручиваемом контейнере, охватываемой по нескольким столбцам, в то время как ленивые горизонтальные сетки будут иметь одинаковое поведение на горизонтальной оси.

Сетки имеют те же мощные возможности API, что и списки, и они также используют очень похожий DSL - LazyGridScope.() Для описания контента.

Скриншот телефона, показывающий сетку фотографий

Параметр columns в параметре LazyVerticalGrid и rows в LazyHorizontalGrid Control, как ячейки образуются в столбцы или строки. В следующем примере отображаются элементы в сетке, используя GridCells.Adaptive для установки каждого столбца как минимум 128.dp шириной:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid позволяет вам указать ширину для элементов, а затем сетка будет соответствовать как можно большему количеству столбцов. Любая оставшаяся ширина распределяется в равной степени между столбцами после рассчитываемого количества столбцов. Этот адаптивный способ размеров особенно полезен для отображения наборов элементов в разных размерах экрана.

Если вы знаете точное количество используемых столбцов, вы можете вместо этого предоставить экземпляр GridCells.Fixed , содержащий количество необходимых столбцов.

Если ваш дизайн требует, чтобы только определенные элементы имели нестандартные размеры, вы можете использовать поддержку сетки для предоставления пользовательских пролетов столбцов для элементов. Укажите промежуток столбца с параметром span LazyGridScope DSL item и методов items . maxLineSpan , одно из значений Span Scope, особенно полезно, когда вы используете адаптивные размеры, потому что количество столбцов не установлено. В этом примере показано, как обеспечить полный промежуток строки:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Ленивая шахматная сетка

LazyVerticalStaggeredGrid и LazyHorizontalStaggeredGrid -это композиции, которые позволяют вам создавать ленивую, ошеломленную сетку предметов. Ленивая вертикальная шахматная сетка отображает свои элементы в вертикально прокручиваемом контейнере, который проходит по нескольким столбцам и позволяет отдельным элементам быть разными высотами. Ленивые горизонтальные сетки имеют одинаковое поведение на горизонтальной оси с предметами разной ширины.

Следующий фрагмент является основным примером использования LazyVerticalStaggeredGrid с шириной 200.dp на элемент:

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Adaptive(200.dp),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)

Рисунок 1. Пример ленивой ошеломленной вертикальной сетки

Чтобы установить фиксированное количество столбцов, вы можете использовать StaggeredGridCells.Fixed(columns) вместо StaggeredGridCells.Adaptive . Это делит доступную ширину на количество столбцов (или рядов для горизонтальной сетки), и каждый предмет забирает эту ширину (или высоту для горизонтальной сетки):

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(3),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)
Lazy staggered grid of images in Compose
Рисунок 2. Пример ленивой ошеломленной вертикальной сетки с фиксированными столбцами

Заполнение контента

Иногда вам нужно добавить накладку по краям контента. Ленивые компоненты позволяют вам передать некоторые PaddingValues в параметр contentPadding , чтобы поддержать это:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

В этом примере мы добавляем 16.dp прокладки в горизонтальные края (слева и справа), а затем 8.dp в верхнюю и нижнюю часть содержания.

Обратите внимание, что эта заполнение применяется к контенту , а не к самой LazyColumn . В приведенном выше примере первый элемент добавит 8.dp Padding в верхнюю часть, последний элемент добавит 8.dp внизу, и все элементы будут иметь 16.dp -заполнение слева и справа.

Расстояние между контентом

Чтобы добавить интервал между элементами, вы можете использовать Arrangement.spacedBy() . Пример ниже добавляет 4.dp пространства между каждым элементом:

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Точно так же для LazyRow :

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Сетки, однако, принимают как вертикальные, так и горизонтальные меры:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Ключи элемента

По умолчанию состояние каждого элемента направлено на позицию элемента в списке или сетке. Тем не менее, это может вызвать проблемы, если набор данных изменяется, поскольку элементы, которые эффективно изменяют позицию, теряют любое запоминающееся состояние. Если вы представляете сценарий LazyRow в пределах LazyColumn , если строка изменит позицию элемента, пользователь потеряет свою позицию прокрутки в строке.

Чтобы бороться с этим, вы можете предоставить стабильный и уникальный ключ для каждого элемента, предоставляя блок параметру key . Предоставление стабильного ключа позволяет состоянию элемента быть согласованным между изменениями набора данных:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Предоставляя ключи, вы помогаете правильно обработать повторные изменения. Например, если ваш элемент содержит запоминающееся состояние, настройка клавиш позволила бы композиции перемещать это состояние вместе с элементом, когда его положение изменяется.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Тем не менее, есть одно ограничение на то, какие типы вы можете использовать в качестве клавиш элементов. Тип ключа должен поддерживаться Bundle , механизм Android для сохранения состояний при воссоздании деятельности. Bundle поддерживает такие типы, как примитивы, перечисление или посылки.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Ключ должен поддерживаться Bundle , чтобы rememberSaveable о композитном элементе могут быть восстановлены при воссоздании деятельности, или даже когда вы прокручиваете от этого предмета и прокрутите обратно.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Анимация предметов

Если вы использовали виджет Recyclerview, вы будете знать, что он анимирует элементы автоматически. Ленивые макеты предоставляют такую ​​же функциональность для повторных порядок предметов. API прост - вам просто нужно установить модификатор animateItemPlacement для содержимого элемента:

LazyColumn {
    // It is important to provide a key to each item to ensure animateItem() works as expected.
    items(books, key = { it.id }) {
        Row(Modifier.animateItem()) {
            // ...
        }
    }
}

Вы даже можете предоставить пользовательскую спецификацию анимации, если вам нужно:

LazyColumn {
    items(books, key = { it.id }) {
        Row(
            Modifier.animateItem(
                fadeInSpec = tween(durationMillis = 250),
                fadeOutSpec = tween(durationMillis = 100),
                placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
            )
        ) {
            // ...
        }
    }
}

Убедитесь, что вы предоставили ключи для своих предметов, чтобы можно было найти новую позицию для перемещенного элемента.

Помимо повторных порядок, анимация предметов для дополнений и удаления в настоящее время находится в разработке. Вы можете отслеживать прогресс в выпуске 150812265 .

Липкие заголовки (экспериментальные)

Паттерн «липкий заголовок» полезен при отображении списков сгруппированных данных. Ниже вы можете увидеть пример «списка контактов», сгруппированный по начальному контакту:

Видео телефона прокрутки вверх и вниз через список контактов

Чтобы достичь липкого заголовка с LazyColumn , вы можете использовать функцию Experimental stickyHeader() , обеспечивая содержание заголовка:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Чтобы получить список с несколькими заголовками, например, пример «Список контактов», вы можете сделать:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

Реагировать на положение прокрутки

Многие приложения должны отреагировать и прослушать позицию прокрутки и изменения макета элемента. Ленивые компоненты поддерживают этот вариант использования путем поднятия LazyListState :

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

Для простых случаев использования приложения обычно должны знать только информацию о первом видимом элементе. Для этого LazyListState предоставляет firstVisibleItemIndex и firstVisibleItemScrollOffset .

Если мы используем пример отображения и сокрытия кнопки в зависимости от того, просмотрел ли пользователь первый элемент:

@Composable
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

Чтение состояния непосредственно в композиции полезно, когда вам нужно обновить другие композиции пользовательского интерфейса, но есть также сценарии, в которых событие не нужно обрабатывать в той же композиции. Распространенным примером этого является отправка аналитического события после того, как пользователь прокрутит определенную точку. Чтобы эффективно справиться с этим, мы можем использовать snapshotFlow() :

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState также предоставляет информацию обо всех отображаемых в настоящее время элементах и ​​их границах на экране, через свойство layoutInfo . Смотрите класс LazyListLayoutInfo для получения дополнительной информации.

Управление положением прокрутки

Помимо реагирования на положение прокрутки, также полезно для приложений, чтобы контролировать положение прокрутки. LazyListState поддерживает это с помощью функции scrollToItem() , которая «немедленно» набирает позицию прокрутки, и animateScrollToItem() , который прокручивается с использованием анимации (также известной как плавный прокруток):

@Composable
fun MessageList(messages: List<Message>) {
    val listState = rememberLazyListState()
    // Remember a CoroutineScope to be able to launch
    val coroutineScope = rememberCoroutineScope()

    LazyColumn(state = listState) {
        // ...
    }

    ScrollToTopButton(
        onClick = {
            coroutineScope.launch {
                // Animate scroll to the first item
                listState.animateScrollToItem(index = 0)
            }
        }
    )
}

Большие наборы данных (пейджинг)

Библиотека Paging позволяет приложениям поддерживать большие списки элементов, загружать и отображать небольшие куски списка по мере необходимости. Paging 3.0, а затем обеспечивает поддержку Compose через библиотеку androidx.paging:paging-compose .

Чтобы отобразить список страхового контента, мы можем использовать функцию расширения collectAsLazyPagingItems() , а затем передавать возвращенные LazyPagingItems в items() в нашей LazyColumn . Подобно поддержке пейджинга в представлениях, вы можете отображать заполнители, когда нагружает данные, проверяя, является ли item null :

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val message = lazyPagingItems[index]
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

Советы по использованию ленивых макетов

Есть несколько советов, которые вы можете принять во внимание, чтобы убедиться, что ваши ленивые макеты работают, как и предполагалось.

Избегайте использования элементов размером 0 пикселей

Это может произойти в сценариях, где, например, вы рассчитываете асинхронно получить некоторые данные, такие как изображения, чтобы заполнить элементы вашего списка на более позднем этапе. Это приведет к тому, что ленивый макет сочинит все свои элементы в первом измерении, так как их высота составляет 0 пикселей, и он может поместить их всех в точку зрения. После того, как предметы загрузились и их высота расширились, ленивые макеты затем отбросили все другие предметы, которые излишне были составлены в первый раз, поскольку они на самом деле не могут соответствовать видовому доку. Чтобы избежать этого, вы должны установить размеры по умолчанию в свои элементы, чтобы ленивый макет мог сделать правильный расчет того, сколько элементов может на самом деле соответствовать в виде просмотра:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Когда вы знаете, что приблизительный размер ваших элементов после асинхронно загружены, хорошая практика - убедиться, что размеры ваших элементов остаются прежними до и после загрузки, например, добавив некоторые заполнители. Это поможет поддерживать правильную позицию прокрутки.

Избегайте гнездования компонентов, прокручиваемых в том же направлении

Это относится только к случаям при гнездовании прокручиваемых детей без предопределенного размера внутри другого и того же направления, прокручиваемого родителем. Например, пытаясь гнездовать ребенка LazyColumn без фиксированной высоты внутри вертикально прокручиваемой Column Родитель:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Вместо этого один и тот же результат может быть достигнут путем завершения всех ваших композиционных продуктов внутри одного родителя LazyColumn и использования его DSL для прохождения различных типов контента. Это позволяет излучать отдельные элементы, а также несколько элементов списка, все в одном месте:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Имейте в виду, что случаи, когда вы гнездовываете различные макеты направления, например, прокручиваемая родительская Row и детская LazyColumn , разрешены:

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

А также случаи, когда вы все еще используете одни и те же макеты направления, но также устанавливаете фиксированный размер для вложенных детей:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Остерегайтесь размещения нескольких элементов в один элемент

В этом примере второй пункт Lambda издает 2 элемента в одном блоке:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

Ленивые макеты будут справляться с этим, как и ожидалось - они выкладывают элементы один за другим, как если бы они были разными предметами. Тем не менее, есть несколько проблем с этим.

Когда несколько элементов испускаются как часть одного элемента, они обрабатываются как одно сущность, а это означает, что они больше не могут быть составлены индивидуально. Если один элемент становится видимым на экране, то все элементы, соответствующие элементу, должны быть составлены и измерены. Это может повредить производительности при чрезмерном использовании. В крайнем случае размещения всех элементов в один элемент полностью побеждает цель использования ленивых макетов. Помимо потенциальных проблем с производительностью, размещение большего количества элементов в одном элементе также будет мешать scrollToItem() и animateScrollToItem() .

Тем не менее, существуют достоверные варианты использования для размещения нескольких элементов в один элемент, такие как разделители в списке. Вы не хотите, чтобы разделители изменили индексы прокрутки, так как их не следует считать независимыми элементами. Кроме того, производительность не будет затронута, поскольку разделители невелики. Разделитель, вероятно, должен быть видимым, когда предмет до его видимого, поэтому он может быть частью предыдущего пункта:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Подумайте об использовании пользовательских договоренностей

Обычно у ленивых списков есть много предметов, и они занимают больше, чем размер контейнера прокрутки. Однако, когда ваш список заполнен несколькими предметами, ваш дизайн может предпринять более конкретные требования к тому, как они должны быть расположены в точке зрения.

Чтобы достичь этого, вы можете использовать пользовательское вертикальное Arrangement и передать его в LazyColumn . В следующем примере объект TopWithFooter должен только реализовать метод arrange . Во -первых, он будет позиционировать предметы один за другим. Во -вторых, если общая используемая высота ниже высоты видового порта, он поместит нижний колонтитул внизу:

object TopWithFooter : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

Подумайте о добавлении contentType

Начиная с Compose 1.2, чтобы максимизировать производительность вашего ленивого макета, рассмотрите возможность добавления contentType в ваши списки или сетки. Это позволяет вам указать тип контента для каждого элемента макета, в тех случаях, когда вы сочиняете список или сетку, состоящую из нескольких различных типов элементов:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

Когда вы предоставляете contentType , Compose может повторно использовать композиции только между элементами того же типа. Поскольку повторное использование является более эффективным, когда вы сочиняете элементы аналогичной структуры, при условии, что типы контента гарантируют, что композиция не пытается составить элемент типа A в дополнение к совершенно другому элементу типа B. Это помогает максимизировать преимущества повторного использования композиции и Ваше ленивое выступление макета.

Измерение производительности

Вы можете надежно измерить производительность ленивого макета при запуске в режиме выпуска и с включенной оптимизацией R8. При сборке отладки ленивая прокрутка макета может показаться медленной. Для получения дополнительной информации об этом прочитайте через Compose Performance .

{% дословно %} {% дословно %} {% дословно %} {% endverbatim %} ,

Многие приложения должны отображать коллекции элементов. Этот документ объясняет, как вы можете эффективно сделать это в JetPack Compose.

Если вы знаете, что ваш вариант использования не требует какого -либо прокрутки, вы можете использовать простой Column или Row (в зависимости от направления) и излучить содержание каждого элемента, итерация над списком следующим образом:

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

Мы можем заставить Column прокрутить, используя модификатор verticalScroll() .

Ленивые списки

Если вам нужно отобразить большое количество элементов (или список неизвестной длины), использование такого макета, как Column может вызвать проблемы с производительностью, поскольку все элементы будут составлены и изложены, независимо от того, видны ли они.

Compose предоставляет набор компонентов, которые составляют только элементы, которые видны в просмотре компонента. Эти компоненты включают LazyColumn и LazyRow .

Как следует из названия, разница между LazyColumn и LazyRow заключается в ориентации, в которой они излагают свои предметы и свиток. LazyColumn создает вертикальный список прокрутки, а LazyRow составляет горизонтальный список прокрутки.

Ленивые компоненты отличаются от большинства макетов в Compose. Вместо того, чтобы принимать параметр блока содержимого @Composable , позволяя приложениям напрямую излучать композиции, ленивые компоненты обеспечивают блок LazyListScope.() . Этот блок LazyListScope предлагает DSL, который позволяет приложениям описывать содержимое элемента. Ленивый компонент затем отвечает за добавление содержимого каждого элемента, как того требует макет и положение прокрутки.

LazyListScope DSL

DSL LazyListScope предоставляет ряд функций для описания элементов в макете. В самом простом, item() добавляет один элемент, а items(Int) добавляет несколько элементов:

LazyColumn {
    // Add a single item
    item {
        Text(text = "First item")
    }

    // Add 5 items
    items(5) { index ->
        Text(text = "Item: $index")
    }

    // Add another single item
    item {
        Text(text = "Last item")
    }
}

Существует также ряд функций расширения, которые позволяют добавлять коллекции элементов, такие как List . Эти расширения позволяют нам легко перенести пример нашего Column сверху:

/**
 * import androidx.compose.foundation.lazy.items
 */
LazyColumn {
    items(messages) { message ->
        MessageRow(message)
    }
}

Существует также вариант функции расширения items() с именем itemsIndexed() , которая предоставляет индекс. Пожалуйста, смотрите ссылку LazyListScope для получения более подробной информации.

Ленивые сетки

Композиции LazyVerticalGrid и LazyHorizontalGrid обеспечивают поддержку отображения элементов в сетке. Ленивая вертикальная сетка будет отображать свои элементы в вертикально прокручиваемом контейнере, охватываемой по нескольким столбцам, в то время как ленивые горизонтальные сетки будут иметь одинаковое поведение на горизонтальной оси.

Сетки имеют те же мощные возможности API, что и списки, и они также используют очень похожий DSL - LazyGridScope.() Для описания контента.

Скриншот телефона, показывающий сетку фотографий

Параметр columns в параметре LazyVerticalGrid и rows в LazyHorizontalGrid Control, как ячейки образуются в столбцы или строки. В следующем примере отображаются элементы в сетке, используя GridCells.Adaptive для установки каждого столбца как минимум 128.dp шириной:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
) {
    items(photos) { photo ->
        PhotoItem(photo)
    }
}

LazyVerticalGrid позволяет вам указать ширину для элементов, а затем сетка будет соответствовать как можно большему количеству столбцов. Любая оставшаяся ширина распределяется в равной степени между столбцами после рассчитываемого количества столбцов. Этот адаптивный способ размеров особенно полезен для отображения наборов элементов в разных размерах экрана.

Если вы знаете точное количество используемых столбцов, вы можете вместо этого предоставить экземпляр GridCells.Fixed , содержащий количество необходимых столбцов.

Если ваш дизайн требует, чтобы только определенные элементы имели нестандартные размеры, вы можете использовать поддержку сетки для предоставления пользовательских пролетов столбцов для элементов. Укажите промежуток столбца с параметром span LazyGridScope DSL item и методов items . maxLineSpan , одно из значений Span Scope, особенно полезно, когда вы используете адаптивные размеры, потому что количество столбцов не установлено. В этом примере показано, как обеспечить полный промежуток строки:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 30.dp)
) {
    item(span = {
        // LazyGridItemSpanScope:
        // maxLineSpan
        GridItemSpan(maxLineSpan)
    }) {
        CategoryCard("Fruits")
    }
    // ...
}

Ленивая шахматная сетка

LazyVerticalStaggeredGrid и LazyHorizontalStaggeredGrid -это композиции, которые позволяют вам создавать ленивую, ошеломленную сетку предметов. Ленивая вертикальная шахматная сетка отображает свои элементы в вертикально прокручиваемом контейнере, который проходит по нескольким столбцам и позволяет отдельным элементам быть разными высотами. Ленивые горизонтальные сетки имеют одинаковое поведение на горизонтальной оси с предметами разной ширины.

Следующий фрагмент является основным примером использования LazyVerticalStaggeredGrid с шириной 200.dp на элемент:

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Adaptive(200.dp),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)

Рисунок 1. Пример ленивой ошеломленной вертикальной сетки

Чтобы установить фиксированное количество столбцов, вы можете использовать StaggeredGridCells.Fixed(columns) вместо StaggeredGridCells.Adaptive . Это делит доступную ширину на количество столбцов (или рядов для горизонтальной сетки), и каждый предмет забирает эту ширину (или высоту для горизонтальной сетки):

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(3),
    verticalItemSpacing = 4.dp,
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    content = {
        items(randomSizedPhotos) { photo ->
            AsyncImage(
                model = photo,
                contentScale = ContentScale.Crop,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth().wrapContentHeight()
            )
        }
    },
    modifier = Modifier.fillMaxSize()
)
Lazy staggered grid of images in Compose
Рисунок 2. Пример ленивой ошеломленной вертикальной сетки с фиксированными столбцами

Заполнение контента

Иногда вам нужно добавить накладку по краям контента. Ленивые компоненты позволяют вам передать некоторые PaddingValues в параметр contentPadding , чтобы поддержать это:

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

В этом примере мы добавляем 16.dp прокладки в горизонтальные края (слева и справа), а затем 8.dp в верхнюю и нижнюю часть содержания.

Обратите внимание, что эта заполнение применяется к контенту , а не к самой LazyColumn . В приведенном выше примере первый элемент добавит 8.dp Padding в верхнюю часть, последний элемент добавит 8.dp внизу, и все элементы будут иметь 16.dp -заполнение слева и справа.

Расстояние между контентом

Чтобы добавить интервал между элементами, вы можете использовать Arrangement.spacedBy() . Пример ниже добавляет 4.dp пространства между каждым элементом:

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Точно так же для LazyRow :

LazyRow(
    horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
    // ...
}

Сетки, однако, принимают как вертикальные, так и горизонтальные меры:

LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
    items(photos) { item ->
        PhotoItem(item)
    }
}

Ключи элемента

По умолчанию состояние каждого элемента направлено на позицию элемента в списке или сетке. Тем не менее, это может вызвать проблемы, если набор данных изменяется, поскольку элементы, которые эффективно изменяют позицию, теряют любое запоминающееся состояние. Если вы представляете сценарий LazyRow в пределах LazyColumn , если строка изменит позицию элемента, пользователь потеряет свою позицию прокрутки в строке.

Чтобы бороться с этим, вы можете предоставить стабильный и уникальный ключ для каждого элемента, предоставляя блок параметру key . Предоставление стабильного ключа позволяет состоянию элемента быть согласованным между изменениями набора данных:

LazyColumn {
    items(
        items = messages,
        key = { message ->
            // Return a stable + unique key for the item
            message.id
        }
    ) { message ->
        MessageRow(message)
    }
}

Предоставляя ключи, вы помогаете правильно обработать повторные изменения. Например, если ваш элемент содержит запоминающееся состояние, настройка клавиш позволила бы композиции перемещать это состояние вместе с элементом, когда его положение изменяется.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = remember {
            Random.nextInt()
        }
    }
}

Тем не менее, есть одно ограничение на то, какие типы вы можете использовать в качестве клавиш элементов. Тип ключа должен поддерживаться Bundle , механизм Android для сохранения состояний при воссоздании деятельности. Bundle поддерживает такие типы, как примитивы, перечисление или посылки.

LazyColumn {
    items(books, key = {
        // primitives, enums, Parcelable, etc.
    }) {
        // ...
    }
}

Ключ должен поддерживаться Bundle , чтобы rememberSaveable о композитном элементе могут быть восстановлены при воссоздании деятельности, или даже когда вы прокручиваете от этого предмета и прокрутите обратно.

LazyColumn {
    items(books, key = { it.id }) {
        val rememberedValue = rememberSaveable {
            Random.nextInt()
        }
    }
}

Анимация предметов

Если вы использовали виджет Recyclerview, вы будете знать, что он анимирует элементы автоматически. Ленивые макеты предоставляют такую ​​же функциональность для повторных порядок предметов. API прост - вам просто нужно установить модификатор animateItemPlacement для содержимого элемента:

LazyColumn {
    // It is important to provide a key to each item to ensure animateItem() works as expected.
    items(books, key = { it.id }) {
        Row(Modifier.animateItem()) {
            // ...
        }
    }
}

Вы даже можете предоставить пользовательскую спецификацию анимации, если вам нужно:

LazyColumn {
    items(books, key = { it.id }) {
        Row(
            Modifier.animateItem(
                fadeInSpec = tween(durationMillis = 250),
                fadeOutSpec = tween(durationMillis = 100),
                placementSpec = spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
            )
        ) {
            // ...
        }
    }
}

Убедитесь, что вы предоставили ключи для своих предметов, чтобы можно было найти новую позицию для перемещенного элемента.

Помимо повторных порядок, анимация предметов для дополнений и удаления в настоящее время находится в разработке. Вы можете отслеживать прогресс в выпуске 150812265 .

Липкие заголовки (экспериментальные)

Паттерн «липкий заголовок» полезен при отображении списков сгруппированных данных. Ниже вы можете увидеть пример «списка контактов», сгруппированный по начальному контакту:

Видео телефона прокрутки вверх и вниз через список контактов

Чтобы достичь липкого заголовка с LazyColumn , вы можете использовать функцию Experimental stickyHeader() , обеспечивая содержание заголовка:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ListWithHeader(items: List<Item>) {
    LazyColumn {
        stickyHeader {
            Header()
        }

        items(items) { item ->
            ItemRow(item)
        }
    }
}

Чтобы получить список с несколькими заголовками, например, пример «Список контактов», вы можете сделать:

// This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

Реагировать на положение прокрутки

Многие приложения должны отреагировать и прослушать позицию прокрутки и изменения макета элемента. Ленивые компоненты поддерживают этот вариант использования путем поднятия LazyListState :

@Composable
fun MessageList(messages: List<Message>) {
    // Remember our own LazyListState
    val listState = rememberLazyListState()

    // Provide it to LazyColumn
    LazyColumn(state = listState) {
        // ...
    }
}

Для простых случаев использования приложения обычно должны знать только информацию о первом видимом элементе. Для этого LazyListState предоставляет firstVisibleItemIndex и firstVisibleItemScrollOffset .

Если мы используем пример отображения и сокрытия кнопки в зависимости от того, просмотрел ли пользователь первый элемент:

@Composable
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

Чтение состояния непосредственно в композиции полезно, когда вам нужно обновить другие композиции пользовательского интерфейса, но есть также сценарии, в которых событие не нужно обрабатывать в той же композиции. Распространенным примером этого является отправка аналитического события после того, как пользователь прокрутит определенную точку. Чтобы эффективно справиться с этим, мы можем использовать snapshotFlow() :

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState также предоставляет информацию обо всех отображаемых в настоящее время элементах и ​​их границах на экране, через свойство layoutInfo . Смотрите класс LazyListLayoutInfo для получения дополнительной информации.

Управление положением прокрутки

Помимо реагирования на положение прокрутки, также полезно для приложений, чтобы контролировать положение прокрутки. LazyListState поддерживает это с помощью функции scrollToItem() , которая «немедленно» набирает позицию прокрутки, и animateScrollToItem() , который прокручивается с использованием анимации (также известной как плавный прокруток):

@Composable
fun MessageList(messages: List<Message>) {
    val listState = rememberLazyListState()
    // Remember a CoroutineScope to be able to launch
    val coroutineScope = rememberCoroutineScope()

    LazyColumn(state = listState) {
        // ...
    }

    ScrollToTopButton(
        onClick = {
            coroutineScope.launch {
                // Animate scroll to the first item
                listState.animateScrollToItem(index = 0)
            }
        }
    )
}

Большие наборы данных (пейджинг)

Библиотека Paging позволяет приложениям поддерживать большие списки элементов, загружать и отображать небольшие куски списка по мере необходимости. Paging 3.0, а затем обеспечивает поддержку Compose через библиотеку androidx.paging:paging-compose .

Чтобы отобразить список страхового контента, мы можем использовать функцию расширения collectAsLazyPagingItems() , а затем передавать возвращенные LazyPagingItems в items() в нашей LazyColumn . Подобно поддержке пейджинга в представлениях, вы можете отображать заполнители, когда нагружает данные, проверяя, является ли item null :

@Composable
fun MessageList(pager: Pager<Int, Message>) {
    val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val message = lazyPagingItems[index]
            if (message != null) {
                MessageRow(message)
            } else {
                MessagePlaceholder()
            }
        }
    }
}

Советы по использованию ленивых макетов

Есть несколько советов, которые вы можете принять во внимание, чтобы убедиться, что ваши ленивые макеты работают, как и предполагалось.

Избегайте использования элементов размером 0 пикселей

Это может произойти в сценариях, где, например, вы рассчитываете асинхронно получить некоторые данные, такие как изображения, чтобы заполнить элементы вашего списка на более позднем этапе. Это приведет к тому, что ленивый макет сочинит все свои элементы в первом измерении, так как их высота составляет 0 пикселей, и он может поместить их всех в точку зрения. После того, как предметы загрузились и их высота расширились, ленивые макеты затем отбросили все другие предметы, которые излишне были составлены в первый раз, поскольку они на самом деле не могут соответствовать видовому доку. Чтобы избежать этого, вы должны установить размеры по умолчанию в свои элементы, чтобы ленивый макет мог сделать правильный расчет того, сколько элементов может на самом деле соответствовать в виде просмотра:

@Composable
fun Item(imageUrl: String) {
    AsyncImage(
        model = rememberAsyncImagePainter(model = imageUrl),
        modifier = Modifier.size(30.dp),
        contentDescription = null
        // ...
    )
}

Когда вы знаете, что приблизительный размер ваших элементов после асинхронно загружены, хорошая практика - убедиться, что размеры ваших элементов остаются прежними до и после загрузки, например, добавив некоторые заполнители. Это поможет поддерживать правильную позицию прокрутки.

Избегайте гнездования компонентов, прокручиваемых в том же направлении

Это относится только к случаям при гнездовании прокручиваемых детей без предопределенного размера внутри другого и того же направления, прокручиваемого родителем. Например, пытаясь гнездовать ребенка LazyColumn без фиксированной высоты внутри вертикально прокручиваемой Column Родитель:

// throws IllegalStateException
Column(
    modifier = Modifier.verticalScroll(state)
) {
    LazyColumn {
        // ...
    }
}

Вместо этого один и тот же результат может быть достигнут путем завершения всех ваших композиционных продуктов внутри одного родителя LazyColumn и использования его DSL для прохождения различных типов контента. Это позволяет излучать отдельные элементы, а также несколько элементов списка, все в одном месте:

LazyColumn {
    item {
        Header()
    }
    items(data) { item ->
        PhotoItem(item)
    }
    item {
        Footer()
    }
}

Имейте в виду, что случаи, когда вы гнездовываете различные макеты направления, например, прокручиваемая родительская Row и детская LazyColumn , разрешены:

Row(
    modifier = Modifier.horizontalScroll(scrollState)
) {
    LazyColumn {
        // ...
    }
}

А также случаи, когда вы все еще используете одни и те же макеты направления, но также устанавливаете фиксированный размер для вложенных детей:

Column(
    modifier = Modifier.verticalScroll(scrollState)
) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
}

Остерегайтесь размещения нескольких элементов в один элемент

В этом примере второй пункт Lambda издает 2 элемента в одном блоке:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
}

Lazy layouts will handle this as expected - they will lay out elements one after another as if they were different items. However, there are a couple of problems with doing so.

When multiple elements are emitted as part of one item, they are handled as one entity, meaning that they cannot be composed individually anymore. If one element becomes visible on the screen, then all elements corresponding to the item have to be composed and measured. This can hurt performance if used excessively. In the extreme case of putting all elements in one item, it completely defeats the purpose of using Lazy layouts. Apart from potential performance issues, putting more elements in one item will also interfere with scrollToItem() & animateScrollToItem() .

However, there are valid use cases for putting multiple elements in one item, like having dividers inside a list. You do not want dividers to change scrolling indices, as they shouldn't be considered independent elements. Also, performance will not be affected as dividers are small. A divider will likely need to be visible when the item before it is visible, so they can be part of the previous item:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp)
) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
}

Consider using custom arrangements

Usually Lazy lists have many items, and they occupy more than the size of the scrolling container. However, when your list is populated with few items, your design can have more specific requirements for how these should be positioned in the viewport.

To achieve this, you can use custom vertical Arrangement and pass it to the LazyColumn . In the following example, the TopWithFooter object only needs to implement the arrange method. Firstly, it will position items one after another. Secondly, if the total used height is lower than the viewport height, it will position the footer at the bottom:

object TopWithFooter : Arrangement.Vertical {
    override fun Density.arrange(
        totalSize: Int,
        sizes: IntArray,
        outPositions: IntArray
    ) {
        var y = 0
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

Consider adding contentType

Starting with Compose 1.2, in order to maximize the performance of your Lazy layout, consider adding contentType to your lists or grids. This allows you to specify the content type for each item of the layout, in cases where you're composing a list or a grid consisting of multiple different types of items:

LazyColumn {
    items(elements, contentType = { it.type }) {
        // ...
    }
}

When you provide the contentType , Compose is able to reuse compositions only between the items of the same type. As reusing is more efficient when you compose items of similar structure, providing the content types ensures Compose doesn't try to compose an item of type A on top of a completely different item of type B. This helps maximize the benefits of composition reusing and your Lazy layout performance.

Measuring performance

You can only reliably measure the performance of a Lazy layout when running in release mode and with R8 optimisation enabled. On debug builds, Lazy layout scrolling may appear slower. For more information on this, read through Compose performance .

{% дословно %} {% дословно %} {% дословно %} {% дословно %}