Flutter Layouts & Responsive Design

SliverAppBar, SliverList & SliverGrid

50 min Lesson 13 of 16

Introduction to Slivers

Slivers are the building blocks of scrollable areas in Flutter. While widgets like ListView and GridView are convenient, they are actually built on top of slivers. When you need advanced scrolling effects — collapsing headers, sticky elements, or mixed scroll layouts — you work directly with slivers inside a CustomScrollView.

Key Concept: A sliver is a portion of a scrollable area. The word “sliver” means a thin slice — each sliver handles only its own piece of the scroll viewport. CustomScrollView composes multiple slivers into one unified scrollable surface.

CustomScrollView Basics

The CustomScrollView widget takes a list of slivers in its slivers property. Every child must be a sliver widget (prefixed with Sliver). You cannot mix regular widgets and slivers directly.

Basic CustomScrollView Structure

class MyScrollPage extends StatelessWidget {
  const MyScrollPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // Sliver widgets go here
          const SliverAppBar(
            title: Text('My App'),
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                title: Text('Item \$index'),
              ),
              childCount: 50,
            ),
          ),
        ],
      ),
    );
  }
}

SliverAppBar

The SliverAppBar is one of the most powerful sliver widgets. It creates an app bar that can expand, collapse, float, pin, and snap as the user scrolls. It replaces the standard AppBar when used inside a CustomScrollView.

Key Properties

  • expandedHeight — The height of the app bar when fully expanded.
  • floating — If true, the app bar becomes visible as soon as the user scrolls up, even if they haven’t reached the top.
  • pinned — If true, the collapsed app bar remains visible at the top.
  • snap — If true (requires floating: true), the app bar snaps open or closed rather than partially showing.
  • flexibleSpace — A widget that stretches to fill the expanded area, typically a FlexibleSpaceBar.

SliverAppBar with FlexibleSpaceBar

class CollapsingHeaderPage extends StatelessWidget {
  const CollapsingHeaderPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 250.0,
            floating: false,
            pinned: true,
            snap: false,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('Mountain View'),
              centerTitle: true,
              background: Image.network(
                'https://picsum.photos/800/400',
                fit: BoxFit.cover,
              ),
              collapseMode: CollapseMode.parallax,
            ),
            actions: [
              IconButton(
                icon: const Icon(Icons.share),
                onPressed: () {},
              ),
            ],
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) => ListTile(
                leading: CircleAvatar(child: Text('\${index + 1}')),
                title: Text('Item \${index + 1}'),
                subtitle: const Text('Subtitle text'),
              ),
              childCount: 30,
            ),
          ),
        ],
      ),
    );
  }
}
Tip: Use CollapseMode.parallax for a parallax scrolling effect on the background image, CollapseMode.pin to keep the background stationary, or CollapseMode.none for no special behavior.

Floating and Snapping

The floating and snap properties work together to control how the app bar reappears when the user scrolls up mid-list.

Floating + Snapping SliverAppBar

SliverAppBar(
  expandedHeight: 200.0,
  floating: true,
  pinned: false,
  snap: true,
  flexibleSpace: FlexibleSpaceBar(
    title: const Text('Quick Access Bar'),
    background: Container(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Colors.deepPurple, Colors.indigo],
        ),
      ),
    ),
  ),
)
Warning: Setting snap: true requires floating: true. If you set snap: true without floating: true, Flutter will throw an assertion error at runtime.

SliverList

SliverList displays a linear list of children inside a CustomScrollView. Flutter 3.x provides convenient constructors that simplify creation.

SliverList.builder

The most common constructor, equivalent to ListView.builder. It lazily builds children on demand.

SliverList.builder Example

final List<String> cities = [
  'London', 'Paris', 'Tokyo', 'New York',
  'Sydney', 'Dubai', 'Berlin', 'Rome',
];

SliverList.builder(
  itemCount: cities.length,
  itemBuilder: (context, index) {
    return Card(
      margin: const EdgeInsets.symmetric(
        horizontal: 16,
        vertical: 4,
      ),
      child: ListTile(
        leading: const Icon(Icons.location_city),
        title: Text(cities[index]),
        trailing: const Icon(Icons.chevron_right),
        onTap: () {
          // Navigate to city detail
        },
      ),
    );
  },
)

SliverList.separated

This constructor adds a separator widget between each item, just like ListView.separated.

SliverList.separated Example

SliverList.separated(
  itemCount: 20,
  itemBuilder: (context, index) {
    return Padding(
      padding: const EdgeInsets.symmetric(
        horizontal: 16,
        vertical: 8,
      ),
      child: Row(
        children: [
          CircleAvatar(
            radius: 24,
            backgroundColor: Colors.primaries[index % Colors.primaries.length],
            child: Text(
              '\${index + 1}',
              style: const TextStyle(color: Colors.white),
            ),
          ),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Contact \${index + 1}',
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 16,
                  ),
                ),
                Text(
                  'contact\${index + 1}@example.com',
                  style: TextStyle(color: Colors.grey[600]),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  },
  separatorBuilder: (context, index) => const Divider(
    height: 1,
    indent: 72,
  ),
)

SliverGrid

SliverGrid lays out children in a two-dimensional grid inside a CustomScrollView. You control the grid layout with a gridDelegate.

SliverGrid with SliverGridDelegateWithFixedCrossAxisCount

SliverGrid(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 8.0,
    crossAxisSpacing: 8.0,
    childAspectRatio: 1.0,
  ),
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      return Card(
        color: Colors.primaries[index % Colors.primaries.length],
        child: Center(
          child: Text(
            'Tile \${index + 1}',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      );
    },
    childCount: 20,
  ),
)
Tip: Use SliverGridDelegateWithMaxCrossAxisExtent when you want each tile to have a maximum width and let Flutter calculate the number of columns automatically. This is great for responsive grid layouts.

SliverFixedExtentList

When all items in a list have the same fixed height, SliverFixedExtentList is more efficient than SliverList because Flutter can calculate item positions without measuring each child.

SliverFixedExtentList Example

SliverFixedExtentList(
  itemExtent: 72.0,
  delegate: SliverChildBuilderDelegate(
    (context, index) {
      return Container(
        alignment: Alignment.centerLeft,
        padding: const EdgeInsets.symmetric(horizontal: 16),
        decoration: BoxDecoration(
          border: Border(
            bottom: BorderSide(
              color: Colors.grey.shade200,
            ),
          ),
        ),
        child: Row(
          children: [
            Icon(
              Icons.music_note,
              color: Colors.blue.shade400,
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Text(
                'Track \${index + 1} - Song Title',
                style: const TextStyle(fontSize: 16),
              ),
            ),
            Text(
              '3:\${(index * 7 % 60).toString().padLeft(2, '0')}',
              style: TextStyle(color: Colors.grey[600]),
            ),
          ],
        ),
      );
    },
    childCount: 100,
  ),
)

SliverAnimatedList

SliverAnimatedList provides animated insertions and removals within a CustomScrollView. It works like AnimatedList but as a sliver.

SliverAnimatedList Example

class AnimatedSliverListPage extends StatefulWidget {
  const AnimatedSliverListPage({super.key});

  @override
  State<AnimatedSliverListPage> createState() =>
      _AnimatedSliverListPageState();
}

class _AnimatedSliverListPageState extends State<AnimatedSliverListPage> {
  final GlobalKey<SliverAnimatedListState> _listKey =
      GlobalKey<SliverAnimatedListState>();
  final List<String> _items = ['Apple', 'Banana', 'Cherry'];

  void _addItem() {
    final index = _items.length;
    _items.add('Fruit \${index + 1}');
    _listKey.currentState?.insertItem(
      index,
      duration: const Duration(milliseconds: 400),
    );
  }

  void _removeItem(int index) {
    final removed = _items.removeAt(index);
    _listKey.currentState?.removeItem(
      index,
      (context, animation) => SizeTransition(
        sizeFactor: animation,
        child: Card(
          color: Colors.red.shade100,
          child: ListTile(title: Text(removed)),
        ),
      ),
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          const SliverAppBar(
            title: Text('Animated Sliver List'),
            pinned: true,
          ),
          SliverAnimatedList(
            key: _listKey,
            initialItemCount: _items.length,
            itemBuilder: (context, index, animation) {
              return SizeTransition(
                sizeFactor: animation,
                child: Card(
                  margin: const EdgeInsets.symmetric(
                    horizontal: 16,
                    vertical: 4,
                  ),
                  child: ListTile(
                    title: Text(_items[index]),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () => _removeItem(index),
                    ),
                  ),
                ),
              );
            },
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addItem,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Practical Example: App Store Layout

Let’s combine everything into a real-world example — an app store page with a collapsing header, a horizontal featured section, and a grid of apps.

Complete App Store Layout

class AppStorePage extends StatelessWidget {
  const AppStorePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // Collapsing Header
          SliverAppBar(
            expandedHeight: 200,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('App Store'),
              background: Container(
                decoration: const BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                    colors: [Colors.blue, Colors.indigo],
                  ),
                ),
                child: const Center(
                  child: Icon(
                    Icons.store,
                    size: 64,
                    color: Colors.white70,
                  ),
                ),
              ),
            ),
          ),

          // Section Title
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'Top Apps',
                style: TextStyle(
                  fontSize: 22,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),

          // App Grid
          SliverPadding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            sliver: SliverGrid(
              gridDelegate:
                  const SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 200,
                mainAxisSpacing: 12,
                crossAxisSpacing: 12,
                childAspectRatio: 0.75,
              ),
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Card(
                    elevation: 2,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        CircleAvatar(
                          radius: 30,
                          backgroundColor: Colors
                              .primaries[index % Colors.primaries.length],
                          child: const Icon(
                            Icons.apps,
                            color: Colors.white,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          'App \${index + 1}',
                          style: const TextStyle(
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 4),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: List.generate(
                            5,
                            (i) => Icon(
                              Icons.star,
                              size: 14,
                              color: i < 4
                                  ? Colors.amber
                                  : Colors.grey.shade300,
                            ),
                          ),
                        ),
                      ],
                    ),
                  );
                },
                childCount: 12,
              ),
            ),
          ),

          // Bottom Spacing
          const SliverToBoxAdapter(
            child: SizedBox(height: 32),
          ),
        ],
      ),
    );
  }
}
Summary: Slivers unlock advanced scrolling patterns in Flutter. SliverAppBar provides collapsing headers with flexible space. SliverList and SliverGrid handle linear and grid layouts within a CustomScrollView. Use SliverFixedExtentList for performance when items share the same height, and SliverAnimatedList for animated additions and removals. Combine multiple slivers in one CustomScrollView for rich, performant scrollable interfaces.