skip to content

In Flutter, what is a sliver, and when does a CustomScrollView with slivers beat a plain ListView?

level: middleimportance: must knowfreq 60%

answer

  1. one scroll, many pieces
  2. laid out by scroll offset
  3. SliverConstraints down, SliverGeometry up
  4. ListView wraps one SliverList
  5. collapse, pin, list then grid

basics

~20 s

A sliver is a piece of a scrollable laid out by scroll offset instead of box size. CustomScrollView stacks several slivers in one viewport, so a collapsing header, a list and a grid scroll as one lazy surface; a ListView holds exactly one list sliver.

solid answer

~40 s

A **sliver** is a portion of a scrollable area. Instead of `BoxConstraints`, the viewport gives each sliver `SliverConstraints` (how far it is scrolled, how much paint space remains) and the sliver answers with a `SliverGeometry` (`scrollExtent`, `paintExtent`, `layoutExtent`). That lets list slivers build only the children in the visible range. `ListView` is itself a scroll view that builds one `SliverList` (or a fixed-extent variant) inside a viewport. I switch to `CustomScrollView` when one scroll must combine different pieces: a `SliverAppBar` that collapses, a `SliverList` of rooms followed by a `SliverGrid` of photos, a pinned header, a footer that fills leftover space. Every list piece stays lazy and there is one scroll position, instead of scrollables nested in a `Column`.

code

dart · 41 lines
dart
import 'package:flutter/material.dart';

class HotelDetailPage extends StatelessWidget {
  const HotelDetailPage({super.key, required this.rooms, required this.photoUrls});

  final List<String> rooms;
  final List<String> photoUrls;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            expandedHeight: 280,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('Harbour View Hotel'),
              background: Image.asset('assets/hotel_cover.jpg', fit: BoxFit.cover),
            ),
          ),
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Text('12 Quay Street - 4.6 rating'),
            ),
          ),
          SliverList.builder(
            itemCount: rooms.length,
            itemBuilder: (context, index) => ListTile(title: Text(rooms[index])),
          ),
          SliverGrid.builder(
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
            itemCount: photoUrls.length,
            itemBuilder: (context, index) => Image.network(photoUrls[index], fit: BoxFit.cover),
          ),
        ],
      ),
    );
  }
}

go deeper

for a junior

Recall that a CustomScrollView takes a list of slivers and that SliverList, SliverGrid and SliverAppBar can share one scroll.

for a middle

Explain the sliver protocol in terms of SliverConstraints and SliverGeometry, and show that ListView is one SliverList inside a viewport.

for a senior

Justify the switch with laziness and a single scroll position, and spot where an adapter quietly builds everything.

for a principal

Frame slivers as the composition primitive for scrolling screens and set a team rule for when a ListView stays a ListView.

