Skip to content

CustomScrollView

Understand how to create complex scrollable layouts with slivers.


What is it?

CustomScrollView is a scrollable widget that allows you to combine multiple types of scrollable elements (slivers) into a single scrollable view. Unlike ListView or GridView, which only display one type of child, CustomScrollView lets you mix different scrollable elements like lists, grids, app bars, and custom widgets together.


Why does it exist?

CustomScrollView exists to:

  • Combine different scrollable elements
  • Create complex scrollable layouts
  • Implement scrollable app bars
  • Mix lists and grids in one scroll view
  • Create custom scrollable effects
  • Build advanced UI patterns
  • Enable smooth scrolling with mixed content

Basic CustomScrollView

CustomScrollView combines different slivers.

// Basic CustomScrollView
class BasicCustomScrollView extends StatelessWidget {
  const BasicCustomScrollView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // 1. App bar
          const SliverAppBar(
            title: Text('Custom Scroll View'),
            pinned: true,
          ),

          // 2. Text section
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Text(
                'Welcome to CustomScrollView',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),

          // 3. List of items
          SliverList(
            delegate: SliverChildListDelegate(
              [
                for (int i = 0; i < 10; i++)
                  Container(
                    height: 60,
                    color: Colors.blue[100 * (i % 9 + 1)],
                    margin: const EdgeInsets.symmetric(
                      horizontal: 16,
                      vertical: 4,
                    ),
                    child: Center(child: Text('List Item $i')),
                  ),
              ],
            ),
          ),

          // 4. Grid section
          SliverGrid(
            delegate: SliverChildListDelegate(
              [
                for (int i = 0; i < 6; i++)
                  Container(
                    color: Colors.green[100 * (i % 9 + 1)],
                    child: Center(child: Text('Grid $i')),
                  ),
              ],
            ),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
              crossAxisSpacing: 4,
              mainAxisSpacing: 4,
            ),
          ),
        ],
      ),
    );
  }
}

// CustomScrollView properties:
// 1. slivers - List of sliver widgets
// 2. scrollDirection - Axis.vertical or Axis.horizontal
// 3. reverse - Reverse scroll direction
// 4. controller - ScrollController
// 5. physics - ScrollPhysics
// 6. primary - Whether to use primary scroll view

What's happening here? - CustomScrollView combines multiple slivers - Mixes lists, grids, app bars, and adapters - All scroll together as one view - Flexible and powerful


Sliver Types

Different sliver types for different purposes.

// Sliver types overview
class SliverTypesOverview extends StatelessWidget {
  const SliverTypesOverview({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        // 1. SliverAppBar - Scrollable app bar
        SliverAppBar(
          title: const Text('Sliver Types'),
          expandedHeight: 150,
          flexibleSpace: const FlexibleSpaceBar(
            background: ColoredBox(
              color: Colors.blue,
              child: Center(
                child: Text(
                  'Custom App Bar',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          pinned: true,
        ),

        // 2. SliverList - List of children
        SliverList(
          delegate: SliverChildListDelegate(
            [
              for (int i = 0; i < 5; i++)
                ListTile(title: Text('List Item $i')),
            ],
          ),
        ),

        // 3. SliverGrid - Grid of children
        SliverGrid(
          delegate: SliverChildListDelegate(
            [
              for (int i = 0; i < 6; i++)
                Container(
                  color: Colors.blue[100 * (i % 9 + 1)],
                  child: Center(child: Text('Grid $i')),
                ),
            ],
          ),
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            crossAxisSpacing: 4,
            mainAxisSpacing: 4,
          ),
        ),

        // 4. SliverToBoxAdapter - Regular widget
        const SliverToBoxAdapter(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              'This is a regular widget in a sliver',
              style: TextStyle(fontSize: 16),
            ),
          ),
        ),

        // 5. SliverPadding - Padding around sliver
        SliverPadding(
          padding: const EdgeInsets.all(16),
          sliver: SliverToBoxAdapter(
            child: Container(
              height: 50,
              color: Colors.orange,
              child: const Center(child: Text('Padded Widget')),
            ),
          ),
        ),

        // 6. SliverFillRemaining - Fill remaining space
        const SliverFillRemaining(
          child: Center(
            child: Text(
              'Fills remaining space',
              style: TextStyle(fontSize: 18),
            ),
          ),
        ),
      ],
    );
  }
}

What's happening here? - SliverAppBar: scrollable app bar - SliverList: list of children - SliverGrid: grid of children - SliverToBoxAdapter: regular widget - SliverPadding: padding around sliver - SliverFillRemaining: fill remaining space


SliverAppBar

SliverAppBar provides scrollable app bar functionality.

