Lesson 24 of 51 intermediate

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

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. 1. Import the Material package
  2. 2.
  3. 3. Define ContactListScreen as a StatelessWidget
  4. 4.
  5. 5.
  6. 6. Build method constructs the UI
  7. 7. Generate a list of 50 fake contacts with name, email, and initial
  8. 8. Each contact is a Map with string keys
  9. 9. Name is Contact 1 through Contact 50
  10. 10. Email follows a pattern
  11. 11. Initial cycles through A-Z using character codes
  12. 12.
  13. 13.
  14. 14. Return a Scaffold with an AppBar
  15. 15. AppBar shows 'Contacts' title
  16. 16. Body uses ListView.separated for dividers between items
  17. 17. itemCount is the total number of contacts
  18. 18. separatorBuilder creates a thin divider indented 72px to align with text
  19. 19.
  20. 20.
  21. 21. itemBuilder creates each contact row
  22. 22. Get the contact map for this index
  23. 23. Return a ListTile for the standard list item layout
  24. 24. Leading CircleAvatar with a color cycling through Material primary colors
  25. 25. Display the contact's initial letter
  26. 26. White bold text for contrast
  27. 27.
  28. 28.
  29. 29.
  30. 30. Title shows the contact name
  31. 31. Subtitle shows the email address
  32. 32. Trailing IconButton with a message icon
  33. 33.
  34. 34. onTap shows a SnackBar with the contact name
  35. 35.
  36. 36.
  37. 37.
  38. 38.
  39. 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

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart