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

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

LazyListScope DSL

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

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

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

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

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

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

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

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

Для отображения элементов в виде сетки используются composable-функции 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 методов LazyGridScope DSL, item и items. maxLineSpan – одно из значений области действия span, которое особенно полезно при использовании адаптивного размера, поскольку количество столбцов не фиксировано. В примере ниже показано, как задать полную ширину строки.

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

Ленивая ступенчатая сетка

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

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

Поля контента

Иногда вам нужно будет добавить отступы по краям контента. Для этого в ленивых компонентах можно передать некоторые 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 вызывает composable-функцию ListAnimatedItems, передавая список data для отображения анимированного списка строк. data is defined elsewhere.

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

@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().

функцию, которая предоставляет контент заголовка:

@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 позволяет приложениям поддерживать большие списки элементов, загружая и отображая небольшие фрагменты списка по мере необходимости. 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 пикселей и все они могут поместиться в области просмотра. После загрузки элементов и увеличения их высоты ленивые макеты отбрасывают все остальные элементы, которые были напрасно скомпонованы в первый раз, поскольку они не могут поместиться в области просмотра. Чтобы избежать этого, задайте размеры по умолчанию для элементов, чтобы макет 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 {
        // ...
    }
}

Вместо этого можно поместить все composable-функции в один родительский элемент 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) }
    // ...
}

Используйте собственные аранжировки

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

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

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