Какие способы реализации списков в Flutter вы знаете?

Ответ

В Flutter есть несколько виджетов для отображения списков, каждый со своими особенностями производительности и использования:

1. ListView — базовый виджет для вертикальных/горизонтальных списков:

// Простой список с заранее созданными детьми
ListView(
  padding: EdgeInsets.all(16),
  children: [
    ListTile(title: Text('Item 1'), leading: Icon(Icons.star)),
    ListTile(title: Text('Item 2'), leading: Icon(Icons.star)),
    ListTile(title: Text('Item 3'), leading: Icon(Icons.star)),
  ],
)

// С разделителями
ListView.separated(
  itemCount: 20,
  separatorBuilder: (context, index) => Divider(height: 1, color: Colors.grey[300]),
  itemBuilder: (context, index) => ListTile(
    title: Text('Item $index'),
    subtitle: Text('Subtitle $index'),
    trailing: Icon(Icons.chevron_right),
  ),
)

2. ListView.builder — для динамических списков (ленивая загрузка):

final List<Product> products = [...]; // 1000+ элементов

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];
    return ProductCard(
      product: product,
      onTap: () => _openProductDetails(product),
    );
  },
  // Оптимизация производительности
  addAutomaticKeepAlives: true, // Сохраняет состояние
  addRepaintBoundaries: true,   // Изолирует перерисовку
  cacheExtent: 500,             // Предзагрузка вне viewport
)

3. GridView — для отображения в виде сетки:

// Фиксированное количество колонок
GridView.count(
  crossAxisCount: 2,
  crossAxisSpacing: 8,
  mainAxisSpacing: 8,
  childAspectRatio: 0.8, // Соотношение сторон
  children: List.generate(20, (index) => 
    Card(
      child: Column(
        children: [
          Image.network('https://picsum.photos/200?random=$index'),
          Text('Item $index'),
        ],
      ),
    ),
  ),
)

// Динамическая сетка с builder
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: MediaQuery.of(context).size.width > 600 ? 4 : 2,
    childAspectRatio: 1.0,
  ),
  itemCount: products.length,
  itemBuilder: (context, index) => ProductGridItem(product: products[index]),
)

4. CustomScrollView + Slivers — для сложных кастомных списков:

CustomScrollView(
  slivers: [
    // Заголовок (прилипающий)
    SliverAppBar(
      title: Text('Products'),
      floating: true,
      snap: true,
      expandedHeight: 200,
      flexibleSpace: FlexibleSpaceBar(
        background: Image.network('https://...', fit: BoxFit.cover),
      ),
    ),

    // Фильтры (не прокручиваются)
    SliverPersistentHeader(
      delegate: _StickyHeaderDelegate(
        child: FilterBar(onFilterChanged: _applyFilter),
      ),
      pinned: true,
    ),

    // Основной список продуктов
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ProductCard(product: filteredProducts[index]),
        childCount: filteredProducts.length,
      ),
    ),

    // Загрузка следующей страницы
    if (_isLoadingMore)
      SliverToBoxAdapter(
        child: Center(child: CircularProgressIndicator()),
      ),

    // Подвал
    SliverToBoxAdapter(
      child: Footer(),
    ),
  ],
)

5. ListView с различными типами элементов:

ListView.builder(
  itemCount: _items.length,
  itemBuilder: (context, index) {
    final item = _items[index];

    // Разные типы элементов в одном списке
    switch (item.type) {
      case 'header':
        return _buildHeader(item);
      case 'product':
        return _buildProductCard(item);
      case 'banner':
        return _buildBanner(item);
      case 'divider':
        return Divider(height: 24);
      default:
        return SizedBox.shrink();
    }
  },
)

6. Оптимизация производительности для больших списков:

// 1. Использование const конструкторов где возможно
ListView.builder(
  itemBuilder: (context, index) => const ProductCard(
    // const параметры
  ),
)

// 2. AutomaticKeepAliveClientMixin для сохранения состояния
class ProductCardState extends State<ProductCard> 
    with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true; // Сохранять состояние при скролле

  @override
  Widget build(BuildContext context) {
    super.build(context); // Обязательный вызов
    return ...;
  }
}

