Custom Widgets & Custom Painting

Slivers & Custom Scroll Views

16 min Lesson 11 of 12

Slivers & Custom Scroll Views

Flutter's standard scrolling widgets — ListView, GridView, SingleChildScrollView — are convenient, but they offer little control over how individual parts of a scrollable area behave. Slivers are the lower-level building blocks that power all Flutter scroll views. A sliver is a portion of a scrollable area that can change its geometry (size, position, visual effect) in response to scroll offset. By composing slivers inside a CustomScrollView, you can create sophisticated layouts such as collapsing app bars, sticky section headers, and mixed list-grid feeds.

Note: Every ListView and GridView is internally implemented with slivers. When you need more than one scrollable region type — e.g., a large banner followed by a grid — you must drop down to the sliver layer and use CustomScrollView directly.

CustomScrollView — The Container

CustomScrollView accepts a list of slivers in its slivers property and scrolls them as a single unified viewport. All children share the same ScrollController and scroll physics, which is why the expanding app bar and the list beneath it collapse in perfect sync.

Minimal CustomScrollView skeleton

CustomScrollView(
  slivers: [
    // 1. A collapsing app bar
    SliverAppBar(
      expandedHeight: 200.0,
      floating: false,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        title: const Text('My Feed'),
        background: Image.network(
          'https://picsum.photos/800/400',
          fit: BoxFit.cover,
        ),
      ),
    ),

    // 2. A section of list items
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return ListTile(title: Text('Item $index'));
        },
        childCount: 20,
      ),
    ),

    // 3. A section of grid items
    SliverGrid(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        mainAxisSpacing: 8.0,
        crossAxisSpacing: 8.0,
        childAspectRatio: 1.0,
      ),
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return Container(color: Colors.teal, child: Center(child: Text('$index')));
        },
        childCount: 12,
      ),
    ),
  ],
)

Core Sliver Widgets

SliverAppBar

SliverAppBar integrates with the scroll offset to expand and collapse a header area. Key properties:

  • expandedHeight — the total height when fully expanded.
  • pinned: true — keeps the toolbar visible after collapsing.
  • floating: true — re-expands as soon as the user scrolls up, even mid-list.
  • snap: true — used together with floating; snaps the bar fully open or closed.
  • flexibleSpace: FlexibleSpaceBar — renders the expanding area (background image, title that fades or shifts).

SliverList

SliverList renders children lazily in a linear list. Use SliverChildBuilderDelegate for large or infinite lists, or SliverChildListDelegate for a small, fixed set of widgets.

SliverGrid

SliverGrid renders children in a two-dimensional grid. The gridDelegate is either SliverGridDelegateWithFixedCrossAxisCount (fixed column count) or SliverGridDelegateWithMaxCrossAxisExtent (automatic column count based on max item width).

SliverToBoxAdapter

Wraps a single normal (box) widget so it can be placed inside a CustomScrollView. Use this for standalone sections like a banner, a search bar, or a heading row that don't belong to a list or grid.

SliverPadding

Applies padding around another sliver. Equivalent to wrapping a box widget in Padding, but operates in sliver geometry.

Tip: Mix and match these slivers freely. A common recipe: SliverAppBarSliverToBoxAdapter (promo banner) → SliverList (featured items) → SliverGrid (all items). Each section scrolls as one seamless experience.

SliverPersistentHeader & Custom Delegates

For fully bespoke collapsing headers — ones that change layout, animate child elements, or apply custom blends as they collapse — use SliverPersistentHeader with a hand-crafted SliverPersistentHeaderDelegate.

You must subclass SliverPersistentHeaderDelegate and implement three members:

  • double get minExtent — the minimum height of the header (fully collapsed).
  • double get maxExtent — the maximum height of the header (fully expanded).
  • Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) — called on every frame; shrinkOffset goes from 0 (expanded) to maxExtent - minExtent (fully collapsed).
  • bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) — return true when the delegate's configuration changes.

Custom collapsing profile header

class ProfileHeaderDelegate extends SliverPersistentHeaderDelegate {
  final String username;
  final String avatarUrl;

  const ProfileHeaderDelegate({
    required this.username,
    required this.avatarUrl,
  });

  @override
  double get minExtent => 60.0;

  @override
  double get maxExtent => 200.0;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    // Compute how far along the collapse we are (0.0 = open, 1.0 = closed)
    final double t = (shrinkOffset / (maxExtent - minExtent)).clamp(0.0, 1.0);
    final double avatarSize = lerpDouble(80.0, 32.0, t)!;

    return Container(
      color: Colors.indigo,
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          CircleAvatar(
            radius: avatarSize / 2,
            backgroundImage: NetworkImage(avatarUrl),
          ),
          const SizedBox(width: 12.0),
          Text(
            username,
            style: TextStyle(
              color: Colors.white,
              fontSize: lerpDouble(22.0, 16.0, t)!,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }

  @override
  bool shouldRebuild(covariant ProfileHeaderDelegate oldDelegate) {
    return oldDelegate.username != username ||
        oldDelegate.avatarUrl != avatarUrl;
  }
}

// Usage inside CustomScrollView:
SliverPersistentHeader(
  pinned: true,
  delegate: ProfileHeaderDelegate(
    username: 'Edrees Salih',
    avatarUrl: 'https://picsum.photos/200',
  ),
),
Warning: Never use a fixed height inside the build method of a SliverPersistentHeaderDelegate. The framework constrains the header to the range [minExtent, maxExtent], so any inner widget that demands a height outside that range will either overflow or be clipped unexpectedly.

Performance Considerations

Slivers render only what is currently visible in the viewport (lazy evaluation), making them suitable for long or infinite lists. Keep these rules in mind:

  • Prefer SliverChildBuilderDelegate over SliverChildListDelegate for lists longer than ~20 items — the builder version builds items on demand.
  • Avoid nesting a ListView (or any other shrink-wrap scroll view) inside a CustomScrollView sliver — use SliverList instead to keep everything in the same scroll context.
  • The build method of your SliverPersistentHeaderDelegate is called on every scroll frame; keep it cheap (no heavy computation, no synchronous I/O).

Summary

Slivers give you granular control over every segment of a scrollable UI. Compose a CustomScrollView from SliverAppBar, SliverList, SliverGrid, SliverToBoxAdapter, and SliverPadding for standard needs. For bespoke collapsing headers that animate or change layout as they shrink, implement a SliverPersistentHeaderDelegate and compute your widget geometry using the shrinkOffset parameter. This combination unlocks rich, performant scroll experiences that would be impossible with ordinary box-layout widgets.