ListView & GridView
Scrollable Lists and Grids
Open interactive version (quiz + challenge)Real-world analogy
Imagine a restaurant menu. A ListView is like a long menu you scroll down one item at a time — appetizers, mains, desserts, all in a single column. A GridView is like looking at a vending machine — items arranged in rows and columns in a grid. ListView.builder is like a magic menu that only prints the items you can see right now, saving paper (memory)!
What is it?
ListView and GridView are scrollable collection widgets. ListView arranges items in a single scrollable column (or row). GridView arranges items in a 2D grid. Both have .builder constructors that lazily create only visible items for optimal performance with large datasets.
Real-world relevance
In team_mvp_kit, ListView.builder powers every list screen — task lists, user directories, notification feeds. GridView.builder displays product catalogs and image galleries. The .builder pattern is essential because real apps often display hundreds of items fetched from an API, and creating all widgets upfront would waste memory and cause jank.
Key points
- ListView Basics — ListView is a scrollable list of widgets. By default it scrolls vertically. You can put any widgets as children. For short lists with a known number of items, use the default ListView constructor with a children list — Flutter builds all items immediately.
- ListView.builder for Performance — ListView.builder lazily builds items on demand — it only creates widgets that are currently visible on screen plus a small buffer. This is critical for large lists (hundreds or thousands of items). It takes an itemCount and an itemBuilder function that receives the index.
- ListView.separated — ListView.separated is like ListView.builder but adds a separator widget between each item. Use it to add dividers, spacing, or any widget between list items without manually adding them inside each item's builder. It takes both an itemBuilder and a separatorBuilder.
- GridView.count — GridView.count creates a grid with a fixed number of columns using crossAxisCount. Items flow left to right, top to bottom. It is the simplest way to create a grid. Use crossAxisSpacing and mainAxisSpacing to add gaps between grid items.
- GridView.builder — GridView.builder lazily builds grid items on demand, just like ListView.builder. Pair it with SliverGridDelegateWithFixedCrossAxisCount for a fixed number of columns, or SliverGridDelegateWithMaxCrossAxisExtent for a maximum item width that auto-calculates the column count.
- Scroll Direction — Both ListView and GridView scroll vertically by default. Set scrollDirection to Axis.horizontal for horizontal scrolling. Horizontal ListViews are common for image carousels, category chips, or story rows. Remember to constrain the height when using horizontal scroll inside a vertical layout.
- ScrollController — ScrollController lets you programmatically control and listen to scroll position. Attach it to a ListView to scroll to specific positions, detect when the user scrolls near the bottom (for infinite scrolling / pagination), or get the current scroll offset.
- ListTile Widget — ListTile is a pre-built row widget designed for lists. It has slots for leading (icon/avatar), title, subtitle, trailing (action widget), and onTap. It handles proper spacing, alignment, and ink splash automatically. Use it for contacts, settings, menus, and any standard list items.
Code example
import 'package:flutter/material.dart';
class ContactListScreen extends StatelessWidget {
const ContactListScreen({super.key});
@override
Widget build(BuildContext context) {
final contacts = List.generate(
50,
(i) => {
'name': 'Contact ${i + 1}',
'email': 'contact${i + 1}@example.com',
'initial': String.fromCharCode(65 + (i % 26)),
},
);
return Scaffold(
appBar: AppBar(title: const Text('Contacts')),
body: ListView.separated(
itemCount: contacts.length,
separatorBuilder: (context, index) {
return const Divider(height: 1, indent: 72);
},
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.primaries[index % Colors.primaries.length],
child: Text(
contact['initial']!,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(contact['name']!),
subtitle: Text(contact['email']!),
trailing: IconButton(
icon: const Icon(Icons.message_outlined),
onPressed: () {},
),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped ${contact["name"]}')),
);
},
);
},
),
);
}
}Line-by-line walkthrough
- 1. Import the Material package
- 2.
- 3. Define ContactListScreen as a StatelessWidget
- 4.
- 5.
- 6. Build method constructs the UI
- 7. Generate a list of 50 fake contacts with name, email, and initial
- 8. Each contact is a Map with string keys
- 9. Name is Contact 1 through Contact 50
- 10. Email follows a pattern
- 11. Initial cycles through A-Z using character codes
- 12.
- 13.
- 14. Return a Scaffold with an AppBar
- 15. AppBar shows 'Contacts' title
- 16. Body uses ListView.separated for dividers between items
- 17. itemCount is the total number of contacts
- 18. separatorBuilder creates a thin divider indented 72px to align with text
- 19.
- 20.
- 21. itemBuilder creates each contact row
- 22. Get the contact map for this index
- 23. Return a ListTile for the standard list item layout
- 24. Leading CircleAvatar with a color cycling through Material primary colors
- 25. Display the contact's initial letter
- 26. White bold text for contrast
- 27.
- 28.
- 29.
- 30. Title shows the contact name
- 31. Subtitle shows the email address
- 32. Trailing IconButton with a message icon
- 33.
- 34. onTap shows a SnackBar with the contact name
- 35.
- 36.
- 37.
- 38.
- 39.
Spot the bug
Column(
children: [
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
),
],
)Need a hint?
What happens when you put a scrollable widget inside an unbounded parent?
Show answer
ListView has infinite height but Column gives it unbounded vertical space, causing a 'Vertical viewport was given unbounded height' error. Fix: wrap the ListView.builder in an Expanded widget so it takes the remaining Column space: Expanded(child: ListView.builder(...))
Explain like I'm 5
Imagine you have 1000 baseball cards but your desk only fits 10 at a time. Instead of spreading all 1000 cards on the floor (that would be a mess!), ListView.builder is like a magical card holder that only shows the 10 cards you are looking at right now. When you slide to see the next cards, it puts away the ones you passed and brings out new ones. GridView is the same magic holder but it shows cards in rows and columns, like a binder page!
Fun fact
ListView.builder is so efficient that Flutter can smoothly scroll through a list of 1 million items without any lag. It achieves this by only keeping about 10-20 widgets in memory at a time, creating and destroying them as you scroll. This technique is called 'windowing' or 'virtualization' and is used by every major platform!
Hands-on challenge
Build a screen with two sections: a horizontal ListView at the top showing category cards (height 100, width 120, with colored containers and centered text), and below it a GridView.builder with 2 columns showing product cards with an image placeholder, title, and price. Add a ScrollController to the GridView that prints a message when the user scrolls near the bottom.
More resources
- ListView Class (Flutter API)
- GridView Class (Flutter API)
- Lists - Flutter Cookbook (Flutter Official)