// 3. Кэширование сложных вычислений
ListView.builder(
  itemBuilder: (context, index) {
    final product = products[index];
    // Дорогие вычисления кэшируем
    final formattedPrice = _priceCache.putIfAbsent(
      product.id,
      () => _formatPrice(product.price),
    );
    return ProductCard(price: formattedPrice);
  },
)

7. Пакеты для специфичных задач:

  • flutter_staggered_grid_view — кастомные раскладки сетки
  • infinite_scroll_pagination — пагинация с подгрузкой
  • reorderables — перетаскивание элементов в списке
  • scrollable_positioned_list — прокрутка к конкретной позиции

В production-приложениях я чаще всего использую комбинацию: CustomScrollView с разными Slivers для сложных интерфейсов и ListView.builder/GridView.builder для ленивой загрузки больших наборов данных. Ключевое правило — всегда использовать builder-конструкторы для списков с переменным или большим количеством элементов, чтобы Flutter создавал виджеты только для видимой области.

Ответ 18+ 🔞

А, ну вот, Flutter и его списки, ебать мои старые костыли. Это как выбирать, на чём домой ехать: можно на метро, а можно на старой «копейке», которая бензина хавает как не в себя, но зато с душой. Сейчас разжую, что к чему, а то удивление пиздец, когда новички ListView с детьми на тысячу элементов пихают и потом орут, что приложение тормозит.

1. ListView — это как базовая шаурма, без изысков.

// Это когда ты заранее знаешь, что у тебя там три пункта меню и всё.
// Создаёт все виджеты сразу, даже те, что за экраном. Для длинных списков — пиздец.
ListView(
  padding: EdgeInsets.all(16),
  children: [
    ListTile(title: Text('Item 1'), leading: Icon(Icons.star)),
    ListTile(title: Text('Item 2'), leading: Icon(Icons.star)),
    ListTile(title: Text('Item 3'), leading: Icon(Icons.star)),
  ],
)

// А это уже поинтереснее, с разделителями. Но опять же, `itemCount` маленький должен быть.
ListView.separated(
  itemCount: 20,
  separatorBuilder: (context, index) => Divider(height: 1, color: Colors.grey[300]),
  itemBuilder: (context, index) => ListTile(
    title: Text('Item $index'),
    subtitle: Text('Subtitle $index'),
    trailing: Icon(Icons.chevron_right),
  ),
)

Суть в чём: separated — это builder под капотом, так что он ленивый. Но если ты в children засунешь сотню виджетов, то приложение будет грузиться, будто на дворе 2002-й год и интернет через модем.

2. ListView.builder — твой главный друг, когда данных овердохуища.

final List<Product> products = [...]; // Допустим, тысяча товаров из апи

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];
    return ProductCard(
      product: product,
      onTap: () => _openProductDetails(product),
    );
  },
  // Вот эти штуки — маст хэв для перформанса, ёпта.
  addAutomaticKeepAlives: true, // Чтобы состояние карточек не сбрасывалось, когда ты скролишь туда-сюда.
  addRepaintBoundaries: true,   // Чтобы перерисовывалась только нужная карточка, а не весь список.
  cacheExtent: 500,             // Он заранее создаст виджеты чуть выше и ниже видимой области, чтобы скролл был плавный.
)

Вот это уже серьёзно. Он создаёт виджеты только для тех элементов, которые на экране или вот-вот появятся. Остальные — похуй, их в памяти нет. Экономия — просто пизда рулю.

3. GridView — когда нужно вывести не в линию, а плиткой, как в маркете.

// Фиксированное число колонок. Просто и понятно.
GridView.count(
  crossAxisCount: 2, // Две колонки, хоть тресни.
  crossAxisSpacing: 8,
  mainAxisSpacing: 8,
  childAspectRatio: 0.8, // Важно поиграть с этим значением, а то квадраты будут кривые.
  children: List.generate(20, (index) => 
    Card(
      child: Column(
        children: [
          Image.network('https://picsum.photos/200?random=$index'),
          Text('Item $index'),
        ],
      ),
    ),
  ),
)

// А это — ленивая версия, для динамических данных. Тоже builder.
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: MediaQuery.of(context).size.width > 600 ? 4 : 2, // Адаптивность, ёб твою мать!
    childAspectRatio: 1.0,
  ),
  itemCount: products.length,
  itemBuilder: (context, index) => ProductGridItem(product: products[index]),
)

