Списки: LazyColumn и отображение данных

LazyColumn и LazyRow отображают длинные списки эффективно: они создают и держат в памяти только видимые элементы, переиспользуя их при прокрутке.
Суть: для списков любой длины используют ленивые контейнеры, которые рендерят лишь видимую часть, — это критично для плавности и экономии памяти.

Если положить сотню элементов в обычный Column, Compose создаст их все сразу — это медленно и расходует память. Для списков существуют ленивые контейнеры: LazyColumn (вертикальный) и LazyRow (горизонтальный). Они создают только видимые на экране элементы и переиспользуют их при прокрутке, как старый RecyclerView, но декларативно.

@Composable
fun UserList(users: List<String>) {
    LazyColumn {
        items(users) { user ->
            Text(
                text = user,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

Функция items принимает список и лямбду, описывающую, как отрисовать один элемент. Compose сам решает, сколько элементов создать, исходя из размера экрана.

Ключи элементов

Когда список меняется (добавление, удаление, перестановка), Compose нужно понимать, какой элемент какому соответствует. Для этого задают стабильный key — обычно идентификатор данных. Без ключей Compose сопоставляет элементы по позиции, что приводит к лишним перерисовкам и багам анимации при изменении порядка.

data class User(val id: Int, val name: String)

@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users, key = { it.id }) { user ->
            Text(user.name, modifier = Modifier.padding(16.dp))
        }
    }
}

Как работает под капотом

Ленивый список держит «окно» из видимых элементов плюс небольшой запас. При прокрутке элемент, ушедший за край, освобождается, а появившийся снизу — создаётся (или переиспользуется слот). Поэтому память почти не зависит от длины списка: хоть десять элементов, хоть десять тысяч. Ключи позволяют Compose сохранять состояние конкретного элемента (например, развёрнутость карточки) при изменении порядка, сопоставляя элементы по идентификатору, а не по позиции.

  LazyColumn: рендерится только видимое

  Данные: [0..9999]

  Экран (видно ~8):
   +-----------+
   | item 12   |  <-- создан
   | item 13   |  <-- создан
   | item 14   |  <-- создан
   +-----------+
   item 0..11   --> НЕ в памяти
   item 15..9999 --> НЕ в памяти

Частые ошибки

Класть много элементов в Column. Обычный Column создаёт всех детей сразу; для списков всегда LazyColumn.

Забыть ключи. Без key при изменении порядка возможны мерцания и потеря состояния элементов.

Вкладывать прокручиваемое в прокручиваемое того же направления. Вертикальный LazyColumn внутри вертикального скролла даёт конфликт измерений.

Best practices

  • Для любых списков используйте ленивые контейнеры, а не Column со всеми элементами.
  • Задавайте стабильные key по идентификатору данных.
  • Выносите отрисовку одного элемента в отдельный stateless-composable.
  • Не вкладывайте прокрутку в прокрутку одного направления.

Идею «рендерим только видимое окно» удобно смоделировать на Python: из большого списка показываем лишь видимую часть. Запустите врезку.

# Аналог ленивого рендера видимого окна из LazyColumn
data = list(range(0, 10000))
scroll = 12          # позиция прокрутки
visible = 3          # сколько помещается на экране

window = data[scroll:scroll + visible]
print('всего элементов:', len(data))
print('создано (в памяти):', len(window), '->', window)

Попробуй сам ▶ — меняйте scroll и visible. Заметьте: сколько бы ни было данных, в памяти держится только маленькое окно — ровно так работает LazyColumn.

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

Главная интуиция ленивых списков — «памяти ровно столько, сколько видно на экране». Это меняет отношение к размеру данных: список на десять тысяч строк рендерится так же легко, как на десять, потому что в каждый момент живёт лишь небольшое окно элементов. Перенесите эту мысль на любой экран с прокруткой — лента, каталог, чат: везде это LazyColumn, а не Column, и это не вопрос стиля, а вопрос плавности интерфейса.

Второй ориентир — ключи как инструмент идентичности. Без ключей Compose думает позициями, и при вставке элемента в начало он считает, что изменились все элементы. Со стабильным key по идентификатору он понимает, что просто появился новый элемент сверху, а остальные те же, — и сохраняет их состояние и анимации. Поэтому в продакшен-списках ключи не опция, а норма, особенно когда список меняется во времени.

Итог: LazyColumn и LazyRow отображают списки любой длины эффективно, создавая только видимые элементы, а ключи обеспечивают корректное обновление при изменении данных. Теперь интерфейс умеет показывать коллекции — пора связать его с данными и сетью.

Проверьте себя
1. Чем LazyColumn принципиально отличается от обычного Column для списков?
ALazyColumn рисует элементы в случайном порядке
BLazyColumn создаёт и держит в памяти только видимые элементы, переиспользуя их при прокрутке
CLazyColumn работает только с числами
DРазницы нет, это синонимы
2. Зачем задавать key в items для LazyColumn?
AЧтобы ускорить компиляцию
BЧтобы Compose сопоставлял элементы по идентификатору при изменении списка и сохранял их состояние
CЧтобы отсортировать список
Dkey не нужен никогда