On this page21 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. ListView — A Column That Scrolls
  5. 2. ListTile — The Standard Row
  6. 3. Card and Divider
  7. 4. ListView.builder — Rows From Data
  8. 5. A List of Your Own Objects
  9. 6. ListView.separated
  10. 7. Horizontal Lists
  11. 8. A List Inside a Column
  12. 9. A Real-World Example
  13. Code Examples
  14. Common Beginner Mistakes
  15. Best Practices
  16. Mini Challenge
  17. Practice Questions
  18. Practice Answers
  19. Quick Revision
  20. Key Takeaways
  21. What's Next?

Lecture 007 — ListView

Category4 — Scrolling Widgets
Lecture007 of 009
Estimated video length18 min
TargetFlutter 3.44 · Dart 3.12
LevelBeginner

A Column stops at the bottom of the screen — anything more overflows. ListView scrolls, so a screen can hold ten items or ten thousand, and ListView.builder turns a Dart List into 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, Expanded and the overflow stripes
  • Container, Card-like decorated boxes, Icon and CircleAvatar
  • from the Dart course: List, Map, index access (list[0]), .length, for loops, simple classes with a constructor, and the conditional operator (a ? b : c)

Connection With Previous Lectures

Put twelve 80-pixel boxes in a Column:

dart
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'),
    ),
  ],
),
Eight teal boxes and yellow-black overflow stripes at the bottom of the screen
A Column cannot scroll
output
══╡ 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 Card and Divider
  • 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:

dart
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'),
    ),
  ],
),
Teal boxes 4 to 12 visible after scrolling up, the title bar slightly tinted
The same boxes in a ListView, scrolled up

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 Column is a printed page: what does not fit is cut off. A ListView is 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:

dart
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),
    ),
  ],
),
Three rows: a person icon with Ali Raza and a chevron, an S avatar with Sara Khan and 2 min ago, and Wi-Fi with a green toggle icon
ListTile
ParameterWhereUsually
leadingleftIcon or CircleAvatar
titlefirst lineText
subtitlesecond line, smallerText
trailingrightIcon, Text or a small button
onTapthe whole rowa 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:

dart
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')),
  ],
),
Two cards with course names, then plain Settings and Help rows separated by lines
Card and Divider

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:

dart
List<String> names = ['Ali', 'Sara', 'Zayan', 'Hina', 'Omar', 'Ayesha'];

and let ListView.builder create one row per item:

dart
body: ListView.builder(
  itemCount: names.length,
  itemBuilder: (context, index) {
    return ListTile(
      leading: CircleAvatar(child: Text('${index + 1}')),
      title: Text(names[index]),
    );
  },
),
Six rows numbered 1 to 6 with Ali, Sara, Zayan, Hina, Omar and Ayesha
ListView.builder
  • itemCount — how many rows there are.
  • itemBuilder — a function Flutter calls once for each row it needs, with that row's index (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:

dart
class Student {
  final String name;
  final int marks;

  const Student(this.name, this.marks);
}
dart
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:

dart
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,
        ),
      ),
    );
  },
),
Five students with initials, Passed or Failed, and marks in green or red
Students from a List of objects

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:

dart
body: ListView.separated(
  itemCount: cities.length,
  itemBuilder: (context, index) => ListTile(title: Text(cities[index])),
  separatorBuilder: (context, index) => const Divider(height: 1),
),
Five city names separated by thin lines
ListView.separated

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:

dart
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.'),
    ),
  ],
),
A row of rounded category chips — All highlighted in indigo — cut off at the right edge
A horizontal ListView of category chips

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:

dart
body: Column(
  children: [
    const Text('Students'),
    ListView.builder(
      itemCount: names.length,
      itemBuilder: (context, index) =>
          ListTile(title: Text(names[index])),
    ),
  ],
),
An empty screen below the title bar
Nothing is drawn
output
══╡ 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. This

A 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.

dart
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])),
      ),
    ),
  ],
),
Students heading and three names below it
Expanded gives the ListView a height

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.

dart
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);
}
An indigo Balance Rs. 28650 card above a list of payments with green down-arrows for money in and red up-arrows for money out
Transaction history
  • The Payment class holds a title, an amount and whether it is money in (isCredit).
  • Expanded gives the ListView.separated the 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

dart
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}')),
      ),
    );
  }
}
Items 8 to 19 visible after scrolling
Example 1, scrolled

Example 2 — A Settings Menu

dart
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: () {},
          ),
        ],
      ),
    );
  }
}
Account, Notifications and Privacy rows with chevrons, a line, and a red Log out row
Example 2

Example 3 — Shopping List With a Total

The list scrolls; the total bar stays put because it is outside the ListView.

