Отложенные списки и сетки

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

Язык описания макета 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 vertical grid будут отображаться в контейнере с вертикальной прокруткой, занимая несколько столбцов, а в Lazy horizontal grid – в контейнере с горизонтальной прокруткой, занимая несколько строк.

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

Скриншот экрана телефона с сеткой фотографий

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

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

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

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

Если вам нужно, чтобы нестандартные размеры были только у некоторых элементов, вы можете использовать сетку, чтобы задать для них нестандартную ширину столбцов. Укажите диапазон столбцов с помощью параметра span методов LazyGridScope DSL, item и 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()
)
Отложенная загрузка изображений в виде сетки в Compose
Рисунок 2. Пример вертикальной сетки с фиксированным количеством столбцов и отложенной загрузкой

Отступы контента

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

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

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

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

Другой пример: вы можете передать Scaffold PaddingValues в LazyColumn contentPadding. Ознакомьтесь с руководством по созданию контента без полей.

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

Чтобы добавить пробелы между элементами, используйте 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 вызывает composable-функцию 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 – это функции lambda, которые выполняются при выборе соответствующей кнопки.
  • Переменная состояния selectedIndex отслеживает выбранный вариант.

Результат

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

Рисунок 1. Список, в котором при добавлении, удалении или сортировке элементов используется анимация.

Закрепленные заголовки

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

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

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

функцию, указав в ней контент заголовка:

@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] }

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

Не добавляйте в один элемент несколько объектов

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

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

Используйте специальные макеты

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

Для этого можно использовать специальную вертикаль 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

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

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

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

Оценка эффективности

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

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