Skip to content

Slivers & Scrolling

A scrollable list might contain ten thousand items, but the screen shows twenty. Building all ten thousand would be catastrophic. The key idea of Flutter scrolling is lazy building: only the items near the visible area are built, and they are recycled as you scroll.

This is why the constructor you choose matters enormously:

// ❌ Builds ALL children up front — fine for a few, fatal for thousands.
ListView(children: [ for (final item in items) Tile(item) ])
// ✅ Builds children on demand as they scroll into view.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => Tile(items[i]),
)

ListView.builder (and GridView.builder) call itemBuilder only for items the viewport needs. For any list that could be long, always use the builder form.

Under a scrollable is a viewport — the window you see — and inside it, slivers. A sliver is a scrollable region that knows how to lay itself out lazily against the viewport. ListView is a convenience that wraps a single sliver; when you need more than one scroll effect in one scroll view, you compose slivers yourself.

flowchart TB
  scroll["CustomScrollView"] --> vp["Viewport
(visible window)"]
  vp --> s1["SliverAppBar
(collapses on scroll)"]
  vp --> s2["SliverList
(builds only visible tiles)"]
  vp --> offscreen["off-screen tiles:
not built yet"]
A viewport lazily builds only the slivers in view

CustomScrollView: composing scroll effects

Section titled “CustomScrollView: composing scroll effects”

CustomScrollView takes a list of slivers and scrolls them as one. This is how you build a collapsing header above a list, or a grid and a list in the same scroll view.

CustomScrollView(
slivers: [
const SliverAppBar(
floating: true,
expandedHeight: 200, // a header that collapses as you scroll
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => Tile(items[i]), // still lazy
childCount: items.length,
),
),
],
)

The mental model: a normal widget occupies a fixed box; a sliver occupies a scrollable extent and negotiates with the viewport about how much of it is visible. SliverAppBar, SliverList, SliverGrid, and SliverToBoxAdapter (to drop a regular widget into a sliver list) are the pieces you compose.

  • Nesting scrollables the same direction (a ListView inside a ListView) fights over the scroll and throws unbounded-constraint errors — use slivers or shrinkWrap deliberately.
  • shrinkWrap: true makes a list size to its content instead of the viewport, but it builds everything and loses laziness — use it only for genuinely small lists.
  • Missing keys on reorderable lists cause the wrong state to attach to the wrong item (see the Keys lesson).
Why must you use ListView.builder for long lists?
What is a sliver?
When do you reach for CustomScrollView?
What is the downside of shrinkWrap: true on a large list?