dart
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);
}
Four products with quantity and price, and a green Total: Rs. 3460 bar at the bottom
Example 3

Example 4 — Weather by City

Two parallel lists, read with the same index. The icon depends on the temperature.

dart
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,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}
Four cards with sun or snowflake icons and temperatures for Lahore, Karachi, Islamabad and Murree
Example 4

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.

dart
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'),
            ),
        ],
      ),
    );
  }
}
A Categories heading with square category tiles in a row, then a Popular heading and deal rows
Example 5

Example 6 — Custom Rows

itemBuilder can return any widget, not only a ListTile.

dart
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);
}
Three light-blue rounded rows with Ali 80%, Sara 92% and Zayan 66%
Example 6

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

dart
import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: ListView.builder(
          itemCount: 3,
          itemBuilder: (index) => Text('Item $index'),
        ),
      ),
    ),
  );
}
output
error • The argument type 'Text Function(BuildContext)' can't be assigned to the parameter type 'NullableIndexedWidgetBuilder'.  • lib/main.dart:9:24 • argument_type_not_assignable

itemBuilder always receives two values: (context, index).

4. Strings where ListTile expects widgets

dart
import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: ListTile(title: 'Ali', subtitle: 'Student'),
      ),
    ),
  );
}
output
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_assignable

title, 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 List and the look in itemBuilder — change one without touching the other.
  • Use ListTile for 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 in Expanded.
  • Give ListView its padding through its own padding parameter, 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
dart
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);
}
Five rows with rank circles, the first one amber, names and bold marks
Mini challenge solution

How it works

  1. The rank is index + 1, because index starts at 0.
  2. index == 0 ? Colors.amber : Colors.grey.shade300 colors only the winner.
  3. separatorBuilder adds a 1-pixel Divider between rows.

Practice Questions

Beginner

  1. Show Apple, Banana, Mango, Orange and Grapes in a ListView of Texts.
  2. Show one ListTile with Icons.email, the title Inbox and the subtitle 3 new messages.
  3. Show 30 rows, Item 1 to Item 30, with ListView.builder.

Application

  1. Show Lahore, Karachi, Islamabad and Multan from a list, with a city icon and dividers between rows.
  2. Show three courses in Cards with a title and a subtitle each.
  3. Make a horizontal list of five 100 × 100 colored boxes (red, orange, yellow, green, blue) from a List<Color>.
  4. Store {'Lahore': 30, 'Karachi': 34, 'Quetta': 18} in a Map and list each city with its temperature on the right.

Challenge

  1. Zayan made four deposits: 5000, 1500, 2500 and 1000. 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

dart
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'),
        ],
      ),
    );
  }
}
Five fruit names at the top-left
Question 1

Question 2 — Solution

dart
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'),
          ),
        ],
      ),
    );
  }
}
An email icon with Inbox and 3 new messages
Question 2

Question 3 — Solution

dart
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}')),
      ),
    );
  }
}
Items 1 to 12 visible
Question 3

Question 4 — Solution

dart
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(),
      ),
    );
  }
}
Four cities with building icons and lines between them
Question 4

Question 5 — Solution

dart
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'),
            ),
          ),
        ],
      ),
    );
  }
}
Three cards: Dart, Flutter Widgets and Node.js
Question 5

Question 6 — Solution

dart
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]),
        ),
      ),
    );
  }
}
Red, orange, yellow and green squares in a row, blue off screen
Question 6

Question 7 — Solution

temperatures.keys.toList() turns the map's keys into a List, so itemBuilder can read them by index.

dart
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'),
          );
        },
      ),
    );
  }
}
Lahore 30°C, Karachi 34°C and Quetta 18°C
Question 7

Question 8 — Solution

dart
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]}'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
A summary card with total deposits Rs. 10000 above four deposit rows
Question 8

Quick Revision

  1. Why can a Column not show 20 tall boxes?
  2. Name the four content slots of a ListTile.
  3. What two parameters does ListView.builder need?
  4. What does itemBuilder receive, and what must it return?
  5. Why is .builder better for long lists?
  6. What does separatorBuilder build?
  7. What does a horizontal ListView need from its parent?
  8. How do you fix "Vertical viewport was given unbounded height" for a list in a Column?

Key Takeaways

  • ListView is a scrolling list of children; it fills the space it is given.
  • ListTile builds standard rows: leading, title, subtitle, trailing, onTap.
  • ListView.builder turns data into rows lazily with itemCount and itemBuilder.
  • ListView.separated adds a widget between rows.
  • scrollDirection: Axis.horizontal scrolls sideways — give it a height.
  • A ListView inside a Column needs Expanded (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.