Master Flutter Lists: ListView, GridView & Builder | (Ch. 10)
Welcome to Chapter 10! We have reached double digits.
Think about the apps you use every day. Instagram? It's a scrollable list of photos. WhatsApp? A scrollable list of chats. YouTube? A scrollable list of videos. Your phone's Settings? A scrollable list of options.
90% of mobile app development is just displaying lists of data.
So far, we've used Column to stack widgets. But
Column has a fatal flaw: it doesn't scroll.
If you put too many items in a Column, you get the dreaded
yellow-and-black "Overflow" error.
In this chapter, we are going to solve that. We will master the
ListView and
GridView widgets. We will learn how to make
static lists, but more importantly, we will learn how to use
Builders to efficiently display thousands of items without
crashing your app.
📖 Chapter 10: Table of Contents
1. The Basic `ListView` (Static Lists)
The simplest way to make a list is to replace your Column
with a ListView.
A basic ListView takes a list of children, just
like a Column. The difference is that a ListView
automatically provides scrolling when the items overflow the screen.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Basic List')),
body: ListView(
padding: EdgeInsets.all(20),
children: [
Text('Item 1', style: TextStyle(fontSize: 24)),
Text('Item 2', style: TextStyle(fontSize: 24)),
Text('Item 3', style: TextStyle(fontSize: 24)),
// Imagine 50 more items here...
Container(height: 200, color: Colors.red),
Container(height: 200, color: Colors.blue),
Container(height: 200, color: Colors.green),
],
),
);
}
Run this, and try to drag the screen up and down. It scrolls! It also gives you a nice "bounce" effect on iOS or a "glow" effect on Android automatically.
Vertical vs. Horizontal Scrolling
By default, lists scroll vertically. But think about the "Stories" bar on Instagram or Netflix movie categories. They scroll horizontally.
You can change the scroll direction easily:
ListView(
scrollDirection: Axis.horizontal, // Changes flow to Left -> Right
children: [
Container(width: 100, color: Colors.red),
Container(width: 100, color: Colors.green),
Container(width: 100, color: Colors.blue),
],
)
Note: If you put a horizontal `ListView` inside a vertical `Column`, you must wrap the `ListView` in a `SizedBox` or `Expanded` to give it a defined height. Otherwise, Flutter won't know how tall to make the list and will crash.
2. The Best Friend: `ListTile`
In Chapter 5 (the Business Card project), we manually built rows using Row, Icon, and Text.
Flutter gives us a specialized widget designed specifically for lists: the ListTile. It conforms to Material Design standards and creates perfectly aligned rows.
A ListTile has three main slots:
- leading: An widget on the far left (usually an Icon or Avatar).
- title: The main text.
- subtitle: Smaller text below the title.
- trailing: A widget on the far right (usually an arrow or checkbox).
ListView(
children: [
ListTile(
leading: Icon(Icons.map),
title: Text('Map'),
subtitle: Text('Navigate the world'),
trailing: Icon(Icons.chevron_right),
onTap: () {
print('Map Tapped');
},
),
ListTile(
leading: Icon(Icons.photo_album),
title: Text('Album'),
subtitle: Text('View your photos'),
trailing: Icon(Icons.chevron_right),
),
ListTile(
leading: Icon(Icons.phone),
title: Text('Phone'),
trailing: Switch(value: true, onChanged: (v){}),
),
],
)
Notice the onTap property? `ListTile` comes with built-in touch detection and a nice "splash" effect when tapped. You don't even need a `GestureDetector`!
3. The Professional Way: `ListView.builder`
Using ListView(children: []) is fine for settings screens with 10-20 items.
But what if you are building a social media feed with 10,000 posts?
Why `Column` and `ListView()` are bad for big data
If you create a standard `ListView` with 10,000 items, Flutter tries to build all 10,000 widgets immediately when the app loads.
This will freeze your app, consume all your memory (RAM), and likely crash the phone.
Lazy Loading Explained
The solution is "Lazy Loading." We only want to build the widgets that are currently visible on the screen.
If your phone screen fits 8 items, we should only build 8 items (plus maybe 1 or 2 extra as a buffer). As you scroll down, Flutter destroys the ones that go off the top and builds the new ones coming in from the bottom.
This keeps memory usage low and performance high, whether you have 50 items or 5 million items.
Using `itemBuilder`
To do this, we use the constructor ListView.builder. It takes two main arguments:
- `itemCount`: How many items are in the list total?
- `itemBuilder`: A function that runs for every visible item. It gives you the
index(0, 1, 2...) and asks you to return a Widget for that specific position.
// Let's imagine we have a list of data
final List<String> myPets = ['Dog', 'Cat', 'Fish', 'Bird', 'Hamster'];
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
// 1. Tell Flutter how many items there are
itemCount: myPets.length,
// 2. Build the widget for index 'i'
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: CircleAvatar(
child: Text((index + 1).toString()), // Show 1, 2, 3...
),
title: Text(myPets[index]), // Get data from our list
);
},
),
);
}
This code is incredibly efficient. itemBuilder is a loop that only runs when it needs to.
4. Adding Separators: `ListView.separated`
Often in UI design, you want a thin grey line between list items to separate them.
Instead of manually adding a `Divider()` widget inside your builder logic, use ListView.separated. It works exactly like `.builder`, but has one extra argument: separatorBuilder.
ListView.separated(
itemCount: 50,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
// This runs BETWEEN every item
separatorBuilder: (context, index) {
return Divider(color: Colors.grey);
},
)
5. Grid Layouts: `GridView`
Sometimes you don't want a list; you want a grid (like an Instagram profile or a Photo Gallery). For this, we use GridView.
Like `ListView`, `GridView` has scroll capability built-in.
`GridView.count` (Fixed Columns)
Use this when you know exactly how many columns you want (e.g., 2 columns).
GridView.count(
crossAxisCount: 2, // 2 Columns wide
crossAxisSpacing: 10, // Space between columns
mainAxisSpacing: 10, // Space between rows
padding: EdgeInsets.all(10),
children: [
Container(color: Colors.red),
Container(color: Colors.blue),
Container(color: Colors.green),
Container(color: Colors.yellow),
],
)
`GridView.builder` (Dynamic Grid)
Just like `ListView.builder`, if you have hundreds of photos, use `GridView.builder`. It requires a special `gridDelegate` object.
GridView.builder(
itemCount: 100,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 3 items per row
),
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.all(2),
color: Colors.blue[100 * (index % 9)], // Different shades of blue
child: Center(child: Text('$index')),
);
},
)
6. Mini-Project: Building a "Contacts List"
Let's combine everything. We will create a contact list using a `List` of Maps (data), `ListView.builder` (efficiency), and `ListTile` (design).
Create a new file or clear your `main.dart` and use this:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ContactListScreen()));
}
class ContactListScreen extends StatelessWidget {
// 1. Our Dummy Data
final List<Map<String, String>> contacts = [
{'name': 'Alice Johnson', 'email': 'alice@example.com'},
{'name': 'Bob Smith', 'email': 'bob@example.com'},
{'name': 'Charlie Brown', 'email': 'charlie@gmail.com'},
{'name': 'David Wilson', 'email': 'david@yahoo.com'},
{'name': 'Eva Green', 'email': 'eva@example.com'},
{'name': 'Frank White', 'email': 'frank@test.com'},
{'name': 'Grace Lee', 'email': 'grace@demo.com'},
{'name': 'Henry Ford', 'email': 'henry@cars.com'},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('My Contacts')),
body: ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
// 2. Get the specific contact for this row
final contact = contacts[index];
// 3. Return the UI
return ListTile(
// Create a circle with the first letter of their name
leading: CircleAvatar(
backgroundColor: Colors.blue,
child: Text(
contact['name']![0], // First letter
style: TextStyle(color: Colors.white),
),
),
title: Text(contact['name']!),
subtitle: Text(contact['email']!),
trailing: Icon(Icons.phone, color: Colors.green),
onTap: () {
print("Calling ${contact['name']}");
},
);
},
),
);
}
}
What just happened?
- We defined a
Listof data. In a real app, this would come from a database or API. - We used
ListView.builderso it's scalable. - Inside the builder, we accessed
contacts[index]to get the specific data for *that* row. - We used
ListTileto automatically format the name, email, and avatar. - We added an
onTapto handle interaction.
Conclusion
You have mastered the scroll. You can now handle 10 items or 10,000 items without breaking a sweat.
You've learned:
- How
ListViewsolves the scrolling problem ofColumn. - How
ListTilecreates beautiful rows instantly. - The critical importance of Lazy Loading with
ListView.builder. - How to create grids with
GridView.
In the next chapter, we will put your skills to the test. We are going to build Project 2: A "Simple To-Do List" UI. This will combine everything from layout to lists to buttons into a single, functional interface.
See you there!

Comments
Post a Comment