Многим приложениям требуется отображать коллекции элементов. В этом документе объясняется, как эффективно реализовать это в Jetpack Compose.
Если вы знаете, что ваш вариант использования не требует прокрутки, вы можете использовать простой Column
или Row
(в зависимости от направления) и выводить содержимое каждого элемента путем итерации по списку следующим образом:
@Composable fun MessageList(messages: List<Message>) { Column { messages.forEach { message -> MessageRow(message) } } }
Мы можем сделать Column
прокручиваемым, используя модификатор verticalScroll()
.
Ленивые списки
Если вам необходимо отобразить большое количество элементов (или список неизвестной длины), использование такого макета, как Column
, может вызвать проблемы с производительностью, поскольку все элементы будут скомпонованы и размещены независимо от того, видны они или нет.
Compose предоставляет набор компонентов, которые компонуют и размещают только те элементы, которые видны в области просмотра компонента. К этим компонентам относятся LazyColumn
и LazyRow
.
Как следует из названия, разница между LazyColumn
и LazyRow
заключается в ориентации расположения элементов и прокрутки. LazyColumn
создаёт вертикально прокручиваемый список, а LazyRow
— горизонтально прокручиваемый.
Компоненты Lazy отличаются от большинства макетов в Compose. Вместо того, чтобы принимать параметр блока контента @Composable
, позволяющий приложениям напрямую создавать компонуемые элементы, компоненты Lazy предоставляют блок LazyListScope.()
. Этот блок LazyListScope
предоставляет DSL, позволяющий приложениям описывать содержимое элементов. Компонент Lazy затем отвечает за добавление содержимого каждого элемента в соответствии с макетом и позицией прокрутки.
LazyListScope
DSL
DSL-языка LazyListScope
предоставляет ряд функций для описания элементов макета. В самом простом случае, item()
добавляет один элемент, а items(Int)
— несколько элементов:
LazyColumn { // Add a single item item { Text(text = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
Существует также ряд функций расширения, позволяющих добавлять коллекции элементов, например, List
. Эти расширения позволяют нам легко перенести наш пример Column
из примера выше:
/** * import androidx.compose.foundation.lazy.items */ LazyColumn { items(messages) { message -> MessageRow(message) } }
Существует также вариант функции расширения items()
, называемый itemsIndexed()
, который предоставляет индекс. Подробнее см. в справочнике LazyListScope
.
Ленивые сетки
Компонуемые элементы LazyVerticalGrid
и LazyHorizontalGrid
поддерживают отображение элементов в сетке. Вертикальная сетка 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
методов LazyGridScope DSL
item
и 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() )
Чтобы задать фиксированное количество столбцов, можно использовать 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() )
Заполнение контента
Иногда вам может понадобиться добавить отступы по краям контента. Для этого ленивые компоненты позволяют передавать значения 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
поддерживает такие типы, как примитивы, перечисления и Parcelables.
LazyColumn { items(books, key = { // primitives, enums, Parcelable, etc. }) { // ... } }
Ключ должен поддерживаться Bundle
, чтобы rememberSaveable
внутри компонуемого элемента можно было восстановить при повторном создании 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
отслеживает выбранный параметр.
Результат
В этом видео показан результат предыдущих фрагментов при переупорядочивании элементов:
Закрепленные заголовки (экспериментальные)
Шаблон «липкого заголовка» полезен при отображении списков сгруппированных данных. Ниже представлен пример списка контактов, сгруппированных по инициалам каждого контакта:
Чтобы добиться закрепленного заголовка с помощью 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 пикселей.
Это может произойти, например, когда вы планируете асинхронно извлекать данные, например, изображения, для заполнения элементов списка на более позднем этапе. В этом случае ленивый макет скомпонует все элементы в первом измерении, поскольку их высота равна 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) ) { // ... } }
Остерегайтесь размещения нескольких элементов в одном элементе
В этом примере вторая лямбда-функция item выдает 2 элемента в одном блоке:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Item(2) } item { Item(3) } // ... }
Ленивые макеты справятся с этим, как и ожидалось: они будут располагать элементы один за другим, как будто это разные объекты. Однако есть несколько проблем с этим.
Когда несколько элементов генерируются как часть одного элемента, они обрабатываются как единое целое, что означает, что их больше нельзя компоновать по отдельности. Если один элемент становится видимым на экране, все элементы, соответствующие этому элементу, должны быть скомпонованы и измерены. Это может снизить производительность при чрезмерном использовании. В крайнем случае, когда все элементы помещаются в один элемент, это полностью сводит на нет смысл использования ленивых макетов. Помимо потенциальных проблем с производительностью, размещение нескольких элементов в одном элементе также помешает работе scrollToItem()
и animateScrollToItem()
.
Однако существуют допустимые варианты размещения нескольких элементов в одном элементе, например, разделители внутри списка. Разделители не должны изменять индексы прокрутки, поскольку они не должны считаться независимыми элементами. Кроме того, производительность не пострадает, поскольку разделители небольшие. Разделитель, вероятно, должен быть видимым, когда виден предшествующий ему элемент, поэтому он может быть частью предыдущего элемента:
LazyVerticalGrid( columns = GridCells.Adaptive(100.dp) ) { item { Item(0) } item { Item(1) Divider() } item { Item(2) } // ... }
Рассмотрите возможность использования индивидуальных договоренностей
Обычно ленивые списки содержат много элементов, занимая пространство, превышающее размер прокручиваемого контейнера. Однако, если в списке мало элементов, ваш дизайн может предъявлять более строгие требования к их расположению в области просмотра.
Для этого можно использовать пользовательское вертикальное Arrangement
и передать его в LazyColumn
. В следующем примере объекту TopWithFooter
достаточно реализовать только метод arrange
. Во-первых, он будет располагать элементы один за другим. Во-вторых, если общая используемая высота меньше высоты области просмотра, нижний колонтитул будет расположен внизу:
object TopWithFooter : Arrangement.Vertical { override fun Density.arrange( totalSize: Int, sizes: IntArray, outPositions: IntArray ) { var y = 0 sizes.forEachIndexed { index, size -> outPositions[index] = y y += size } if (y < totalSize) { val lastIndex = outPositions.lastIndex outPositions[lastIndex] = totalSize - sizes.last() } } }
Рассмотрите возможность добавления contentType
Начиная с Compose 1.2, чтобы максимально повысить производительность вашего Lazy-макета, рассмотрите возможность добавления contentType
к спискам или сеткам. Это позволит вам указать тип контента для каждого элемента макета, если вы создаете список или сетку, состоящую из нескольких разных типов элементов:
LazyColumn { items(elements, contentType = { it.type }) { // ... } }
При указании contentType
Compose может повторно использовать композиции только между элементами одного типа. Поскольку повторное использование более эффективно при компоновке элементов схожей структуры, указание типов контента гарантирует, что Compose не будет пытаться компоновать элемент типа A поверх совершенно другого элемента типа B. Это помогает максимально эффективно использовать преимущества повторного использования композиций и производительность ленивой компоновки.
Измерение производительности
Достоверно измерить производительность ленивого макета можно только в режиме релиза с включённой оптимизацией R8. В отладочных сборках прокрутка ленивого макета может выглядеть медленнее. Подробнее об этом см. в статье «Производительность Compose» .
Дополнительные ресурсы
- Создать конечный прокручиваемый список
- Создайте прокручиваемую сетку
- Отображение вложенных прокручиваемых элементов в списке
- Фильтрация списка во время ввода текста
- Ленивая загрузка данных с помощью списков и пагинации
- Создайте список, используя несколько типов элементов
- Видео: списки в Compose
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Перенести
RecyclerView
в список ленивых - Сохранение состояния пользовательского интерфейса в Compose
- Kotlin для Jetpack Compose