Updated June 2026. Tested on Flutter 3.x and Dart 3. Part of the Techalyst Flutter series.
Lists are in almost every app: feeds, settings, search results, chats. Flutter makes them easy, but there is one decision that separates a smooth list from one that stutters and eats memory, and it is which ListView constructor you pick. Get that right and lists are a non-issue.
The plain ListView
The default constructor takes a list of children and builds every one of them up front:
ListView(
children: const [
ListTile(title: Text('Profile')),
ListTile(title: Text('Notifications')),
ListTile(title: Text('Privacy')),
],
)
This is fine for a short, fixed list like a settings screen. The catch is the words "every one of them up front". If you feed it five hundred items, it builds all five hundred immediately, even the ones far off screen. That is wasted work and wasted memory.
ListView.builder for real lists
For anything long or driven by data, use ListView.builder. It is lazy: it only builds the items that are actually visible, plus a small buffer, and builds more as you scroll. You give it a count and a function that builds one item by index.
ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
);
},
)
A list of ten thousand items costs about the same as a list of ten, because off-screen items simply do not exist yet. Make this your default for any list that comes from data.
Adding separators
When you want dividers between items, ListView.separated builds them for you without you interleaving widgets by hand:
ListView.separated(
itemCount: products.length,
itemBuilder: (context, index) => ListTile(title: Text(products[index].name)),
separatorBuilder: (context, index) => const Divider(),
)
The Column overflow trap
The most common list error is putting a ListView straight inside a Column. Both want unbounded height, so Flutter cannot size them and throws. The fix is to wrap the list in Expanded so it takes the height that is actually left over:
Column(
children: [
const Text('Header'),
Expanded(child: ListView.builder(...)), // bounded now
],
)
This is the constraints model in action, which we go through in Flutter layout and constraints. If you genuinely need a non-scrolling list inside another scroll view, shrinkWrap: true with physics: NeverScrollableScrollPhysics() does it, but reach for that sparingly, because it gives up the laziness that makes builder fast.
A simple infinite scroll
Long lists usually load a page at a time. The basic pattern is to watch the scroll position and load the next page as the user nears the bottom. A ScrollController makes that clean:
final _controller = ScrollController();
@override
void initState() {
super.initState();
_controller.addListener(() {
final nearBottom = _controller.position.pixels >=
_controller.position.maxScrollExtent - 300;
if (nearBottom && !_loading) _loadNextPage();
});
}
Pass controller: _controller to the ListView.builder, append the new items to your list, and call setState. Remember to dispose the controller when the widget goes away, the same cleanup habit as any other resource.
Wrapping up
Lists in Flutter come down to one choice. Use the plain ListView only for short, fixed lists, and reach for ListView.builder for anything long or data-driven so off-screen items are never built. Use ListView.separated for dividers, wrap a list in Expanded when it lives inside a Column, and add a ScrollController near-bottom check when you need pagination. Pick the lazy builder by default and your lists stay smooth no matter how much data you throw at them.
All comments ()
No comments yet
Be the first to leave a comment on this post.