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

Многим приложениям необходимо отображать коллекции элементов. В этом документе объясняется, как эффективно это сделать в 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 — список с горизонтальной прокруткой.

Компоненты 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 и items класса LazyGridScope DSL . 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.Adaptive можно использовать StaggeredGridCells.Fixed(columns) . Это разделит доступную ширину на количество столбцов (или строк для горизонтальной сетки), и каждый элемент будет занимать эту ширину (или высоту для горизонтальной сетки):

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

В качестве еще одного примера, вы можете передать PaddingValues ​​из Scaffold в contentPadding из LazyColumn . См. руководство по передаче данных от края до края .

Интервалы между элементами контента

Для добавления отступов между элементами можно использовать 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 для сохранения состояний при повторном создании Activity. Bundle поддерживает такие типы, как примитивы, перечисления или Parcelable.

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

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

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

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

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

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)
            )
        ) {
            // ...
        }
    }
}

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

Пример: Анимация элементов в ленивых списках

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

Этот фрагмент кода отображает список строк с анимированными переходами при добавлении, удалении или изменении порядка элементов:

@Composable
fun ListAnimatedItems(
    items: List<String>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // Use a unique key per item, so that animations work as expected.
        items(items, key = { it }) {
            ListItem(
                headlineContent = { Text(it) },
                modifier = Modifier
                    .animateItem(
                        // Optionally add custom animation specs
                    )
                    .fillParentMaxWidth()
                    .padding(horizontal = 8.dp, vertical = 0.dp),
            )
        }
    }
}

Основные моменты, касающиеся кода.

  • ListAnimatedItems отображает список строк в LazyColumn с анимированными переходами при изменении элементов.
  • Функция items присваивает каждому элементу в списке уникальный ключ. Compose использует эти ключи для отслеживания элементов и определения изменений в их позициях.
  • ListItem определяет макет каждого элемента списка. Он принимает параметр headlineContent , который определяет основное содержимое элемента.
  • Модификатор animateItem применяет стандартные анимации к добавлению, удалению и перемещению предметов.

В следующем фрагменте кода представлен экран, содержащий элементы управления для добавления и удаления элементов, а также сортировки предопределенного списка:

@Composable
private fun ListAnimatedItemsExample(
    data: List<String>,
    modifier: Modifier = Modifier,
    onAddItem: () -> Unit = {},
    onRemoveItem: () -> Unit = {},
    resetOrder: () -> Unit = {},
    onSortAlphabetically: () -> Unit = {},
    onSortByLength: () -> Unit = {},
) {
    val canAddItem = data.size < 10
    val canRemoveItem = data.isNotEmpty()

    Scaffold(modifier) { paddingValues ->
        Column(
            modifier = Modifier
                .padding(paddingValues)
                .fillMaxSize()
        ) {
            // Buttons that change the value of displayedItems.
            AddRemoveButtons(canAddItem, canRemoveItem, onAddItem, onRemoveItem)
            OrderButtons(resetOrder, onSortAlphabetically, onSortByLength)

            // List that displays the values of displayedItems.
            ListAnimatedItems(data)
        }
    }
}

Основные моменты, касающиеся кода.

  • ListAnimatedItemsExample представляет собой экран, содержащий элементы управления для добавления, удаления и сортировки элементов.
    • onAddItem и onRemoveItem — это лямбда-выражения, которые передаются в AddRemoveButtons для добавления и удаления элементов из списка.
    • resetOrder , onSortAlphabetically и onSortByLength — это лямбда-выражения, которые передаются в OrderButtons для изменения порядка элементов в списке.
  • AddRemoveButtons отображает кнопки «Добавить» и «Удалить». Она включает/отключает кнопки и обрабатывает нажатия на них.
  • OrderButtons отображает кнопки для изменения порядка элементов в списке. Он принимает лямбда-функции для сброса порядка и сортировки списка по длине или в алфавитном порядке.
  • ListAnimatedItems вызывает составной объект ListAnimatedItems , передавая список data для отображения анимированного списка строк. data определены в другом месте.

