Flutter Scrollable Widgets: ListView, GridView, and CustomScrollView
When building Flutter apps, you'll often need to display lists of items that users can scroll through. Whether it's a simple list of text items, a grid of images, or a complex scrolling interface, Flutter provides powerful widgets to handle these scenarios efficiently. In this article, we'll explore the three main scrollable widgets: ListView, GridView, and CustomScrollView, and learn when to use each one.
Understanding Scrollable Widgets
Scrollable widgets in Flutter are designed to handle content that exceeds the available screen space. They automatically manage scrolling behavior, handle touch gestures, and optimize performance by only rendering visible items. This lazy loading approach makes them incredibly efficient, even when dealing with thousands of items.
The three main scrollable widgets each serve different purposes:
- ListView: Displays items in a single column or row
- GridView: Displays items in a two-dimensional grid
- CustomScrollView: Combines multiple scrollable widgets into a single scrollable area
Visual Overview
Here's how the three scrollable widgets differ in structure:
ListView: The Foundation of Lists
ListView: The Foundation of Lists
ListView is the most commonly used scrollable widget. It displays a linear list of items that can scroll vertically (default) or horizontally. ListView comes in several constructors, each optimized for different use cases.
ListView.builder: Efficient for Large Lists
The ListView.builder constructor is perfect when you have a large or infinite list. It builds items on-demand as they scroll into view, making it memory-efficient.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${items[index]}'),
subtitle: Text('This is item number ${index + 1}'),
);
},
)
The itemBuilder callback receives the BuildContext and the current index. It only builds widgets that are currently visible on screen, which is why it's so efficient for large lists.
ListView.separated: Adding Dividers
When you need visual separation between items, ListView.separated is your friend. It automatically inserts a separator widget between each item.
ListView.separated(
itemCount: items.length,
separatorBuilder: (context, index) => Divider(),
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${items[index]}'),
);
},
)
ListView: Simple Lists
For small, fixed lists, you can use the default ListView constructor with a list of children. This is convenient but less efficient for large lists since all items are built upfront.
ListView(
children: [
ListTile(title: Text('First Item')),
ListTile(title: Text('Second Item')),
ListTile(title: Text('Third Item')),
],
)
Horizontal ListView
To create a horizontal scrolling list, set the scrollDirection property:
ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: items.length,
itemBuilder: (context, index) {
return Container(
width: 150,
margin: EdgeInsets.all(8),
child: Card(
child: Center(child: Text('Item ${items[index]}')),
),
);
},
)
GridView: Two-Dimensional Layouts
GridView displays items in a two-dimensional grid, perfect for image galleries, product catalogs, or any content that benefits from a grid layout. Like ListView, GridView has multiple constructors optimized for different scenarios.
GridView.builder: Efficient Grids
The GridView.builder constructor builds grid items on-demand, making it efficient for large grids:
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1.0,
),
itemCount: items.length,
itemBuilder: (context, index) {
return Card(
child: Center(child: Text('Item ${items[index]}')),
);
},
)
The SliverGridDelegateWithFixedCrossAxisCount delegate specifies that you want a fixed number of columns (2 in this case). You can also control spacing between items and the aspect ratio of each grid item.
GridView.count: Simple Fixed Grids
For simpler cases, GridView.count provides a more straightforward API:
GridView.count(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
children: List.generate(20, (index) {
return Card(
child: Center(child: Text('Item $index')),
);
}),
)
GridView.extent: Maximum Width-Based Grids
When you want grid items to have a maximum width rather than a fixed number of columns, use GridView.extent:
GridView.extent(
maxCrossAxisExtent: 200,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
children: List.generate(30, (index) {
return Card(
child: Center(child: Text('Item $index')),
);
}),
)
This creates a grid where each item has a maximum width of 200 pixels, and the number of columns adjusts automatically based on the available screen width.
CustomScrollView: Combining Scrollable Widgets
Sometimes you need more than a simple list or grid. You might want to combine a header, a list, and a grid all in a single scrollable area. This is where CustomScrollView shines. It uses slivers, which are pieces of scrollable content that can be combined together.
Understanding Slivers
Slivers are widgets that can produce different effects as you scroll. Common slivers include:
SliverAppBar: An app bar that can expand, collapse, or pinSliverList: A list that works within a CustomScrollViewSliverGrid: A grid that works within a CustomScrollViewSliverToBoxAdapter: Wraps regular widgets to use in CustomScrollView
Basic CustomScrollView Example
Here's a simple example combining a header and a list:
CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.blue,
child: Center(
child: Text(
'Header Section',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('List Item $index'),
);
},
childCount: 50,
),
),
],
)
Advanced CustomScrollView with AppBar
A more sophisticated example uses SliverAppBar with a collapsible header:
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text('My App'),
background: Image.network(
'https://example.com/header-image.jpg',
fit: BoxFit.cover,
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('Item $index'),
);
},
childCount: 100,
),
),
],
)
The SliverAppBar can expand when scrolled to the top and collapse as you scroll down. The pinned: true property keeps the app bar visible even when collapsed.
Combining List and Grid
You can even combine different scrollable widgets in a single CustomScrollView:
CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'Featured Items',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
),
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return Card(
child: Center(child: Text('Grid Item $index')),
);
},
childCount: 10,
),
),
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'All Items',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text('List Item $index'),
);
},
childCount: 50,
),
),
],
)
Performance Considerations
All three scrollable widgets are optimized for performance, but there are some best practices to keep in mind:
- Use builder constructors: Always prefer
ListView.builderandGridView.builderover their non-builder counterparts for large lists - Avoid heavy computations in itemBuilder: Keep the itemBuilder callback lightweight. If you need to process data, do it beforehand
- Use const constructors: When possible, use const widgets to help Flutter optimize rebuilds
- Consider itemExtent: For ListView with items of known height, provide
itemExtentto improve performance
Using itemExtent for Better Performance
When all items have the same height, specifying itemExtent helps Flutter calculate scroll positions more efficiently:
ListView.builder(
itemExtent: 80, // Fixed height for all items
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${items[index]}'),
);
},
)
Common Patterns and Tips
Adding Pull-to-Refresh
You can add pull-to-refresh functionality using RefreshIndicator:
RefreshIndicator(
onRefresh: () async {
// Fetch new data
await fetchData();
},
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${items[index]}'),
);
},
),
)
Handling Empty States
Always handle the case when your list is empty:
if (items.isEmpty) {
return Center(
child: Text('No items to display'),
);
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${items[index]}'),
);
},
)
Adding Loading States
Show a loading indicator while data is being fetched:
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item ${items[index]}'),
);
},
)
Choosing the Right Widget
Here's a quick guide to help you choose:
- Use ListView when you need a simple vertical or horizontal list of items
- Use GridView when you need a two-dimensional grid layout, like an image gallery or product grid
- Use CustomScrollView when you need to combine multiple scrollable sections, add collapsible headers, or create complex scrolling interfaces
Conclusion
Flutter's scrollable widgets are powerful and flexible tools for building dynamic user interfaces. ListView handles simple lists efficiently, GridView creates beautiful grid layouts, and CustomScrollView lets you combine multiple scrollable sections into cohesive experiences. By understanding when to use each widget and following performance best practices, you can create smooth, responsive scrolling experiences in your Flutter apps.
Remember to always use builder constructors for large lists, handle empty and loading states gracefully, and don't be afraid to combine widgets to create the exact scrolling experience your users need. Happy coding!