Flutter Lesson 19 of 83 3 min read
ListView in Flutter: Scrolling Lists
Learn ListView in Flutter: ListView.builder for long lists, separators, ListTile, horizontal lists, and fixing common list errors.
On this page
ListView is a scrolling column. It is the widget behind message lists, feeds, settings pages and most data screens.
A short, fixed list #
ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(leading: Icon(Icons.wifi), title: Text('Wi-Fi')),
ListTile(leading: Icon(Icons.bluetooth), title: Text('Bluetooth')),
ListTile(leading: Icon(Icons.battery_full), title: Text('Battery')),
],
)
This builds every child immediately. That is fine for a dozen items.
ListView.builder for long lists #
ListView.builder builds items only when they scroll into view. It handles ten items or a million with the same memory.
import 'package:flutter/material.dart';
void main() {
final contacts = List.generate(500, (i) => 'Contact ${i + 1}');
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Contacts')),
body: ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
final name = contacts[index];
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(name),
subtitle: const Text('Tap to view'),
trailing: const Icon(Icons.chevron_right),
onTap: () => debugPrint('Tapped $name'),
);
},
),
),
),
);
}
itemBuilder is called with an index and returns the widget for that row. Use the builder whenever the list comes from data.
Separators #
ListView.separated(
itemCount: items.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) => ListTile(title: Text(items[index])),
)
ListTile #
ListTile is a ready-made row with slots.
| Slot | Usually holds |
|---|---|
leading | An icon or avatar |
title | The main text |
subtitle | Secondary text |
trailing | An icon, switch or value |
onTap | What happens on tap |
For a row that does not fit this shape, return your own widget from itemBuilder.
Horizontal lists #
SizedBox(
height: 120,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: 10,
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) => Container(
width: 160,
decoration: BoxDecoration(
color: Colors.teal.shade100,
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.center,
child: Text('Card $index'),
),
),
)
A horizontal list needs a fixed height from its parent.
A list inside a Column #
Placing a ListView directly in a Column throws “unbounded height”. Give it the remaining space with Expanded.
Column(
children: [
const Padding(padding: EdgeInsets.all(16), child: Text('Recent orders')),
Expanded(
child: ListView.builder(
itemCount: orders.length,
itemBuilder: (context, i) => ListTile(title: Text(orders[i])),
),
),
],
)
You may see shrinkWrap: true suggested. It works, but it makes the list build all its items to measure itself, which defeats the purpose of the builder. Use it only for a handful of items. For a page that mixes headers and lists and scrolls as one, use slivers.
Empty lists #
body: items.isEmpty
? const Center(child: Text('Nothing here yet'))
: ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ListTile(title: Text(items[i])),
),
Controlling the scroll #
final _controller = ScrollController();
// Scroll to the top with an animation.
_controller.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
// Remember to call _controller.dispose() in dispose().
Pass it to the list with controller: _controller.
Performance #
- Use
ListView.builderorListView.separatedfor anything from data. - When every row has the same height, set
itemExtent. Scrolling to a position becomes instant. - Keep row widgets
constwhere you can, and keep theirbuildmethods light.
Try it yourself #
Build a to-do list screen from a List<String>. Show each task in a ListTile with a checkbox on the left, dividers between rows, and a friendly message when the list is empty.