## What a sliver is A **sliver** is a portion of a scrollable area that lays itself out *relative to the scroll position*. Every Flutter scroll view that shows many children is a **viewport** (the window you look through) with one or more slivers inside it. The widgets you write are sliver widgets such as `SliverList`, `SliverGrid`, `SliverAppBar` or `SliverToBoxAdapter`; each creates a `RenderSliver`, the render object that speaks the sliver layout protocol. `CustomScrollView` is the scroll view that lets you supply that list of slivers yourself, through its `slivers` parameter. Everything in the list scrolls as **one** surface with a single scroll position, one scrollbar and one set of physics. ## Box layout versus sliver layout Ordinary widgets use the **box protocol**: the parent passes `BoxConstraints` down, the child picks a `Size` and passes it up. A scrollable is unbounded in its scroll direction and most of its content is off screen, so slivers use a different contract: | | Box protocol | Sliver protocol | |---|---|---| | Passed down | `BoxConstraints` (min and max width and height) | `SliverConstraints` (`scrollOffset`, `remainingPaintExtent`, `overlap`, `precedingScrollExtent`, `crossAxisExtent`) | | Passed up | a `Size` | a `SliverGeometry` (`scrollExtent`, `paintExtent`, `layoutExtent`, `maxPaintExtent`) | | Question answered | how big am I? | how much of me is scrolled past, how much do I paint now, how far do I push the next sliver? | The viewport lays its slivers out in order. Each is told how far into it the user has scrolled and how much paintable space is left; it reports its total length (`scrollExtent`), what it paints right now (`paintExtent`) and how much space it consumes before the next sliver starts (`layoutExtent`). A list sliver only needs the children that fall inside the visible range plus a cache region, so it **builds lazily**: rows far off screen are never built. ## ListView is already a sliver in disguise `ListView` is not a separate mechanism. Its `buildChildLayout` returns a single `SliverList`, or `SliverFixedExtentList` when you pass `itemExtent`, `SliverPrototypeExtentList` for `prototypeItem`, and `SliverVariedExtentList` for `itemExtentBuilder`. `GridView` returns one `SliverGrid`. So the real choice is not "lazy ListView versus heavy CustomScrollView"; it is **one sliver or several**. ## When CustomScrollView wins Reach for `CustomScrollView` when one scroll has to contain pieces of different kinds: - a header that **collapses or pins** as the user scrolls (`SliverAppBar`, `SliverPersistentHeader`, `PinnedHeaderSliver`); - a **list followed by a grid**, or several lists with headings between them; - a block that should **fill whatever space is left** below short content (`SliverFillRemaining`); - padding, grouping or decoration applied **at sliver level** (`SliverPadding`, `SliverMainAxisGroup`, `DecoratedSliver`). The usual workaround without slivers — a `ListView` and a `GridView` stacked in a `Column` inside a `SingleChildScrollView` — forces the inner lists to measure every child to report a height, which throws laziness away. With slivers each list stays lazy, and there is exactly one scroll position to save, restore or drive. ## When a ListView is enough - The screen is one homogeneous list, perhaps with a static bar above it. - Nothing collapses, pins or fills. - The convenience constructors (`ListView.builder`, `ListView.separated`) cover the need. A `ListView` is shorter to write and produces the same sliver underneath, so there is no performance reason to rewrite one as a `CustomScrollView` with a single `SliverList`. ## A hotel-detail page as slivers 1. `SliverAppBar` with `pinned: true`, an `expandedHeight` and a `FlexibleSpaceBar` background photo, collapsing to a toolbar. 2. `SliverToBoxAdapter` for the single address-and-rating block. 3. `SliverList.builder` for the room types, built lazily. 4. `SliverGrid.builder` for the photo wall, also lazy. 5. `SliverFillRemaining(hasScrollBody: false)` for a booking footer when the content is short. All five scroll together, nothing is nested, and nothing far off screen is built. That composition — heterogeneous, lazy and single-scroll — is the case interviewers want to hear when they ask why slivers exist.

  • Is everything inside a CustomScrollView built lazily?
    Only the multi-child slivers are lazy. `SliverList`, `SliverGrid` and their fixed-extent variants build children in the visible range plus the cache region. A `SliverToBoxAdapter` builds and lays out its whole box child whenever the sliver itself is laid out, so a `Column` of hundreds of rows inside one adapter loses laziness entirely.
  • What does the viewport pass to each sliver and what does it get back?
    It passes `SliverConstraints`: the `scrollOffset` into this sliver, the `remainingPaintExtent`, any `overlap` from pinned slivers before it, the `precedingScrollExtent` and the `crossAxisExtent`. The sliver returns `SliverGeometry`: its total `scrollExtent`, the `paintExtent` it paints now, the `layoutExtent` that pushes the next sliver, and its `maxPaintExtent`.
  • Why not replace every ListView with a CustomScrollView holding one SliverList?
    Because `ListView` already builds exactly that: one list sliver in a viewport. Rewriting it gains nothing at runtime and costs readability. The switch pays off only when you add a second kind of sliver, such as a collapsing app bar, a grid or a fill-remaining footer.

A viewport is a window over a long roll of film. Each sliver is a strip on the roll: it is told how much of itself has already slid past the window and how much window is left, and it answers how much of itself to show and how far to push the next strip.

saying these in an interview costs you the question

  • A sliver is just a widget with a fixed height.
  • CustomScrollView builds all its children up front, unlike ListView.
  • ListView and CustomScrollView use unrelated layout mechanisms.
  • Any widget can go straight into the slivers list.
  • Mixing a list and a grid needs two nested scroll views.