FlutterJuniorTechnical

В чём разница между Column и ListView? Когда использовать ListView.builder?

Column строит всех детей сразу и не скроллится. ListView — скроллируемый виджет. ListView.builder создаёт элементы лениво по мере прокрутки — нужен при списке от ~20 элементов или неизвестной длине.

Column vs ListView: в чём разница

Column — layout-виджет без прокрутки. Он строит все дочерние виджеты сразу и размещает их вертикально. Если суммарная высота детей превышает доступное пространство, Flutter бросит исключение RenderFlex overflowed.

ListView — скроллируемый контейнер. В базовой форме ListView(children: [...]) он тоже строит всех детей сразу, но позволяет прокрутку. Когда список фиксированный и небольшой (до ~10–15 элементов), это допустимо.

ListView.builder создаёт элементы лениво: виджет строится только тогда, когда он попадает в viewport. Flutter вызывает itemBuilder по индексу по мере прокрутки и переиспользует RenderObject'ы, освобождая память для виджетов вне экрана.

Когда что использовать

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

Пример: все три варианта

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Column vs ListView')),
        body: const ListDemo(),
      ),
    );
  }
}

class ListDemo extends StatelessWidget {
  const ListDemo({super.key});

  // Генерируем 1000 элементов — Column здесь упадёт с overflow
  static final items = List.generate(1000, (i) => 'Элемент $i');

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // --- 1. Column: только для коротких фиксированных наборов ---
        const Text('Column (3 элемента, без прокрутки):',
            style: TextStyle(fontWeight: FontWeight.bold)),
        Column(
          children: const [
            ListTile(title: Text('A')),
            ListTile(title: Text('B')),
            ListTile(title: Text('C')),
          ],
        ),

        const Divider(),

        // --- 2. ListView.builder: 1000 элементов, ленивое построение ---
        const Text('ListView.builder (1000 элементов):',
            style: TextStyle(fontWeight: FontWeight.bold)),
        Expanded(
          child: ListView.builder(
            itemCount: items.length,
            // itemBuilder вызывается только для видимых + буферных элементов
            itemBuilder: (context, index) {
              return ListTile(
                leading: CircleAvatar(child: Text('$index')),
                title: Text(items[index]),
              );
            },
          ),
        ),
      ],
    );
  }
}

ListView.builder с реальным API

import 'package:flutter/material.dart';

// Имитация сетевого запроса
Future<List<String>> fetchJobs(int page) async {
  await Future.delayed(const Duration(milliseconds: 400));
  return List.generate(20, (i) => 'Job ${page * 20 + i}');
}

class JobListScreen extends StatefulWidget {
  const JobListScreen({super.key});

  @override
  State<JobListScreen> createState() => _JobListScreenState();
}

class _JobListScreenState extends State<JobListScreen> {
  final List<String> _jobs = [];
  int _page = 0;
  bool _loading = false;
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _loadMore();
    _scrollController.addListener(() {
      // Подгружаем следующую страницу при приближении к концу
      if (_scrollController.position.pixels >
          _scrollController.position.maxScrollExtent - 200) {
        _loadMore();
      }
    });
  }

  Future<void> _loadMore() async {
    if (_loading) return;
    setState(() => _loading = true);
    final newJobs = await fetchJobs(_page);
    setState(() {
      _jobs.addAll(newJobs);
      _page++;
      _loading = false;
    });
  }

  @override
  void dispose() {
    // ВАЖНО: всегда освобождать ScrollController
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Вакансии')),
      body: ListView.builder(
        controller: _scrollController,
        // +1 для индикатора загрузки внизу
        itemCount: _jobs.length + (_loading ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == _jobs.length) {
            return const Center(
              child: Padding(
                padding: EdgeInsets.all(16),
                child: CircularProgressIndicator(),
              ),
            );
          }
          return ListTile(
            title: Text(_jobs[index]),
            subtitle: Text('Страница ${index ~/ 20}'),
          );
        },
      ),
    );
  }
}

Подводные камни

  • Column внутри ListView без Expanded/Flexible. Если положить Column с Expanded прямо в ListView, получите ошибку — ListView даёт неограниченную высоту дочернему Column, а Expanded требует ограниченное пространство.
  • ListView.builder без itemCount = бесконечный скролл. Если не передать itemCount, Flutter будет вызывать itemBuilder бесконечно, пока builder не вернёт null. Легко получить выход за границы списка.
  • Неправильные ключи при динамическом списке. Если элементы меняются (удаление, вставка), без key Flutter может перепутать состояние виджетов — особенно критично для StatefulWidget внутри builder.
  • Забытый dispose для ScrollController. ScrollController, созданный в State, нужно освобождать в dispose(). Иначе — утечка памяти и ошибка при обращении к контроллеру после unmount.
  • Тяжёлые виджеты в itemBuilder без кэширования. itemBuilder вызывается при каждом rebuild. Если внутри создаётся дорогой виджет (например, с Image.network без кэша), список будет дёргаться при прокрутке.
  • Column не скроллится — не оборачивайте в SingleChildScrollView без необходимости. Комбинация SingleChildScrollView + Column строит всех детей сразу и не даёт преимуществ ленивой загрузки — используйте ListView.builder.
  • shrinkWrap: true убивает производительность. ListView(shrinkWrap: true) рассчитывает полный размер всего списка сразу, отключая ленивость. При больших списках это катастрофа для производительности.
  • setState из scrollListener после dispose. Если пользователь ушёл с экрана во время загрузки, setState в callback вызовет ошибку. Нужна проверка if (!mounted) return; перед каждым setState в асинхронном коде.

Common mistakes

  • Сводить «Column и ListView? Когда использовать ListView.builder» к синтаксису и не объяснять widget tree.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии flutter-10.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «Column и ListView? Когда использовать ListView.builder» и подтверждает ее корректным примером.
  • Умеет связать ответ с element tree, тестированием и отладкой на устройстве.
  • Называет ограничения подхода flutter-10, включая производительность, память и сопровождение.

Sources

Related topics