Explain ListView vs ListView.builder
2 min read
Widgets & UI
| Constructor | When children are built | When to use |
|---|---|---|
ListView(children: ...) | All at once | Static, small lists (~20 items, no dynamic data) |
ListView.builder(itemBuilder: ...) | Lazily, near the viewport | Most lists — anything dynamic or potentially long |
ListView.separated(...) | Lazily, with separators | Lists with dividers, sectioned UI |
ListView.custom(...) | Lazy, fully custom delegate | Advanced cases (chunked loading, custom indexing) |
Lazy variants use a SliverChildBuilderDelegate under the hood — only items in the viewport plus the cacheExtent are built; the rest are discarded.
Code in action
// Tiny static list — fine
ListView(
children: const [
ListTile(title: Text('Profile')),
ListTile(title: Text('Settings')),
ListTile(title: Text('Log out')),
],
);
// Long / dynamic list — use builder
ListView.builder(
itemCount: messages.length,
itemBuilder: (ctx, i) => MessageTile(messages[i]),
);
// With separators
ListView.separated(
itemCount: items.length,
itemBuilder: (ctx, i) => Tile(items[i]),
separatorBuilder: (_, __) => const Divider(height: 1),
);
// Pagination / infinite scroll
ListView.builder(
itemCount: items.length + 1,
itemBuilder: (ctx, i) {
if (i == items.length) {
_loadMore();
return const Center(child: CircularProgressIndicator());
}
return ItemTile(items[i]);
},
);
When to use which
| Situation | Use |
|---|---|
| Fewer than ~20 static items | ListView(children: [...]) |
| Dynamic list from a backend / state | ListView.builder |
| Dividers between rows | ListView.separated |
| Mixed content (header + list + footer) | CustomScrollView with SliverList + SliverToBoxAdapter |
| Horizontal list | Add scrollDirection: Axis.horizontal (works on both) |
| Pull-to-refresh | Wrap in RefreshIndicator |
Common mistakes to avoid
// ❌ Building giant lists eagerly
ListView(
children: bigList.map(buildHeavyTile).toList(), // 5000 widgets at once
);
// ✅ ListView.builder
// ❌ Forgetting itemCount when using builder
ListView.builder(itemBuilder: (ctx, i) => Tile(...)); // infinite!
// ❌ ListView inside a Column without a bounded height
Column(children: [const Header(), ListView(...)]); // 💥 unbounded height
// ✅ Wrap ListView in Expanded, OR use shrinkWrap: true (avoid for big lists)
// ❌ Mixing shrinkWrap + nested scrolling for performance
ListView(shrinkWrap: true, physics: NeverScrollableScrollPhysics(), ...);
// fine for tiny static lists; for long lists use Slivers / CustomScrollView
// ❌ No keys on stateful list items that can reorder
// (See Q21 — use ValueKey(item.id))
Interview follow-ups
-
What does
cacheExtentdo on aListView? It's the amount of off-screen pixels Flutter keeps built ahead/behind the viewport. Higher cache extent = smoother scrolling but more memory; lower = lighter but more rebuild flicker. The default is sensible for most cases. -
What's the difference between
ListView.builderandSliverList?ListView.builderis a scroll view with a single sliver.SliverListis the underlying primitive — use it directly insideCustomScrollViewwhen you want to combine multiple scrollable regions (e.g.,SliverAppBar+SliverList+SliverGrid). -
When would you use
shrinkWrap: trueand what's the cost? When aListViewmust size itself to its children (e.g., inside aColumn). The cost is that Flutter has to build all children to measure them, defeating laziness. Only acceptable for short lists. -
How do you implement infinite scroll / pagination? Detect when you're near the end (either with an extra "loading" tile at the bottom or via a
ScrollControllerlistener that triggers whenpixels >= maxScrollExtent - threshold) and fetch the next page, thensetState/ update your state store.
How helpful was this content?
Please sign in to rate this article.