Этот фрагмент кода создает пользовательский интерфейс с кнопками «Добавить элемент» и «Удалить элемент» :

@Composable
private fun AddRemoveButtons(
    canAddItem: Boolean,
    canRemoveItem: Boolean,
    onAddItem: () -> Unit,
    onRemoveItem: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        Button(enabled = canAddItem, onClick = onAddItem) {
            Text("Add Item")
        }
        Spacer(modifier = Modifier.padding(25.dp))
        Button(enabled = canRemoveItem, onClick = onRemoveItem) {
            Text("Delete Item")
        }
    }
}

Основные моменты, касающиеся кода.

  • AddRemoveButtons отображает ряд кнопок для добавления и удаления элементов из списка.
  • Параметры canAddItem и canRemoveItem управляют состоянием активности кнопок. Если canAddItem или canRemoveItem имеют значение false, соответствующая кнопка отключена.
  • Параметры onAddItem и onRemoveItem представляют собой лямбда-функции, которые выполняются при нажатии пользователем соответствующей кнопки.

Наконец, этот фрагмент кода отображает три кнопки для сортировки списка ( Сброс, Алфавитный порядок и Длина ):

@Composable
private fun OrderButtons(
    resetOrder: () -> Unit,
    orderAlphabetically: () -> Unit,
    orderByLength: () -> Unit
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        var selectedIndex by remember { mutableIntStateOf(0) }
        val options = listOf("Reset", "Alphabetical", "Length")

        SingleChoiceSegmentedButtonRow {
            options.forEachIndexed { index, label ->
                SegmentedButton(
                    shape = SegmentedButtonDefaults.itemShape(
                        index = index,
                        count = options.size
                    ),
                    onClick = {
                        Log.d("AnimatedOrderedList", "selectedIndex: $selectedIndex")
                        selectedIndex = index
                        when (options[selectedIndex]) {
                            "Reset" -> resetOrder()
                            "Alphabetical" -> orderAlphabetically()
                            "Length" -> orderByLength()
                        }
                    },
                    selected = index == selectedIndex
                ) {
                    Text(label)
                }
            }
        }
    }
}

Основные моменты, касающиеся кода.

  • OrderButtons отображает SingleChoiceSegmentedButtonRow , позволяющий пользователям выбирать способ сортировки в списке или изменять порядок элементов в списке. Компонент SegmentedButton позволяет выбрать один вариант из списка вариантов.
  • resetOrder , orderAlphabetically и orderByLength — это лямбда-функции, которые выполняются при выборе соответствующей кнопки.
  • Переменная состояния selectedIndex отслеживает выбранный параметр.

Результат

В этом видео показан результат применения приведенных выше фрагментов кода при изменении порядка элементов:

Рисунок 1. Список, в котором анимируются переходы элементов при их добавлении, удалении или сортировке.

Фиксированные заголовки

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

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

Для создания «фиксированного» заголовка с помощью 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 позволяет приложениям поддерживать большие списки элементов, загружая и отображая небольшие фрагменты списка по мере необходимости. Paging 3.0 и более поздние версии обеспечивают поддержку Compose через библиотеку androidx.paging:paging-compose .

Для отображения списка постраничного контента можно использовать функцию расширения collectAsLazyPagingItems() , а затем передать возвращенные LazyPagingItems в items() в нашем LazyColumn . Аналогично поддержке постраничной навигации в представлениях, можно отображать заполнители во время загрузки данных, проверяя, является ли item null (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 может повторно использовать композиции только между элементами одного типа. Поскольку повторное использование более эффективно при компоновке элементов схожей структуры, указание типов содержимого гарантирует, что Compose не попытается скомпоновать элемент типа A поверх совершенно другого элемента типа B. Это помогает максимально использовать преимущества повторного использования композиций и повысить производительность ленивой компоновки.

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

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

Дополнительные ресурсы

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}