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

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

DSL-модуль LazyListScope

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

Сетки обладают такими же мощными возможностями 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 , одно из значений области span, особенно полезно при использовании адаптивного размера, поскольку количество столбцов не фиксировано. В этом примере показано, как предоставить полный диапазон строк:

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 слева и справа.

В качестве другого примера, вы можете передать PaddingValues Scaffold в contentPadding LazyColumn . Смотрите руководство edge-to-edge .

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

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

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 :

@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 пикселей.

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

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

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

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

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

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

Когда несколько элементов выдаются как часть одного элемента, они обрабатываются как одна сущность, что означает, что они больше не могут быть составлены по отдельности. Если один элемент становится видимым на экране, то все элементы, соответствующие элементу, должны быть составлены и измерены. Это может повредить производительности, если используется чрезмерно. В крайнем случае помещения всех элементов в один элемент, это полностью сводит на нет цель использования Lazy layouts. Помимо потенциальных проблем с производительностью, размещение большего количества элементов в одном элементе также будет мешать 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. Это помогает максимизировать преимущества повторного использования композиции и производительность вашего Lazy-макета.

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

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

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

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