On this page21 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. ListView — A Column That Scrolls
- 2. ListTile — The Standard Row
- 3. Card and Divider
- 4. ListView.builder — Rows From Data
- 5. A List of Your Own Objects
- 6. ListView.separated
- 7. Horizontal Lists
- 8. A List Inside a Column
- 9. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 007 — ListView
| Category | 4 — Scrolling Widgets |
| Lecture | 007 of 009 |
| Estimated video length | 18 min |
| Target | Flutter 3.44 · Dart 3.12 |
| Level | Beginner |
A
Columnstops at the bottom of the screen — anything more overflows.ListViewscrolls, so a screen can hold ten items or ten thousand, andListView.builderturns a DartListinto rows automatically.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every screenshot and error message in this lecture is real. Each app was run on Flutter 3.44, and each error was produced by the code shown. Screenshots of scrolled lists were taken after dragging the list up.
Prerequisites
Students should already understand, from Lectures 001–006:
Row,Column,Expandedand the overflow stripesContainer,Card-like decorated boxes,IconandCircleAvatar- from the Dart course:
List,Map, index access (list[0]),.length,forloops, simple classes with a constructor, and the conditional operator (a ? b : c)
Connection With Previous Lectures
Put twelve 80-pixel boxes in a Column:
body: Column(
children: [
Container(
height: 80,
color: Colors.teal.shade100,
alignment: Alignment.center,
child: const Text('Box 1'),
),
Container(
height: 80,
color: Colors.teal.shade200,
alignment: Alignment.center,
child: const Text('Box 2'),
),
Container(
height: 80,
color: Colors.teal.shade300,
alignment: Alignment.center,
child: const Text('Box 3'),
),
Container(
height: 80,
color: Colors.teal.shade400,
alignment: Alignment.center,
child: const Text('Box 4'),
),
Container(
height: 80,
color: Colors.teal.shade100,
alignment: Alignment.center,
child: const Text('Box 5'),
),
Container(
height: 80,
color: Colors.teal.shade200,
alignment: Alignment.center,
child: const Text('Box 6'),
),
Container(
height: 80,
color: Colors.teal.shade300,
alignment: Alignment.center,
child: const Text('Box 7'),
),
Container(
height: 80,
color: Colors.teal.shade400,
alignment: Alignment.center,
child: const Text('Box 8'),
),
Container(
height: 80,
color: Colors.teal.shade100,
alignment: Alignment.center,
child: const Text('Box 9'),
),
Container(
height: 80,
color: Colors.teal.shade200,
alignment: Alignment.center,
child: const Text('Box 10'),
),
Container(
height: 80,
color: Colors.teal.shade300,
alignment: Alignment.center,
child: const Text('Box 11'),
),
Container(
height: 80,
color: Colors.teal.shade400,
alignment: Alignment.center,
child: const Text('Box 12'),
),
],
),
══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 276 pixels on the bottom.Twelve boxes need 960 pixels; the screen has about 680. A Column has no way to show the rest.
Contacts, messages, transactions and products are all lists that are longer than one screen, so
we need a widget that scrolls.
Learning Objectives
By the end of this lecture you will be able to:
- Make content scroll with
ListView - Build standard rows with
ListTile(leading,title,subtitle,trailing,onTap) - Group rows with
CardandDivider - Build a list from Dart data with
ListView.builder(itemCount,itemBuilder) - Show a list of your own class objects
- Add dividers between rows with
ListView.separated - Make a horizontal list with
scrollDirection - Fix the "unbounded height" error when a list sits inside a
Column
1. ListView — A Column That Scrolls
Change Column to ListView and nothing else:
body: ListView(
children: [
Container(
height: 80,
color: Colors.teal.shade100,
alignment: Alignment.center,
child: const Text('Box 1'),
),
Container(
height: 80,
color: Colors.teal.shade200,
alignment: Alignment.center,
child: const Text('Box 2'),
),
Container(
height: 80,
color: Colors.teal.shade300,
alignment: Alignment.center,
child: const Text('Box 3'),
),
Container(
height: 80,
color: Colors.teal.shade400,
alignment: Alignment.center,
child: const Text('Box 4'),
),
Container(
height: 80,
color: Colors.teal.shade100,
alignment: Alignment.center,
child: const Text('Box 5'),
),
Container(
height: 80,
color: Colors.teal.shade200,
alignment: Alignment.center,
child: const Text('Box 6'),
),
Container(
height: 80,
color: Colors.teal.shade300,
alignment: Alignment.center,
child: const Text('Box 7'),
),
Container(
height: 80,
color: Colors.teal.shade400,
alignment: Alignment.center,
child: const Text('Box 8'),
),
Container(
height: 80,
color: Colors.teal.shade100,
alignment: Alignment.center,
child: const Text('Box 9'),
),
Container(
height: 80,
color: Colors.teal.shade200,
alignment: Alignment.center,
child: const Text('Box 10'),
),
Container(
height: 80,
color: Colors.teal.shade300,
alignment: Alignment.center,
child: const Text('Box 11'),
),
Container(
height: 80,
color: Colors.teal.shade400,
alignment: Alignment.center,
child: const Text('Box 12'),
),
],
),
Now the user drags to see every box. While content scrolls under it, the AppBar changes tint
slightly — a Material Design detail you get for free.
Analogy — a
Columnis a printed page: what does not fit is cut off. AListViewis a scroll of paper: the window shows one part, and you roll it to see the rest.
A ListView fills the whole space it is given in its scrolling direction, and its children
stretch across the other direction — the boxes need no width.
2. ListTile — The Standard Row
Almost every list row has the same parts: a picture or icon on the left, one or two lines of
text, and something on the right. ListTile builds exactly that, with the right sizes and
spacing:
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.person),
title: const Text('Ali Raza'),
subtitle: const Text('Software engineer'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
leading: const CircleAvatar(child: Text('S')),
title: const Text('Sara Khan'),
subtitle: const Text('Designer'),
trailing: const Text('2 min ago'),
onTap: () {},
),
const ListTile(
leading: Icon(Icons.wifi),
title: Text('Wi-Fi'),
trailing: Icon(Icons.toggle_on, color: Colors.green, size: 40),
),
],
),
| Parameter | Where | Usually |
|---|---|---|
leading | left | Icon or CircleAvatar |
title | first line | Text |
subtitle | second line, smaller | Text |
trailing | right | Icon, Text or a small button |
onTap | the whole row | a function; adds the tap ripple |
3. Card and Divider
Card puts its child on a slightly raised, rounded surface. Divider draws a thin horizontal
line; its height is the total space it takes, with the line in the middle:
body: ListView(
padding: const EdgeInsets.all(12),
children: const [
Card(
child: ListTile(
leading: Icon(Icons.book),
title: Text('Dart Complete Course'),
subtitle: Text('140 lectures'),
),
),
Card(
child: ListTile(
leading: Icon(Icons.widgets),
title: Text('Flutter Essential Widgets'),
subtitle: Text('9 lectures'),
),
),
Divider(height: 32),
ListTile(title: Text('Settings')),
Divider(),
ListTile(title: Text('Help')),
],
),
padding on the ListView adds space around all its children — and it scrolls with them.
4. ListView.builder — Rows From Data
Real lists come from data. Store the data in a Dart List:
List<String> names = ['Ali', 'Sara', 'Zayan', 'Hina', 'Omar', 'Ayesha'];and let ListView.builder create one row per item:
body: ListView.builder(
itemCount: names.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(names[index]),
);
},
),
itemCount— how many rows there are.itemBuilder— a function Flutter calls once for each row it needs, with that row'sindex(0, 1, 2, …). It returns the widget for that row.
ListView.builder only builds the rows that are on screen (plus a few around them). A list of
10,000 contacts costs almost nothing until the user scrolls — always use .builder for long or
data-driven lists.
5. A List of Your Own Objects
Most lists show objects with several fields. A small class from the Dart course does the job:
class Student {
final String name;
final int marks;
const Student(this.name, this.marks);
}List<Student> students = const [
Student('Ali', 240),
Student('Sara', 275),
Student('Zayan', 198),
Student('Hina', 150),
Student('Omar', 262),
];Inside itemBuilder, take the object for this index and build the row from its fields:
body: ListView.builder(
itemCount: students.length,
itemBuilder: (context, index) {
Student student = students[index];
bool passed = student.marks >= 165;
return ListTile(
leading: CircleAvatar(child: Text(student.name[0])),
title: Text(student.name),
subtitle: Text(passed ? 'Passed' : 'Failed'),
trailing: Text(
'${student.marks} / 300',
style: TextStyle(
color: passed ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
);
},
),
The conditional operator chooses the subtitle and the color for each row: 165 out of 300 is
the pass mark (55 %).
6. ListView.separated
ListView.separated is .builder with one extra function, separatorBuilder, that builds the
widget between two rows — never before the first or after the last:
body: ListView.separated(
itemCount: cities.length,
itemBuilder: (context, index) => ListTile(title: Text(cities[index])),
separatorBuilder: (context, index) => const Divider(height: 1),
),
The arrow syntax (context, index) => ... from the Dart course keeps short builders on one line.
7. Horizontal Lists
scrollDirection: Axis.horizontal makes a list scroll sideways. A horizontal list needs a
height — wrap it in a SizedBox:
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 56,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(8),
itemCount: categories.length,
separatorBuilder: (context, index) => const SizedBox(width: 8),
itemBuilder: (context, index) => Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
decoration: BoxDecoration(
color: index == 0 ? Colors.indigo : Colors.indigo.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Text(
categories[index],
style: TextStyle(
color: index == 0 ? Colors.white : Colors.indigo,
),
),
),
),
),
const Padding(
padding: EdgeInsets.all(16),
child: Text('Swipe the row sideways to see every category.'),
),
],
),
The last chip is cut off at the edge — a hint to the user that the row scrolls. index == 0
highlights the first chip.
8. A List Inside a Column
A heading above a list is common. The obvious code shows a blank screen:
body: Column(
children: [
const Text('Students'),
ListView.builder(
itemCount: names.length,
itemBuilder: (context, index) =>
ListTile(title: Text(names[index])),
),
],
),
══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════
The following assertion was thrown during performResize():
Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their
container. In this case, a vertical viewport was given an
unlimited amount of vertical space in which to expand. ThisA Column lets each child be as tall as it likes, and a ListView wants to be as tall as
possible — so it asks for infinite height and fails. Give the list a limit: wrap it in
Expanded, which hands it the space left under the heading.
body: Column(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text('Students', style: TextStyle(fontSize: 20)),
),
Expanded(
child: ListView.builder(
itemCount: names.length,
itemBuilder: (context, index) =>
ListTile(title: Text(names[index])),
),
),
],
),
9. A Real-World Example
Zayan's transaction history: a balance card that stays at the top, and a scrolling list of
payments below it. The balance is calculated from the list with a for loop before build
returns.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const TransactionsScreen(),
);
}
}
class TransactionsScreen extends StatelessWidget {
const TransactionsScreen({super.key});
@override
Widget build(BuildContext context) {
List<Payment> payments = const [
Payment('Salary', 40000, true),
Payment('Grocery store', 6500, false),
Payment('Electricity bill', 4200, false),
Payment('Transfer from Sara', 3000, true),
Payment('Mobile top-up', 500, false),
Payment('Restaurant', 2800, false),
Payment('Book shop', 1250, false),
Payment('Refund', 900, true),
];
int balance = 0;
for (Payment payment in payments) {
balance += payment.isCredit ? payment.amount : -payment.amount;
}
return Scaffold(
appBar: AppBar(title: const Text('Transactions')),
body: Column(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.indigo,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Balance\nRs. $balance',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: ListView.separated(
itemCount: payments.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
Payment payment = payments[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: payment.isCredit
? Colors.green.shade100
: Colors.red.shade100,
child: Icon(
payment.isCredit
? Icons.arrow_downward
: Icons.arrow_upward,
color: payment.isCredit ? Colors.green : Colors.red,
),
),
title: Text(payment.title),
trailing: Text(
'${payment.isCredit ? '+' : '-'} Rs. ${payment.amount}',
style: TextStyle(
color: payment.isCredit ? Colors.green : Colors.red,
fontWeight: FontWeight.w600,
),
),
);
},
),
),
],
),
);
}
}
class Payment {
final String title;
final int amount;
final bool isCredit;
const Payment(this.title, this.amount, this.isCredit);
}
- The
Paymentclass holds a title, an amount and whether it is money in (isCredit). Expandedgives theListView.separatedthe space under the card.- Every color, icon and sign in a row is chosen with
payment.isCredit ? … : …. '${payment.isCredit ? '+' : '-'} Rs. ${payment.amount}'— a whole expression inside${}.
Code Examples
Example 1 — Fifty Rows
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Example 1')),
body: ListView.builder(
itemCount: 50,
itemBuilder: (context, index) =>
ListTile(title: Text('Item ${index + 1}')),
),
);
}
}
Example 2 — A Settings Menu
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const SettingsScreen(),
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
children: [
ListTile(
leading: const Icon(Icons.person_outline),
title: const Text('Account'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.notifications_outlined),
title: const Text('Notifications'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.lock_outline),
title: const Text('Privacy'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
const Divider(),
ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text('Log out', style: TextStyle(color: Colors.red)),
onTap: () {},
),
],
),
);
}
}
Example 3 — Shopping List With a Total
The list scrolls; the total bar stays put because it is outside the ListView.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.green),
home: const ShoppingScreen(),
);
}
}
class ShoppingScreen extends StatelessWidget {
const ShoppingScreen({super.key});
@override
Widget build(BuildContext context) {
List<Product> products = const [
Product('Notebook', 120, 3),
Product('Pen', 30, 5),
Product('Geometry box', 450, 1),
Product('School bag', 2500, 1),
];
int total = 0;
for (Product product in products) {
total += product.price * product.quantity;
}
return Scaffold(
appBar: AppBar(title: const Text('Shopping List')),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
Product product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.quantity} × Rs. ${product.price}'),
trailing: Text('Rs. ${product.price * product.quantity}'),
);
},
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
color: Colors.green.shade100,
child: Text(
'Total: Rs. $total',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
],
),
);
}
}
class Product {
final String name;
final int price;
final int quantity;
const Product(this.name, this.price, this.quantity);
}
Example 4 — Weather by City
Two parallel lists, read with the same index. The icon depends on the temperature.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.lightBlue),
home: const WeatherScreen(),
);
}
}
class WeatherScreen extends StatelessWidget {
const WeatherScreen({super.key});
@override
Widget build(BuildContext context) {
List<String> cities = ['Lahore', 'Karachi', 'Islamabad', 'Murree'];
List<int> temperatures = [30, 34, 26, 12];
return Scaffold(
appBar: AppBar(title: const Text('Weather')),
body: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: cities.length,
itemBuilder: (context, index) {
int celsius = temperatures[index];
return Card(
child: ListTile(
leading: Icon(
celsius >= 25 ? Icons.wb_sunny : Icons.ac_unit,
color: celsius >= 25 ? Colors.orange : Colors.lightBlue,
),
title: Text(cities[index]),
trailing: Text(
'$celsius°C',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
);
}
}
Example 5 — A Horizontal List Inside a Vertical One
A whole screen can be one ListView. The for inside the list — collection for from the
Dart course — adds eight ListTiles.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const ShopScreen(),
);
}
}
class ShopScreen extends StatelessWidget {
const ShopScreen({super.key});
@override
Widget build(BuildContext context) {
List<String> categories = [
'Phones',
'Laptops',
'Cameras',
'Audio',
'Games',
];
return Scaffold(
appBar: AppBar(title: const Text('Shop')),
body: ListView(
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Categories',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: categories.length,
itemBuilder: (context, index) => Container(
width: 100,
margin: const EdgeInsets.symmetric(horizontal: 4),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Text(categories[index]),
),
),
),
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Popular',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
for (int i = 1; i <= 8; i++)
ListTile(
leading: const Icon(Icons.local_offer),
title: Text('Deal number $i'),
),
],
),
);
}
}
Example 6 — Custom Rows
itemBuilder can return any widget, not only a ListTile.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.blue),
home: const StudentsScreen(),
);
}
}
class StudentsScreen extends StatelessWidget {
const StudentsScreen({super.key});
@override
Widget build(BuildContext context) {
List<Student> students = const [
Student('Ali', 240),
Student('Sara', 275),
Student('Zayan', 198),
];
return Scaffold(
appBar: AppBar(title: const Text('Class 8-B')),
body: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: students.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
Student student = students[index];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: Text(
student.name,
style: const TextStyle(fontSize: 18),
),
),
Text('${(student.marks / 300 * 100).round()}%'),
],
),
);
},
),
);
}
}
class Student {
final String name;
final int marks;
const Student(this.name, this.marks);
}
Common Beginner Mistakes
1. A ListView directly in a Column
Section 8: a blank screen and "Vertical viewport was given unbounded height". Wrap the list in
Expanded.
2. A horizontal list with no height
The same problem sideways: a horizontal ListView in a Column must be wrapped in a SizedBox
with a height.
3. The wrong itemBuilder parameters
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: 3,
itemBuilder: (index) => Text('Item $index'),
),
),
),
);
}error • The argument type 'Text Function(BuildContext)' can't be assigned to the parameter type 'NullableIndexedWidgetBuilder'. • lib/main.dart:9:24 • argument_type_not_assignableitemBuilder always receives two values: (context, index).
4. Strings where ListTile expects widgets
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: ListTile(title: 'Ali', subtitle: 'Student'),
),
),
);
}error • A value of type 'String' can't be assigned to a parameter of type 'Widget?' in a const constructor. Try using a subtype, or removing the keyword 'const' • lib/main.dart:7:24 • const_constructor_param_type_mismatch
error • The argument type 'String' can't be assigned to the parameter type 'Widget?'. • lib/main.dart:7:31 • argument_type_not_assignable
error • The argument type 'String' can't be assigned to the parameter type 'Widget?'. • lib/main.dart:7:48 • argument_type_not_assignabletitle, subtitle, leading and trailing are widget slots: title: Text('Ali').
5. Building a long list with ListView(children: …)
It works, but every row is built at once, even the thousands that are off screen. Use
ListView.builder whenever the rows come from data.
Best Practices
- Use
ListView.builder(or.separated) for any list built from data. - Keep the data in a
Listand the look initemBuilder— change one without touching the other. - Use
ListTilefor standard rows; build a custom row only when the design needs it. - Put headers, totals and buttons that must stay visible outside the
ListView, with the list inExpanded. - Give
ListViewits padding through its ownpaddingparameter, so the padding scrolls with the content.
Mini Challenge
Build a class leaderboard. Store five Student objects, already sorted by marks: Sara 275,
Omar 262, Ali 240, Zayan 198, Hina 150. Show them with ListView.separated: a CircleAvatar
with the rank (1–5) on the left — amber for first place, light grey for the others — the
name, and the marks in bold on the right.
Try it yourself before opening the solution.
Show solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.amber),
home: const LeaderboardScreen(),
);
}
}
class LeaderboardScreen extends StatelessWidget {
const LeaderboardScreen({super.key});
@override
Widget build(BuildContext context) {
List<Student> students = const [
Student('Sara', 275),
Student('Omar', 262),
Student('Ali', 240),
Student('Zayan', 198),
Student('Hina', 150),
];
return Scaffold(
appBar: AppBar(title: const Text('Leaderboard')),
body: ListView.separated(
itemCount: students.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
Student student = students[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: index == 0 ? Colors.amber : Colors.grey.shade300,
child: Text('${index + 1}'),
),
title: Text(student.name),
trailing: Text(
'${student.marks}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
);
},
),
);
}
}
class Student {
final String name;
final int marks;
const Student(this.name, this.marks);
}
How it works
- The rank is
index + 1, becauseindexstarts at 0. index == 0 ? Colors.amber : Colors.grey.shade300colors only the winner.separatorBuilderadds a 1-pixelDividerbetween rows.
Practice Questions
Beginner
- Show
Apple,Banana,Mango,OrangeandGrapesin aListViewofTexts. - Show one
ListTilewithIcons.email, the titleInboxand the subtitle3 new messages. - Show 30 rows,
Item 1toItem 30, withListView.builder.
Application
- Show
Lahore,Karachi,IslamabadandMultanfrom a list, with a city icon and dividers between rows. - Show three courses in
Cards with a title and a subtitle each. - Make a horizontal list of five 100 × 100 colored boxes (red, orange, yellow, green, blue)
from a
List<Color>. - Store
{'Lahore': 30, 'Karachi': 34, 'Quetta': 18}in aMapand list each city with its temperature on the right.
Challenge
- Zayan made four deposits:
5000,1500,2500and1000. Show a summary card at the top (Zayan — total deposits: Rs. 10000, calculated with a loop) and a scrolling list of the deposits below it.
Practice Answers
Question 1 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Question 1')),
body: ListView(
children: const [
Text('Apple'),
Text('Banana'),
Text('Mango'),
Text('Orange'),
Text('Grapes'),
],
),
);
}
}
Question 2 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Question 2')),
body: ListView(
children: const [
ListTile(
leading: Icon(Icons.email),
title: Text('Inbox'),
subtitle: Text('3 new messages'),
),
],
),
);
}
}
Question 3 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Question 3')),
body: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) =>
ListTile(title: Text('Item ${index + 1}')),
),
);
}
}
Question 4 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const CitiesScreen(),
);
}
}
class CitiesScreen extends StatelessWidget {
const CitiesScreen({super.key});
@override
Widget build(BuildContext context) {
List<String> cities = ['Lahore', 'Karachi', 'Islamabad', 'Multan'];
return Scaffold(
appBar: AppBar(title: const Text('Question 4')),
body: ListView.separated(
itemCount: cities.length,
itemBuilder: (context, index) => ListTile(
leading: const Icon(Icons.location_city),
title: Text(cities[index]),
),
separatorBuilder: (context, index) => const Divider(),
),
);
}
}
Question 5 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const CoursesScreen(),
);
}
}
class CoursesScreen extends StatelessWidget {
const CoursesScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Question 5')),
body: ListView(
padding: const EdgeInsets.all(8),
children: const [
Card(
child: ListTile(
title: Text('Dart'),
subtitle: Text('140 lectures'),
),
),
Card(
child: ListTile(
title: Text('Flutter Widgets'),
subtitle: Text('9 lectures'),
),
),
Card(
child: ListTile(
title: Text('Node.js'),
subtitle: Text('Coming soon'),
),
),
],
),
);
}
}
Question 6 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
List<Color> colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
];
return Scaffold(
appBar: AppBar(title: const Text('Question 6')),
body: SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: colors.length,
itemBuilder: (context, index) =>
Container(width: 100, color: colors[index]),
),
),
);
}
}
Question 7 — Solution
temperatures.keys.toList() turns the map's keys into a List, so itemBuilder can read them
by index.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const TemperatureScreen(),
);
}
}
class TemperatureScreen extends StatelessWidget {
const TemperatureScreen({super.key});
@override
Widget build(BuildContext context) {
Map<String, int> temperatures = {'Lahore': 30, 'Karachi': 34, 'Quetta': 18};
List<String> cities = temperatures.keys.toList();
return Scaffold(
appBar: AppBar(title: const Text('Question 7')),
body: ListView.builder(
itemCount: cities.length,
itemBuilder: (context, index) {
String city = cities[index];
return ListTile(
title: Text(city),
trailing: Text('${temperatures[city]}°C'),
);
},
),
);
}
}
Question 8 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const AccountScreen(),
);
}
}
class AccountScreen extends StatelessWidget {
const AccountScreen({super.key});
@override
Widget build(BuildContext context) {
String owner = 'Zayan';
List<int> deposits = [5000, 1500, 2500, 1000];
int total = 0;
for (int deposit in deposits) {
total += deposit;
}
return Scaffold(
appBar: AppBar(title: const Text('Question 8')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'$owner — total deposits: Rs. $total',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
Expanded(
child: ListView.builder(
itemCount: deposits.length,
itemBuilder: (context, index) => ListTile(
leading: const Icon(Icons.savings),
title: Text('Deposit ${index + 1}'),
trailing: Text('Rs. ${deposits[index]}'),
),
),
),
],
),
);
}
}
Quick Revision
- Why can a
Columnnot show 20 tall boxes? - Name the four content slots of a
ListTile. - What two parameters does
ListView.builderneed? - What does
itemBuilderreceive, and what must it return? - Why is
.builderbetter for long lists? - What does
separatorBuilderbuild? - What does a horizontal
ListViewneed from its parent? - How do you fix "Vertical viewport was given unbounded height" for a list in a
Column?
Key Takeaways
ListViewis a scrolling list of children; it fills the space it is given.ListTilebuilds standard rows:leading,title,subtitle,trailing,onTap.ListView.builderturns data into rows lazily withitemCountanditemBuilder.ListView.separatedadds a widget between rows.scrollDirection: Axis.horizontalscrolls sideways — give it a height.- A
ListViewinside aColumnneedsExpanded(or another height limit).
What's Next?
Lecture 008 — GridView & Scroll Helpers. Photo galleries and product catalogs show items in
a grid, not a single column. Next we build grids with GridView, and use
SingleChildScrollView to make any screen — like a long form — scroll.