// SliverAppBar features
class SliverAppBarFeatures extends StatelessWidget {
  const SliverAppBarFeatures({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        // 1. Standard SliverAppBar
        SliverAppBar(
          title: const Text('Standard App Bar'),
          expandedHeight: 200,
          flexibleSpace: const FlexibleSpaceBar(
            background: ColoredBox(
              color: Colors.blue,
              child: Center(
                child: Text(
                  'Flexible Space',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          // pinned - stays at top when scrolling
          pinned: true,
        ),

        // 2. Floating App Bar
        SliverAppBar(
          title: const Text('Floating App Bar'),
          expandedHeight: 150,
          flexibleSpace: const FlexibleSpaceBar(
            background: ColoredBox(
              color: Colors.green,
              child: Center(
                child: Text(
                  'Floating',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          // floating - appears when scrolling up
          floating: true,
        ),

        // 3. Snapping App Bar
        SliverAppBar(
          title: const Text('Snapping App Bar'),
          expandedHeight: 150,
          flexibleSpace: const FlexibleSpaceBar(
            background: ColoredBox(
              color: Colors.red,
              child: Center(
                child: Text(
                  'Snapping',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          // snap - snaps open when scrolling
          snap: true,
        ),

        // Content after app bar
        SliverList(
          delegate: SliverChildListDelegate(
            [
              for (int i = 0; i < 20; i++)
                Container(
                  height: 60,
                  color: Colors.blue[100 * (i % 9 + 1)],
                  margin: const EdgeInsets.all(4),
                  child: Center(child: Text('Item $i')),
                ),
            ],
          ),
        ),
      ],
    );
  }
}

// SliverAppBar properties:
// 1. title - App bar title
// 2. expandedHeight - Height when fully expanded
// 3. flexibleSpace - Space behind the app bar
// 4. pinned - Stays visible when scrolling
// 5. floating - Appears when scrolling up
// 6. snap - Snaps open when scrolling
// 7. collapsedHeight - Height when collapsed
// 8. stretch - Stretches when pulling down

What's happening here? - Pinned: stays at top - Floating: reappears on scroll up - Snap: snaps to expanded state - FlexibleSpace: background content


SliverList and SliverGrid

Efficient slivers for lists and grids.

// Efficient sliver delegates
class EfficientSlivers extends StatelessWidget {
  const EfficientSlivers({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const SliverAppBar(
          title: Text('Efficient Slivers'),
          pinned: true,
        ),

        // 1. SliverList with builder delegate
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return Container(
                height: 60,
                color: Colors.blue[100 * (index % 9 + 1)],
                margin: const EdgeInsets.all(4),
                child: Center(child: Text('List Item $index')),
              );
            },
            childCount: 50,
            // Performance optimizations
            addRepaintBoundaries: true,
            addSemanticIndexes: true,
          ),
        ),

        // 2. SliverGrid with builder delegate
        SliverGrid(
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return Container(
                color: Colors.green[100 * (index % 9 + 1)],
                child: Center(child: Text('Grid $index')),
              );
            },
            childCount: 30,
          ),
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            crossAxisSpacing: 4,
            mainAxisSpacing: 4,
          ),
        ),

        // 3. SliverFixedExtentList - Fixed height items
        SliverFixedExtentList(
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return Container(
                color: Colors.orange[100 * (index % 9 + 1)],
                child: Center(child: Text('Fixed $index')),
              );
            },
            childCount: 20,
          ),
          itemExtent: 50, // All items are 50px tall
        ),
      ],
    );
  }
}

What's happening here? - SliverChildBuilderDelegate: lazy building - SliverChildListDelegate: fixed children - SliverFixedExtentList: fixed height items - Performance optimizations


Real-World Examples

Common patterns using CustomScrollView.

// 1. Profile screen with header
class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // Profile header
          SliverAppBar(
            expandedHeight: 250,
            flexibleSpace: FlexibleSpaceBar(
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [
                      Colors.blue[400]!,
                      Colors.purple[400]!,
                    ],
                  ),
                ),
                child: const Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    CircleAvatar(
                      radius: 50,
                      backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
                    ),
                    SizedBox(height: 16),
                    Text(
                      'John Doe',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      'Flutter Developer',
                      style: TextStyle(
                        color: Colors.white70,
                        fontSize: 16,
                      ),
                    ),
                  ],
                ),
              ),
            ),
            pinned: true,
          ),

          // Stats section
          SliverToBoxAdapter(
            child: Container(
              padding: const EdgeInsets.all(16),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  _buildStat('Posts', '150'),
                  _buildStat('Followers', '2.5K'),
                  _buildStat('Following', '350'),
                ],
              ),
            ),
          ),

          // Posts list
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (context, index) {
                return Card(
                  margin: const EdgeInsets.symmetric(
                    horizontal: 16,
                    vertical: 8,
                  ),
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Row(
                          children: [
                            const CircleAvatar(
                              radius: 20,
                              backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
                            ),
                            const SizedBox(width: 12),
                            const Text(
                              'John Doe',
                              style: TextStyle(
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            const Spacer(),
                            Text(
                              '2h ago',
                              style: TextStyle(
                                color: Colors.grey[600],
                                fontSize: 12,
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 12),
                        Text(
                          'This is post number ${index + 1}. '
                          'It contains some content about Flutter.',
                          style: const TextStyle(height: 1.5),
                        ),
                        const SizedBox(height: 12),
                        Row(
                          children: [
                            IconButton(
                              icon: const Icon(Icons.favorite_border),
                              onPressed: () {},
                            ),
                            const Text('25'),
                            const SizedBox(width: 16),
                            IconButton(
                              icon: const Icon(Icons.comment_outlined),
                              onPressed: () {},
                            ),
                            const Text('8'),
                          ],
                        ),
                      ],
                    ),
                  ),
                );
              },
              childCount: 20,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildStat(String label, String value) {
    return Column(
      children: [
        Text(
          value,
          style: const TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
          ),
        ),
        Text(
          label,
          style: TextStyle(
            color: Colors.grey[600],
            fontSize: 14,
          ),
        ),
      ],
    );
  }
}

