В чём разница между 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. Легко получить выход за границы списка. - Неправильные ключи при динамическом списке. Если элементы меняются (удаление, вставка), без
keyFlutter может перепутать состояние виджетов — особенно критично для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, включая производительность, память и сопровождение.