Списки: ListView и ListView.builder

ListView показывает прокручиваемый список, а ListView.builder рисует элементы лениво — только видимые.

Суть: ListView подходит для короткого статичного списка, а ListView.builder — для длинных или бесконечных данных: он создаёт виджеты по мере прокрутки, экономя память.

У ленивых списков есть близкие родственники для двумерных раскладок — GridView.builder для сеток и CustomScrollView со slivers для сложных прокручиваемых композиций, где шапка схлопывается при прокрутке. Все они разделяют ту же идею ленивого построения. Освоив принцип itemBuilder на ListView, вы практически бесплатно понимаете и эти более мощные инструменты, потому что контракт у них одинаковый.

Ленты, чаты, каталоги — везде списки, которые не помещаются на экран и должны прокручиваться. Простой ListView с явным children хорош для нескольких элементов. Но если их сотни или они приходят из сети, создавать все сразу расточительно — экран всё равно покажет лишь часть. Здесь нужен ListView.builder.

// короткий статичный список
ListView(
  children: const [
    ListTile(title: Text('Первый')),
    ListTile(title: Text('Второй')),
    ListTile(title: Text('Третий')),
  ],
)

// длинный список — ленивая отрисовка
final items = List.generate(1000, (i) => 'Элемент $i');

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      leading: const Icon(Icons.label),
      title: Text(items[index]),
    );
  },
)

itemBuilder — это функция, которую Flutter вызывает для каждого видимого элемента, передавая его index. Вы возвращаете виджет строки. itemCount сообщает общее число элементов.

Как работает ленивая отрисовка под капотом

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

        весь список данных (1000 элементов)
   [0][1][2] ... [498][499][500][501] ... [999]
                     ^^^^^^^^^^^^^^^^^
                     |   видимая    |
                     | область +/-  |  <- только эти построены
                     ^^^^^^^^^^^^^^^^^
   прокрутка вниз -->  верхние утилизируются,
                       нижние строятся builder'ом
# Модель ListView.builder: строим только видимые элементы
items = [f'Элемент {i}' for i in range(1000)]

def build_visible(items, scroll_offset, viewport=5):
    # строим только окно видимых элементов, а не все 1000
    start = scroll_offset
    end = min(len(items), scroll_offset + viewport)
    built = [items[i] for i in range(start, end)]
    print(f'построено виджетов: {len(built)} из {len(items)}')
    return built

print(build_visible(items, scroll_offset=0))
print(build_visible(items, scroll_offset=498))   # прокрутили вниз

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

  • Использовать обычный ListView с тысячами элементов — это создаёт их все сразу и тормозит. Берите .builder.
  • Класть ListView в Column без ограничения высоты — список не знает, сколько места занять. Нужен Expanded.
  • Забыть itemCount — список не будет знать, где остановиться.

Best practices

  • Для длинных или сетевых данных всегда используйте ListView.builder.
  • Для разделителей между элементами есть ListView.separated.
  • Добавляйте уникальные key элементам, если список меняется (вставки/удаления), — Flutter точнее переиспользует виджеты.

Тонкий, но важный момент — ключи (Key). Когда список меняется (элементы добавляются, удаляются, переставляются), Flutter сопоставляет старые и новые виджеты, и без уникальных ключей он может перепутать, какой элемент какой, что приводит к странностям вроде сохранённого состояния не в той строке. Присваивая каждому элементу ValueKey по его идентификатору, вы помогаете Flutter точно отслеживать элементы при изменениях списка.

Итог: ListView.builder — ключ к производительным спискам любой длины. Понимание ленивой отрисовки объясняет, почему Flutter-приложения остаются плавными даже на огромных данных. Дальше украсим элементы карточками и иконками.

Проверьте себя
1. Когда предпочтительнее ListView.builder вместо обычного ListView?
AДля двух-трёх элементов
BДля длинных или бесконечных списков — он строит только видимые элементы
CКогда нужен цвет фона
DТолько для текста
2. Что передаёт Flutter в функцию itemBuilder?
AТолько context
Bcontext и index текущего элемента
CВесь список целиком
DНичего