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

«Какие способы реализации списков в Flutter вы знаете?» — вопрос из категории Алгоритмы и структуры данных, который задают на 29% собеседований 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 создавал виджеты только для видимой области.