Запомни: GridView с children — для статики, GridView.builder — для всего остального. Иначе получишь хуй с горы в виде лагов.

4. CustomScrollView + Slivers — это высший пилотаж, для сложных, блядь, интерфейсов.

CustomScrollView(
  slivers: [
    // Это чтобы шапка красиво схлопывалась и выезжала.
    SliverAppBar(
      title: Text('Products'),
      floating: true,
      snap: true,
      expandedHeight: 200,
      flexibleSpace: FlexibleSpaceBar(
        background: Image.network('https://...', fit: BoxFit.cover),
      ),
    ),

    // А это чтобы панель фильтров прилипала сверху при скролле.
    SliverPersistentHeader(
      delegate: _StickyHeaderDelegate(
        child: FilterBar(onFilterChanged: _applyFilter),
      ),
      pinned: true,
    ),

    // Сам список товаров. Обрати внимание — SliverList, а не просто ListView.
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (context, index) => ProductCard(product: filteredProducts[index]),
        childCount: filteredProducts.length,
      ),
    ),

    // Индикатор загрузки в самом низу, когда подгружаем новую страницу.
    if (_isLoadingMore)
      SliverToBoxAdapter(
        child: Center(child: CircularProgressIndicator()),
      ),

    // И какой-нибудь футер в конце.
    SliverToBoxAdapter(
      child: Footer(),
    ),
  ],
)

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

5. Список с разными типами элементов — обычная история.

ListView.builder(
  itemCount: _items.length,
  itemBuilder: (context, index) {
    final item = _items[index];

    // Просто свитч-кейс, ничего сложного.
    switch (item.type) {
      case 'header':
        return _buildHeader(item);
      case 'product':
        return _buildProductCard(item);
      case 'banner':
        return _buildBanner(item);
      case 'divider':
        return Divider(height: 24);
      default:
        return SizedBox.shrink(); // На всякий пожарный.
    }
  },
)

Тут главное — логику в itemBuilder не засовывать тяжёлую, а то будет тормозить.

6. Оптимизация, или как не облажаться на больших списках.

// 1. Константы — твои лучшие друзья. Где можно, пиши const.
ListView.builder(
  itemBuilder: (context, index) => const ProductCard( // Вот этот const
    // ...параметры
  ),
)

// 2. Миксин для сохранения состояния. Например, чтобы видео в карточке не перезапускалось при скролле.
class ProductCardState extends State<ProductCard> 
    with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true; // Говорим: "Храни меня, мама!"

  @override
  Widget build(BuildContext context) {
    super.build(context); // ЭТУ СТРОКУ ПРОПУСТИТЬ НЕЛЬЗЯ, иначе накроется медным тазом.
    return ...;
  }
}

// 3. Кэшируй всё, что дорого считается. Не надо на каждом фрейме цены форматировать.
ListView.builder(
  itemBuilder: (context, index) {
    final product = products[index];
    final formattedPrice = _priceCache.putIfAbsent(
      product.id,
      () => _formatPrice(product.price), // Сложная функция форматирования.
    );
    return ProductCard(price: formattedPrice);
  },
)

7. Пакеты, когда родных возможностей не хватает.

Тут доверия ебать ноль к левым библиотекам, но некоторые реально годные:

  • flutter_staggered_grid_view — если нужна сетка с элементами разной высоты, как в Pinterest.
  • infinite_scroll_pagination — для пагинации с подгрузкой, чтоб не изобретать велосипед.
  • reorderables — чтобы пользователь мог перетаскивать элементы в списке местами.
  • scrollable_positioned_list — если надо программно проскроллить к 255-му элементу. Сам от себя охуеешь, когда попробуешь сделать это на чистом виджете.

Итог, чувак: Для простого списка — ListView.builder. Для сетки — GridView.builder. Если интерфейс — это ёперный театр с шапкой, баннерами и прилипающими штуками — CustomScrollView и Slivers. И никогда, слышишь, НИКОГДА не используй обычный ListView или GridView с children для динамических данных. Это прямой путь в ад с лагами и вылетами. Всё, теперь ты знаешь.