Skip to content

ScrollView

Understand the foundation of scrolling in Flutter.


What is it?

ScrollView is the base widget for all scrollable content in Flutter. It provides the infrastructure for scrolling, including the viewport, scrollable widget, and physics. While you typically use specialized scrollable widgets like ListView, GridView, and CustomScrollView, understanding ScrollView helps you understand how scrolling works in Flutter.


Why does it exist?

ScrollView exists to:

  • Provide scrolling infrastructure
  • Handle scrollable content
  • Manage viewport and rendering
  • Support different scroll behaviors
  • Enable lazy loading of children
  • Handle scroll physics and interactions
  • Support custom scrolling configurations

ScrollView Basics

ScrollView is the foundation for scrolling widgets.

// Basic ScrollView usage
class BasicScrollView extends StatelessWidget {
  const BasicScrollView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ScrollView')),
      body: ScrollView(
        // Custom scroll view with children
        // Usually you'd use ListView or CustomScrollView
        slivers: [
          SliverList(
            delegate: SliverChildListDelegate(
              [
                for (int i = 0; i < 20; i++)
                  Container(
                    height: 50,
                    color: Colors.blue[100 * (i % 9 + 1)],
                    margin: const EdgeInsets.all(4),
                    child: Center(child: Text('Item $i')),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ScrollView properties:
// 1. slivers - List of sliver widgets
// 2. scrollDirection - Axis.vertical or Axis.horizontal
// 3. reverse - Whether to reverse scroll direction
// 4. controller - ScrollController for controlling scroll
// 5. physics - ScrollPhysics for behavior
// 6. viewportBuilder - Builds the viewport
// 7. scrollBehavior - Custom scroll behavior
// 8. clipBehavior - Clipping behavior
// 9. keyboardDismissBehavior - Dismiss keyboard on scroll

What's happening here? - ScrollView is the base for all scrolling - Slivers are the building blocks - Supports vertical and horizontal scrolling - Customizable physics and controllers - Usually use specialized widgets


ScrollView vs Other Scrolling Widgets

Comparing ScrollView with specialized scrollable widgets.

// Different scrolling widgets
class ScrollingWidgetsComparison extends StatelessWidget {
  const ScrollingWidgetsComparison({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 1. ListView - Most common
        Expanded(
          child: ListView(
            children: [
              for (int i = 0; i < 20; i++)
                Container(
                  height: 50,
                  color: Colors.blue[100 * (i % 9 + 1)],
                  child: Center(child: Text('ListView $i')),
                ),
            ],
          ),
        ),

        // 2. CustomScrollView - Sliver-based
        Expanded(
          child: CustomScrollView(
            slivers: [
              SliverList(
                delegate: SliverChildListDelegate(
                  [
                    for (int i = 0; i < 20; i++)
                      Container(
                        height: 50,
                        color: Colors.green[100 * (i % 9 + 1)],
                        child: Center(child: Text('Custom $i')),
                      ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

// ScrollView hierarchy:
// ScrollView (abstract)
//   ├── BoxScrollView (abstract)
//   │   ├── ListView
//   │   └── GridView
//   └── CustomScrollView
//       └── (uses Slivers directly)

// When to use:
// ListView: Simple lists
// GridView: Grid layouts
// CustomScrollView: Complex scrollable layouts with mixed slivers
// ScrollView: Rarely used directly

What's happening here? - ListView is most common for lists - GridView for grid layouts - CustomScrollView for complex layouts - ScrollView is usually not used directly


ScrollView Slivers

Slivers are the building blocks of ScrollView.

// Sliver types in ScrollView
class SliverTypesExample extends StatelessWidget {
  const SliverTypesExample({super.key});

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

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

        // 3. SliverGrid - Grid of children
        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,
          ),
        ),

        // 4. SliverFixedExtentList - Fixed height list
        SliverFixedExtentList(
          delegate: SliverChildListDelegate(
            [
              for (int i = 0; i < 5; i++)
                Container(
                  color: Colors.orange[100 * (i % 9 + 1)],
                  child: Center(child: Text('Fixed $i')),
                ),
            ],
          ),
          itemExtent: 60,
        ),

        // 5. SliverPadding - Adds padding
        const SliverPadding(
          padding: EdgeInsets.all(16),
          sliver: SliverToBoxAdapter(
            child: Text('Padded content'),
          ),
        ),
      ],
    );
  }
}

What's happening here? - SliverAppBar: scrollable app bar - SliverList: list of children - SliverGrid: grid of children - SliverFixedExtentList: fixed height list - SliverPadding: adds padding to slivers


ScrollController

ScrollController controls scrolling programmatically.

// ScrollController example
class ScrollControllerExample extends StatefulWidget {
  const ScrollControllerExample({super.key});

  @override
  State<ScrollControllerExample> createState() => _ScrollControllerExampleState();
}

class _ScrollControllerExampleState extends State<ScrollControllerExample> {
  final ScrollController _controller = ScrollController();
  double _scrollPosition = 0;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      setState(() {
        _scrollPosition = _controller.position.pixels;
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ScrollController'),
        actions: [
          TextButton(
            onPressed: () {
              setState(() {});
            },
            child: Text('Position: ${_scrollPosition.toInt()}'),
          ),
        ],
      ),
      body: Column(
        children: [
          // Scroll controls
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: () {
                  _controller.animateTo(
                    0,
                    duration: const Duration(milliseconds: 500),
                    curve: Curves.easeInOut,
                  );
                },
                child: const Text('Top'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: () {
                  _controller.animateTo(
                    _controller.position.maxScrollExtent,
                    duration: const Duration(milliseconds: 500),
                    curve: Curves.easeInOut,
                  );
                },
                child: const Text('Bottom'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: () {
                  _controller.jumpTo(_controller.position.maxScrollExtent / 2);
                },
                child: const Text('Middle'),
              ),
            ],
          ),

          const SizedBox(height: 8),

          // Scrollable content
          Expanded(
            child: ListView.builder(
              controller: _controller,
              itemCount: 50,
              itemBuilder: (context, index) {
                return Container(
                  height: 60,
                  color: Colors.blue[100 * ((index + 1) % 9 + 1)],
                  margin: const EdgeInsets.symmetric(
                    horizontal: 8,
                    vertical: 4,
                  ),
                  child: Center(
                    child: Text(
                      'Item $index',
                      style: const TextStyle(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

// ScrollController properties:
// 1. position - Current scroll position
// 2. offset - Current scroll offset
// 3. hasClients - Whether controller is attached
// 4. hasListeners - Whether listeners are attached

// ScrollController methods:
// 1. animateTo() - Animated scroll to position
// 2. jumpTo() - Jump to position
// 3. addListener() - Add scroll listener
// 4. removeListener() - Remove scroll listener
// 5. dispose() - Clean up controller

What's happening here? - Controller tracks scroll position - Animate to specific positions - Jump to positions instantly - Listen to scroll events - Control scrolling programmatically


ScrollPhysics

ScrollPhysics controls scroll behavior and physics.

// ScrollPhysics examples
class ScrollPhysicsExample extends StatelessWidget {
  const ScrollPhysicsExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 1. BouncingScrollPhysics - iOS style
        Expanded(
          child: ListView.builder(
            physics: const BouncingScrollPhysics(),
            itemCount: 30,
            itemBuilder: (context, index) {
              return Container(
                height: 50,
                color: Colors.red[100 * (index % 9 + 1)],
                margin: const EdgeInsets.all(4),
                child: Center(child: Text('Bouncing $index')),
              );
            },
          ),
        ),

        // 2. ClampingScrollPhysics - Android style
        Expanded(
          child: ListView.builder(
            physics: const ClampingScrollPhysics(),
            itemCount: 30,
            itemBuilder: (context, index) {
              return Container(
                height: 50,
                color: Colors.green[100 * (index % 9 + 1)],
                margin: const EdgeInsets.all(4),
                child: Center(child: Text('Clamping $index')),
              );
            },
          ),
        ),

        // 3. NeverScrollableScrollPhysics - Disable scrolling
        Expanded(
          child: ListView.builder(
            physics: const NeverScrollableScrollPhysics(),
            itemCount: 10,
            itemBuilder: (context, index) {
              return Container(
                height: 50,
                color: Colors.blue[100 * (index % 9 + 1)],
                margin: const EdgeInsets.all(4),
                child: Center(child: Text('Not Scrollable $index')),
              );
            },
          ),
        ),

        // 4. AlwaysScrollableScrollPhysics - Always scrollable
        Expanded(
          child: ListView.builder(
            physics: const AlwaysScrollableScrollPhysics(),
            itemCount: 5,
            itemBuilder: (context, index) {
              return Container(
                height: 50,
                color: Colors.orange[100 * (index % 9 + 1)],
                margin: const EdgeInsets.all(4),
                child: Center(child: Text('Always Scrollable $index')),
              );
            },
          ),
        ),
      ],
    );
  }
}

// ScrollPhysics options:
// 1. BouncingScrollPhysics - Bounces at edge (iOS)
// 2. ClampingScrollPhysics - Clamps at edge (Android)
// 3. NeverScrollableScrollPhysics - Disables scrolling
// 4. AlwaysScrollableScrollPhysics - Always scrollable
// 5. RangeMaintainingScrollPhysics - Maintains range
// 6. Custom physics - Extend ScrollPhysics

What's happening here? - BouncingScrollPhysics: iOS-style bounce - ClampingScrollPhysics: Android-style clamp - NeverScrollableScrollPhysics: no scrolling - AlwaysScrollableScrollPhysics: always scrollable


ScrollBehavior

ScrollBehavior customizes scroll appearance.

// Custom scroll behavior
class CustomScrollBehavior extends ScrollBehavior {
  @override
  Widget buildViewportChrome(
    BuildContext context,
    Widget child,
    AxisDirection axisDirection,
  ) {
    // Custom scrollbar
    return GlowingOverscrollIndicator(
      child: child,
      axisDirection: axisDirection,
      color: Colors.blue,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ScrollBehavior')),
      body: ScrollConfiguration(
        behavior: CustomScrollBehavior(),
        child: ListView.builder(
          itemCount: 50,
          itemBuilder: (context, index) {
            return Container(
              height: 60,
              color: Colors.blue[100 * (index % 9 + 1)],
              margin: const EdgeInsets.all(4),
              child: Center(
                child: Text(
                  'Item $index',
                  style: const TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

What's happening here? - Custom scroll behavior - Custom scrollbar appearance - Overscroll indicator customization - Platform-specific behavior


Real-World Examples

Common patterns using scrollable widgets.

// 1. Infinite scrolling with pagination
class InfiniteScrollExample extends StatefulWidget {
  const InfiniteScrollExample({super.key});

  @override
  State<InfiniteScrollExample> createState() => _InfiniteScrollExampleState();
}

class _InfiniteScrollExampleState extends State<InfiniteScrollExample> {
  List<int> _items = [];
  bool _isLoading = false;
  final ScrollController _controller = ScrollController();

  @override
  void initState() {
    super.initState();
    _loadMore();
    _controller.addListener(() {
      if (_controller.position.pixels >= _controller.position.maxScrollExtent - 200) {
        _loadMore();
      }
    });
  }

  Future<void> _loadMore() async {
    if (_isLoading) return;
    setState(() => _isLoading = true);

    await Future.delayed(const Duration(seconds: 1));
    final start = _items.length;
    _items.addAll(List.generate(20, (i) => start + i));

    setState(() => _isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Infinite Scroll')),
      body: ListView.builder(
        controller: _controller,
        itemCount: _items.length + (_isLoading ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == _items.length) {
            return const Center(child: CircularProgressIndicator());
          }
          return Container(
            height: 60,
            color: Colors.blue[100 * (_items[index] % 9 + 1)],
            margin: const EdgeInsets.all(4),
            child: Center(
              child: Text(
                'Item ${_items[index]}',
                style: const TextStyle(fontWeight: FontWeight.bold),
              ),
            ),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

// 2. Horizontal scroll
class HorizontalScrollExample extends StatelessWidget {
  const HorizontalScrollExample({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 120,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: 20,
        itemBuilder: (context, index) {
          return Container(
            width: 100,
            margin: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: Colors.blue[100 * (index % 9 + 1)],
              borderRadius: BorderRadius.circular(8),
            ),
            child: Center(
              child: Text(
                'Item $index',
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

// 3. Scroll to top button
class ScrollToTopExample extends StatefulWidget {
  const ScrollToTopExample({super.key});

  @override
  State<ScrollToTopExample> createState() => _ScrollToTopExampleState();
}

class _ScrollToTopExampleState extends State<ScrollToTopExample> {
  final ScrollController _controller = ScrollController();
  bool _showButton = false;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      setState(() {
        _showButton = _controller.position.pixels > 100;
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _scrollToTop() {
    _controller.animateTo(
      0,
      duration: const Duration(milliseconds: 500),
      curve: Curves.easeInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Scroll to Top')),
      body: Stack(
        children: [
          ListView.builder(
            controller: _controller,
            itemCount: 50,
            itemBuilder: (context, index) {
              return Container(
                height: 60,
                color: Colors.blue[100 * (index % 9 + 1)],
                margin: const EdgeInsets.all(4),
                child: Center(
                  child: Text(
                    'Item $index',
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              );
            },
          ),
          if (_showButton)
            Positioned(
              bottom: 20,
              right: 20,
              child: FloatingActionButton(
                onPressed: _scrollToTop,
                child: const Icon(Icons.arrow_upward),
              ),
            ),
        ],
      ),
    );
  }
}

What's happening here? - Infinite scrolling with pagination - Horizontal scroll view - Scroll to top with floating button


Best Practices

Use Appropriate Widget

// Good - ListView for simple lists
@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      return Text(items[index]);
    },
  );
}

// Bad - Using CustomScrollView for simple list
@override
Widget build(BuildContext context) {
  return CustomScrollView(
    slivers: [
      SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, index) => Text(items[index]),
          childCount: items.length,
        ),
      ),
    ],
  );
}

Dispose ScrollController

// Good - Dispose controller
@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

// Bad - Not disposing
@override
void dispose() {
  super.dispose();
}

Use Lazy Loading

// Good - Lazy loading
@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: 1000,
    itemBuilder: (context, index) {
      return Text('Item $index');
    },
  );
}

// Bad - All items built at once
@override
Widget build(BuildContext context) {
  return ListView(
    children: List.generate(1000, (index) {
      return Text('Item $index');
    }),
  );
}

Common Mistakes

Not Using Builder

Wrong:

// Inefficient for large lists
ListView(
  children: [
    for (int i = 0; i < 1000; i++)
      Text('Item $i'),
  ],
)

Correct:

// Efficient for large lists
ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return Text('Item $index');
  },
)

Not Disposing Controller

Wrong:

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final ScrollController _controller = ScrollController();
  // No dispose method
}

Correct:

class _MyWidgetState extends State<MyWidget> {
  final ScrollController _controller = ScrollController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}


Summary

ScrollView is the foundation for all scrollable content in Flutter. It provides scrolling infrastructure and supports various scroll behaviors through slivers, controllers, and physics. Use specialized widgets like ListView, GridView, and CustomScrollView for most use cases.


Next Steps


Did You Know?

  • ScrollView is the base for all scrolling
  • Slivers are the building blocks of ScrollView
  • ListView and GridView extend ScrollView
  • CustomScrollView uses slivers directly
  • ScrollController controls scroll position
  • ScrollPhysics controls scroll behavior
  • ScrollBehavior customizes appearance
  • Lazy loading improves performance