// 2. Store front
class StoreFront extends StatelessWidget {
  const StoreFront({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          const SliverAppBar(
            title: Text('Store'),
            pinned: true,
          ),

          // Banner
          SliverToBoxAdapter(
            child: Container(
              height: 150,
              margin: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(12),
              ),
              child: const Center(
                child: Text(
                  'Summer Sale!\n50% OFF',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),
          ),

          // Categories
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.symmetric(horizontal: 16),
              child: Text(
                'Categories',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),

          SliverPadding(
            padding: const EdgeInsets.all(8),
            sliver: SliverGrid(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  final categories = ['Electronics', 'Clothing', 'Books', 'Home'];
                  return Container(
                    decoration: BoxDecoration(
                      color: Colors.blue[100 * (index % 9 + 1)],
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(
                          Icons.category,
                          color: Colors.blue[700],
                          size: 32,
                        ),
                        const SizedBox(height: 4),
                        Text(categories[index % categories.length]),
                      ],
                    ),
                  );
                },
                childCount: 8,
              ),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 4,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
            ),
          ),

          // Products
          const SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.symmetric(horizontal: 16),
              child: Text(
                'Featured Products',
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ),

          SliverPadding(
            padding: const EdgeInsets.all(8),
            sliver: SliverGrid(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  return Card(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Expanded(
                          child: Container(
                            width: double.infinity,
                            color: Colors.blue[100 * (index % 9 + 1)],
                            child: const Center(
                              child: Icon(
                                Icons.shopping_bag,
                                color: Colors.white,
                                size: 40,
                              ),
                            ),
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.all(8),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                'Product ${index + 1}',
                                style: const TextStyle(
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              Text(
                                '\$${(index + 1) * 19.99}',
                                style: const TextStyle(
                                  color: Colors.blue,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  );
                },
                childCount: 20,
              ),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
                childAspectRatio: 0.7,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

What's happening here? - Profile screen with header and posts - Store front with banners, categories, products - Complex scrollable layouts - Mixed content types


Best Practices

Use Efficient Delegates

// Good - Builder delegate
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => Text('Item $index'),
    childCount: 1000,
  ),
)

// Bad - List delegate for large data
SliverList(
  delegate: SliverChildListDelegate(
    List.generate(1000, (index) => Text('Item $index')),
  ),
)

Use SliverPadding

// Good - SliverPadding for slivers
SliverPadding(
  padding: const EdgeInsets.all(16),
  sliver: SliverList(...),
)

// Bad - Padding inside sliver
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => Padding(
      padding: const EdgeInsets.all(16),
      child: Container(...),
    ),
  ),
)

Combine Slivers Strategically

// Good - Strategic combination
CustomScrollView(
  slivers: [
    SliverAppBar(...), // Header
    SliverToBoxAdapter(...), // Static content
    SliverList(...), // Scrollable list
    SliverGrid(...), // Grid
    SliverFillRemaining(...), // Footer
  ],
)

Common Mistakes

Mixing Slivers and Non-Slivers

Wrong:

// Can't mix directly
CustomScrollView(
  slivers: [
    Container(), // Error - must be Sliver
    ListView(), // Error - must be Sliver
  ],
)

Correct:

// Wrap in SliverToBoxAdapter
CustomScrollView(
  slivers: [
    SliverToBoxAdapter(child: Container()),
    SliverToBoxAdapter(child: ListView()),
  ],
)

Not Using Builder for Large Data

Wrong:

// All items built at once
SliverList(
  delegate: SliverChildListDelegate(
    List.generate(1000, (index) => Text('Item $index')),
  ),
)

Correct:

// Lazy loading
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => Text('Item $index'),
    childCount: 1000,
  ),
)


Summary

CustomScrollView combines different slivers into a single scrollable view. Use it for complex layouts with mixed content types like app bars, lists, grids, and custom widgets. Slivers provide lazy loading and efficient scrolling for large datasets.


Next Steps


Did You Know?

  • CustomScrollView combines multiple slivers
  • SliverAppBar provides scrollable app bars
  • SliverList and SliverGrid support lazy loading
  • SliverToBoxAdapter wraps regular widgets
  • SliverPadding adds padding to slivers
  • SliverFillRemaining fills remaining space
  • CustomScrollView is powerful for complex layouts
  • Slivers are the building blocks of scrollable content