On this page22 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. GridView.count
  5. 2. Spacing and Padding
  6. 3. childAspectRatio
  7. 4. GridView.builder
  8. 5. Columns That Adapt: MaxCrossAxisExtent
  9. 6. A Product Grid
  10. 7. SingleChildScrollView
  11. 8. Scrollbar
  12. 9. A Grid Inside Scrolling Content
  13. 10. A Real-World Example
  14. Code Examples
  15. Common Beginner Mistakes
  16. Best Practices
  17. Mini Challenge
  18. Practice Questions
  19. Practice Answers
  20. Quick Revision
  21. Key Takeaways
  22. What's Next?

Lecture 008 — GridView & Scroll Helpers

Category4 — Scrolling Widgets
Lecture008 of 009
Estimated video length16 min
TargetFlutter 3.44 · Dart 3.12
LevelBeginner

A ListView shows one item per row. A photo gallery or a product catalog shows two, three or four per row — that is a GridView. And when a whole screen is just a little too tall, SingleChildScrollView makes it scroll.

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 screens were taken after dragging the content up.


Prerequisites

Students should already understand, from Lectures 001–007:

  • Container, Card, Image.network and BoxFit.cover
  • Column, Expanded and why a ListView in a Column needs Expanded
  • ListView.builder, itemCount and itemBuilder
  • Dart classes, lists, maps and collection for

Connection With Previous Lectures

In Lecture 007 every list showed one item per row. That is right for messages or transactions, but a shop showing one product per row wastes half the screen, and a gallery of 300 photos in a single column would take forever to scroll through. We need rows that hold several items and still scroll — and, separately, a way to make an ordinary Column scroll when it is only a little too tall.


Learning Objectives

By the end of this lecture you will be able to:

  • Build a grid with GridView.count and a set number of columns
  • Add spacing and padding to a grid, and change the cell shape with childAspectRatio
  • Build a grid from data with GridView.builder and a grid delegate
  • Let the screen width decide the number of columns with MaxCrossAxisExtent
  • Build a product grid with images
  • Make any long content scroll with SingleChildScrollView, and show a Scrollbar
  • Put a grid inside scrolling content with shrinkWrap and NeverScrollableScrollPhysics

1. GridView.count

GridView.count takes the number of columns — crossAxisCount — and a list of children:

dart
body: GridView.count(
  crossAxisCount: 2,
  children: [
    Container(
      color: Colors.indigo.shade100,
      alignment: Alignment.center,
      child: const Text('1'),
    ),
    Container(
      color: Colors.indigo.shade200,
      alignment: Alignment.center,
      child: const Text('2'),
    ),
    Container(
      color: Colors.indigo.shade300,
      alignment: Alignment.center,
      child: const Text('3'),
    ),
    Container(
      color: Colors.indigo.shade400,
      alignment: Alignment.center,
      child: const Text('4'),
    ),
    Container(
      color: Colors.indigo.shade100,
      alignment: Alignment.center,
      child: const Text('5'),
    ),
    Container(
      color: Colors.indigo.shade200,
      alignment: Alignment.center,
      child: const Text('6'),
    ),
  ],
),
Six indigo squares in three rows of two, filling the width
GridView.count with two columns

Every cell is a square by default: the width is the screen width divided by the column count, and the height is the same. Like ListView, a GridView scrolls when it has more rows than fit.

Analogy — a GridView is an egg tray. You choose how many eggs fit across; the tray keeps adding rows as long as you have eggs.


2. Spacing and Padding

mainAxisSpacing is the gap between rows (the grid scrolls vertically, so its main axis is vertical); crossAxisSpacing is the gap between columns. padding adds space around the whole grid:

dart
body: GridView.count(
  crossAxisCount: 3,
  mainAxisSpacing: 12,
  crossAxisSpacing: 12,
  padding: const EdgeInsets.all(12),
  children: [
    Container(
      color: Colors.indigo.shade100,
      alignment: Alignment.center,
      child: const Text('1'),
    ),
    Container(
      color: Colors.indigo.shade200,
      alignment: Alignment.center,
      child: const Text('2'),
    ),
    Container(
      color: Colors.indigo.shade300,
      alignment: Alignment.center,
      child: const Text('3'),
    ),
    Container(
      color: Colors.indigo.shade400,
      alignment: Alignment.center,
      child: const Text('4'),
    ),
    Container(
      color: Colors.indigo.shade100,
      alignment: Alignment.center,
      child: const Text('5'),
    ),
    Container(
      color: Colors.indigo.shade200,
      alignment: Alignment.center,
      child: const Text('6'),
    ),
    Container(
      color: Colors.indigo.shade300,
      alignment: Alignment.center,
      child: const Text('7'),
    ),
    Container(
      color: Colors.indigo.shade400,
      alignment: Alignment.center,
      child: const Text('8'),
    ),
    Container(
      color: Colors.indigo.shade100,
      alignment: Alignment.center,
      child: const Text('9'),
    ),
  ],
),
Nine squares in three columns with gaps between them and around the edges
Spacing and padding

3. childAspectRatio

childAspectRatio changes the cell shape — width ÷ height, exactly like AspectRatio in Lecture 002:

dart
body: GridView.count(
  crossAxisCount: 2,
  childAspectRatio: 2,
  mainAxisSpacing: 12,
  crossAxisSpacing: 12,
  padding: const EdgeInsets.all(12),
  children: [
    Container(
      color: Colors.indigo.shade100,
      alignment: Alignment.center,
      child: const Text('1'),
    ),
    Container(
      color: Colors.indigo.shade200,
      alignment: Alignment.center,
      child: const Text('2'),
    ),
    Container(
      color: Colors.indigo.shade300,
      alignment: Alignment.center,
      child: const Text('3'),
    ),
    Container(
      color: Colors.indigo.shade400,
      alignment: Alignment.center,
      child: const Text('4'),
    ),
    Container(
      color: Colors.indigo.shade100,
      alignment: Alignment.center,
      child: const Text('5'),
    ),
    Container(
      color: Colors.indigo.shade200,
      alignment: Alignment.center,
      child: const Text('6'),
    ),
  ],
),
Six cells twice as wide as they are tall
childAspectRatio: 2

2 makes wide cells; values below 1, such as 0.75, make tall cells — the usual shape for a product with a picture above its name.


4. GridView.builder

For data-driven grids, use GridView.builder. It works like ListView.builder, with one extra required parameter: a gridDelegate that describes the columns.

dart
body: GridView.builder(
  padding: const EdgeInsets.all(12),
  itemCount: 40,
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 4,
    mainAxisSpacing: 8,
    crossAxisSpacing: 8,
  ),
  itemBuilder: (context, index) => Container(
    alignment: Alignment.center,
    decoration: BoxDecoration(
      color: Colors.teal.shade100,
      borderRadius: BorderRadius.circular(12),
    ),
    child: Text('${index + 1}', style: const TextStyle(fontSize: 18)),
  ),
),
Numbered teal tiles 1 to 32 in four columns
GridView.builder with 40 items

SliverGridDelegateWithFixedCrossAxisCount — a long name for "a fixed number of columns" — takes the same crossAxisCount, spacing and childAspectRatio you just used. As with ListView.builder, only the visible cells are built.


5. Columns That Adapt: MaxCrossAxisExtent

On a phone, three columns are fine; on a tablet they would be huge. With SliverGridDelegateWithMaxCrossAxisExtent you set the largest a cell may be, and Flutter chooses how many columns fit:

dart
body: GridView.builder(
  padding: const EdgeInsets.all(12),
  itemCount: 12,
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 150,
    mainAxisSpacing: 8,
    crossAxisSpacing: 8,
  ),
  itemBuilder: (context, index) => Container(
    alignment: Alignment.center,
    color: Colors.orange.shade100,
    child: Text('${index + 1}'),
  ),
),
Twelve orange tiles in three columns
maxCrossAxisExtent: 150 on a 360-pixel-wide phone

With two columns, each cell would be about 164 pixels wide — more than the 150 allowed — so Flutter uses three narrower columns. On a 1000-pixel-wide tablet the same code gives seven.


6. A Product Grid

Combine the grid with what you know from Lecture 006: each cell is a Card with an image that fills the space above the text.

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.deepPurple),
      home: const ShopScreen(),
    );
  }
}

class ShopScreen extends StatelessWidget {
  const ShopScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<Product> products = const [
      Product('Laptop', 185000, 'https://picsum.photos/id/1/400/400'),
      Product('Smartphone', 95000, 'https://picsum.photos/id/160/400/400'),
      Product('Camera', 120000, 'https://picsum.photos/id/250/400/400'),
      Product('Controller', 12000, 'https://picsum.photos/id/96/400/400'),
      Product('Coffee Mug', 1200, 'https://picsum.photos/id/30/400/400'),
      Product('E-reader', 38000, 'https://picsum.photos/id/367/400/400'),
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Shop')),
      body: GridView.builder(
        padding: const EdgeInsets.all(12),
        itemCount: products.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 0.75,
          mainAxisSpacing: 12,
          crossAxisSpacing: 12,
        ),
        itemBuilder: (context, index) {
          Product product = products[index];
          return Card(
            clipBehavior: Clip.antiAlias,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                Expanded(
                  child: Image.network(product.imageUrl, fit: BoxFit.cover),
                ),
                Padding(
                  padding: const EdgeInsets.all(8),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        product.name,
                        style: const TextStyle(fontWeight: FontWeight.w600),
                      ),
                      Text('Rs. ${product.price}'),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

class Product {
  final String name;
  final int price;
  final String imageUrl;

  const Product(this.name, this.price, this.imageUrl);
}
Six product cards in two columns, each with a photo, a bold name and a price
A product grid
  • childAspectRatio: 0.75 makes cells taller than wide.
  • Expanded around the Image gives the photo all the height the text does not need; BoxFit.cover crops it to fit.
  • clipBehavior: Clip.antiAlias on the Card cuts the photo to the card's rounded corners.

7. SingleChildScrollView

ListView and GridView are for many similar items. For a screen that is one long piece of content — an article, a form, a settings page that is a little too tall — wrap the Column in a SingleChildScrollView:

dart
body: SingleChildScrollView(
  padding: const EdgeInsets.all(16),
  child: Column(
    children: [
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade100,
        alignment: Alignment.center,
        child: const Text('Section 1'),
      ),
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade200,
        alignment: Alignment.center,
        child: const Text('Section 2'),
      ),
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade300,
        alignment: Alignment.center,
        child: const Text('Section 3'),
      ),
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade100,
        alignment: Alignment.center,
        child: const Text('Section 4'),
      ),
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade200,
        alignment: Alignment.center,
        child: const Text('Section 5'),
      ),
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade300,
        alignment: Alignment.center,
        child: const Text('Section 6'),
      ),
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade100,
        alignment: Alignment.center,
        child: const Text('Section 7'),
      ),
      Container(
        height: 120,
        margin: const EdgeInsets.only(bottom: 12),
        color: Colors.blueGrey.shade200,
        alignment: Alignment.center,
        child: const Text('Section 8'),
      ),
    ],
  ),
),
Sections 4 to 8 visible after scrolling
SingleChildScrollView, scrolled

It has exactly one child and makes it scrollable. Unlike ListView.builder, it builds the whole child at once, so keep it for content of a reasonable size.


8. Scrollbar

Wrap any scrolling widget in a Scrollbar to show the scroll position on the side. thumbVisibility: true keeps it visible even when the user is not scrolling:

dart
body: Scrollbar(
  thumbVisibility: true,
  child: SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
      children: [
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade100,
          alignment: Alignment.center,
          child: const Text('Section 1'),
        ),
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade200,
          alignment: Alignment.center,
          child: const Text('Section 2'),
        ),
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade300,
          alignment: Alignment.center,
          child: const Text('Section 3'),
        ),
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade100,
          alignment: Alignment.center,
          child: const Text('Section 4'),
        ),
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade200,
          alignment: Alignment.center,
          child: const Text('Section 5'),
        ),
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade300,
          alignment: Alignment.center,
          child: const Text('Section 6'),
        ),
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade100,
          alignment: Alignment.center,
          child: const Text('Section 7'),
        ),
        Container(
          height: 120,
          margin: const EdgeInsets.only(bottom: 12),
          color: Colors.blueGrey.shade200,
          alignment: Alignment.center,
          child: const Text('Section 8'),
        ),
      ],
    ),
  ),
),
Sections 1 to 5 with a thin scroll thumb at the right edge
Scrollbar

9. A Grid Inside Scrolling Content

A home screen often has a banner, a heading and then a grid — all scrolling together. A GridView inside a SingleChildScrollView has the same problem as a ListView in a Column: it wants infinite height. Two settings fix it:

dart
body: SingleChildScrollView(
  padding: const EdgeInsets.all(16),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Container(
        height: 120,
        alignment: Alignment.center,
        color: Colors.indigo.shade100,
        child: const Text('Banner'),
      ),
      const Padding(
        padding: EdgeInsets.symmetric(vertical: 12),
        child: Text('Categories', style: TextStyle(fontSize: 20)),
      ),
      GridView.count(
        crossAxisCount: 3,
        shrinkWrap: true,
        physics: const NeverScrollableScrollPhysics(),
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
        children: [
          Container(
            color: Colors.pink.shade100,
            alignment: Alignment.center,
            child: const Text('1'),
          ),
          Container(
            color: Colors.pink.shade200,
            alignment: Alignment.center,
            child: const Text('2'),
          ),
          Container(
            color: Colors.pink.shade300,
            alignment: Alignment.center,
            child: const Text('3'),
          ),
          Container(
            color: Colors.pink.shade400,
            alignment: Alignment.center,
            child: const Text('4'),
          ),
          Container(
            color: Colors.pink.shade100,
            alignment: Alignment.center,
            child: const Text('5'),
          ),
          Container(
            color: Colors.pink.shade200,
            alignment: Alignment.center,
            child: const Text('6'),
          ),
        ],
      ),
    ],
  ),
),
A banner, a Categories heading and six pink tiles below it
shrinkWrap and NeverScrollableScrollPhysics
  • shrinkWrap: true — the grid is only as tall as its cells need.
  • physics: const NeverScrollableScrollPhysics() — the grid does not scroll on its own; the SingleChildScrollView scrolls everything together.

shrinkWrap builds every cell at once, so use it for short grids such as categories, not for thousands of photos.


10. A Real-World Example

The home screen of a shop: a sale banner, a row of category shortcuts and a grid of popular products, scrolling as one page.

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.deepPurple),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<Product> products = const [
      Product('Laptop', 185000, 'https://picsum.photos/id/1/400/400'),
      Product('Smartphone', 95000, 'https://picsum.photos/id/160/400/400'),
      Product('Camera', 120000, 'https://picsum.photos/id/250/400/400'),
      Product('Controller', 12000, 'https://picsum.photos/id/96/400/400'),
      Product('Coffee Mug', 1200, 'https://picsum.photos/id/30/400/400'),
      Product('E-reader', 38000, 'https://picsum.photos/id/367/400/400'),
    ];
    List<IconData> icons = [
      Icons.laptop,
      Icons.phone_android,
      Icons.camera_alt,
      Icons.headphones,
    ];
    List<String> labels = ['Laptops', 'Phones', 'Cameras', 'Audio'];

    return Scaffold(
      appBar: AppBar(title: const Text('Shop')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          spacing: 16,
          children: [
            Container(
              height: 140,
              padding: const EdgeInsets.all(20),
              alignment: Alignment.bottomLeft,
              decoration: BoxDecoration(
                gradient: const LinearGradient(
                  colors: [Colors.deepPurple, Colors.pink],
                ),
                borderRadius: BorderRadius.circular(16),
              ),
              child: const Text(
                'Summer Sale\nUp to 40% off',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 22,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                for (int i = 0; i < icons.length; i++)
                  Column(
                    children: [
                      CircleAvatar(radius: 26, child: Icon(icons[i])),
                      const SizedBox(height: 4),
                      Text(labels[i]),
                    ],
                  ),
              ],
            ),
            const Text(
              'Popular',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            GridView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: products.length,
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                childAspectRatio: 0.75,
                mainAxisSpacing: 12,
                crossAxisSpacing: 12,
              ),
              itemBuilder: (context, index) {
                Product product = products[index];
                return Card(
                  clipBehavior: Clip.antiAlias,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Expanded(
                        child: Image.network(
                          product.imageUrl,
                          fit: BoxFit.cover,
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(8),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              product.name,
                              style: const TextStyle(
                                fontWeight: FontWeight.w600,
                              ),
                            ),
                            Text('Rs. ${product.price}'),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

class Product {
  final String name;
  final int price;
  final String imageUrl;

  const Product(this.name, this.price, this.imageUrl);
}
A purple-pink Summer Sale banner, four round category icons, a Popular heading and product cards below
Shop home screen

The category row uses a collection for to build four Columns from two parallel lists; the grid is the product grid from section 6 with shrinkWrap and NeverScrollableScrollPhysics.


Code Examples

Example 1 — Numbered Circles

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: GridView.count(
        crossAxisCount: 3,
        padding: const EdgeInsets.all(8),
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
        children: [
          for (int i = 1; i <= 15; i++)
            Container(
              alignment: Alignment.center,
              decoration: const BoxDecoration(
                color: Colors.indigo,
                shape: BoxShape.circle,
              ),
              child: Text('$i', style: const TextStyle(color: Colors.white)),
            ),
        ],
      ),
    );
  }
}
Fifteen indigo circles numbered 1 to 15 in three columns
Example 1

Two-pixel gaps make a tight gallery. The photo URL is built from the id in the list.

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 GalleryScreen(),
    );
  }
}

class GalleryScreen extends StatelessWidget {
  const GalleryScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<int> photoIds = [10, 11, 15, 17, 28, 29, 37, 42, 49, 54, 57, 58];

    return Scaffold(
      appBar: AppBar(title: const Text('Gallery')),
      body: GridView.builder(
        itemCount: photoIds.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 2,
          crossAxisSpacing: 2,
        ),
        itemBuilder: (context, index) => Image.network(
          'https://picsum.photos/id/${photoIds[index]}/300/300',
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}
Twelve landscape photos in a tight three-column grid
Example 2

Example 3 — Bank Services 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 MenuScreen(),
    );
  }
}

class MenuScreen extends StatelessWidget {
  const MenuScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<IconData> icons = [
      Icons.send,
      Icons.receipt_long,
      Icons.phone_android,
      Icons.bolt,
      Icons.water_drop,
      Icons.wifi,
      Icons.school,
      Icons.more_horiz,
    ];
    List<String> labels = [
      'Transfer',
      'Bills',
      'Top-up',
      'Electricity',
      'Water',
      'Internet',
      'Fees',
      'More',
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Services')),
      body: GridView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: icons.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 4,
          mainAxisSpacing: 16,
        ),
        itemBuilder: (context, index) => Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton.filledTonal(onPressed: () {}, icon: Icon(icons[index])),
            Text(labels[index], style: const TextStyle(fontSize: 12)),
          ],
        ),
      ),
    );
  }
}
Eight tinted icon buttons with labels in four columns: Transfer, Bills, Top-up, Electricity, Water, Internet, Fees, More
Example 3

Example 4 — Calculator Keypad

A shrinkWrap grid at the bottom of a Column; the display above it is Expanded.

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.orange),
      home: const KeypadScreen(),
    );
  }
}

class KeypadScreen extends StatelessWidget {
  const KeypadScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<String> keys = [
      '7', '8', '9', '÷', //
      '4', '5', '6', '×', //
      '1', '2', '3', '−', //
      '0', '.', '=', '+', //
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Calculator')),
      body: Column(
        children: [
          Expanded(
            child: Container(
              alignment: Alignment.bottomRight,
              padding: const EdgeInsets.all(24),
              child: const Text('1,250', style: TextStyle(fontSize: 56)),
            ),
          ),
          GridView.count(
            crossAxisCount: 4,
            shrinkWrap: true,
            padding: const EdgeInsets.all(12),
            mainAxisSpacing: 12,
            crossAxisSpacing: 12,
            children: [
              for (String key in keys)
                FilledButton.tonal(
                  onPressed: () {},
                  child: Text(key, style: const TextStyle(fontSize: 24)),
                ),
            ],
          ),
        ],
      ),
    );
  }
}
A large 1,250 display above a four-by-four grid of round keys
Example 4

Example 5 — Long Text

A single long Text made scrollable.

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 TermsScreen(),
    );
  }
}

class TermsScreen extends StatelessWidget {
  const TermsScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Terms')),
      body: const SingleChildScrollView(
        padding: EdgeInsets.all(20),
        child: Text(
          'Welcome to the course portal. These terms explain how lectures, '
          'videos and practice files may be used. '
          'You may watch every lecture as often as you like and copy the code '
          'into your own projects. You may not upload the videos to other sites '
          'or sell the course material. '
          'Progress is saved on your own device only. Clearing your browser data '
          'clears your progress as well. '
          'Lectures are updated when Flutter changes, so a screenshot may look '
          'slightly different from what you see. Every code sample is tested '
          'before it is published. '
          'Questions about a lecture can be sent to the instructor through the '
          'contact page. We try to answer within two working days. '
          'These terms may change when new courses are added. The latest version '
          'is always available on this page. '
          'Thank you for learning with us, and enjoy building your first apps.',
          style: TextStyle(fontSize: 18, height: 1.6),
        ),
      ),
    );
  }
}
A full screen of terms text in large type
Example 5

Example 6 — Temperature Tiles

Hot cities (30 °C and above) are orange, the rest blue.

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 TemperatureScreen(),
    );
  }
}

class TemperatureScreen extends StatelessWidget {
  const TemperatureScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<String> cities = [
      'Lahore',
      'Karachi',
      'Islamabad',
      'Quetta',
      'Multan',
      'Murree',
    ];
    List<int> temperatures = [30, 34, 26, 18, 36, 12];

    return Scaffold(
      appBar: AppBar(title: const Text('Cities')),
      body: GridView.builder(
        padding: const EdgeInsets.all(12),
        itemCount: cities.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 1.4,
          mainAxisSpacing: 12,
          crossAxisSpacing: 12,
        ),
        itemBuilder: (context, index) {
          int celsius = temperatures[index];
          return Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: celsius >= 30
                  ? Colors.orange.shade100
                  : Colors.lightBlue.shade100,
              borderRadius: BorderRadius.circular(16),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(cities[index], style: const TextStyle(fontSize: 18)),
                Text(
                  '$celsius°C',
                  style: const TextStyle(
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}
Six city tiles with temperatures, orange for hot and blue for cooler
Example 6

Common Beginner Mistakes

1. Forgetting gridDelegate

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

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: GridView.builder(
          itemCount: 6,
          itemBuilder: (context, index) => Text('$index'),
        ),
      ),
    ),
  );
}
output
error • The named parameter 'gridDelegate' is required, but there's no corresponding argument. Try adding the required argument • lib/main.dart:7:15 • missing_required_argument

GridView.builder must know the columns: add a SliverGridDelegateWithFixedCrossAxisCount or …WithMaxCrossAxisExtent.

2. Content too tall for its cell

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('Oops')),
      body: GridView.count(
        crossAxisCount: 2,
        childAspectRatio: 3,
        children: [
          for (int i = 1; i <= 4; i++)
            Card(
              child: Column(
                children: [
                  const Icon(Icons.image, size: 64),
                  Text('Product $i'),
                  const Text('Rs. 1200'),
                ],
              ),
            ),
        ],
      ),
    );
  }
}
Short wide cards with icons cut off and overflow stripes
Cells too short for their content
output
══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 52 pixels on the bottom.

Cells have a fixed size from childAspectRatio; content does not make them grow. Lower the ratio (taller cells) or shrink the content.

3. Expanded inside a SingleChildScrollView

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('Oops')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            const Text('Header'),
            Expanded(child: Container(color: Colors.red)),
          ],
        ),
      ),
    );
  }
}
An empty screen
Nothing is drawn
output
══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════
The following assertion was thrown during performLayout():
RenderFlex children have non-zero flex but incoming height
constraints are unbounded.

A SingleChildScrollView gives its child unlimited height, so "fill the remaining space" has no meaning. Inside scrolling content, give children real heights instead of Expanded.

4. A GridView inside a SingleChildScrollView or Column without limits

The "unbounded height" error from Lecture 007. Use Expanded around the grid in a Column, or shrinkWrap + NeverScrollableScrollPhysics inside a scroll view.


Best Practices

  • Use GridView.builder for data, GridView.count for a small fixed set of cells.
  • Prefer MaxCrossAxisExtent when the app may run on tablets or the web.
  • Choose childAspectRatio from the content: below 1 for picture-and-text cards, above 1 for short labels.
  • Use SingleChildScrollView for one long piece of content, ListView/GridView for many repeated items.
  • Use shrinkWrap only for short lists and grids.

Mini Challenge

Show the results of six students — Ali 240, Sara 275, Zayan 198, Hina 150, Omar 262, Ayesha 221 (out of 300) — in a two-column grid. Each cell shows the name and the percentage (rounded) on two lines, centered. Students with 55 % or more get a light-green cell with a green border; the others light red with a red border.

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.blue),
      home: const ResultsScreen(),
    );
  }
}

class ResultsScreen extends StatelessWidget {
  const ResultsScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<String> names = ['Ali', 'Sara', 'Zayan', 'Hina', 'Omar', 'Ayesha'];
    List<int> marks = [240, 275, 198, 150, 262, 221];

    return Scaffold(
      appBar: AppBar(title: const Text('Results')),
      body: GridView.builder(
        padding: const EdgeInsets.all(12),
        itemCount: names.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 1.5,
          mainAxisSpacing: 12,
          crossAxisSpacing: 12,
        ),
        itemBuilder: (context, index) {
          int percent = (marks[index] / 300 * 100).round();
          bool passed = percent >= 55;
          return Container(
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: passed ? Colors.green.shade50 : Colors.red.shade50,
              border: Border.all(color: passed ? Colors.green : Colors.red),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              '${names[index]}\n$percent%',
              textAlign: TextAlign.center,
              style: const TextStyle(fontSize: 20),
            ),
          );
        },
      ),
    );
  }
}
Six result tiles in two columns; Hina's 50% tile is red, the others green
Mini challenge solution

How it works

  1. (marks[index] / 300 * 100).round() turns marks into a whole percentage.
  2. passed decides both the fill and the border color.
  3. childAspectRatio: 1.5 makes the tiles wider than tall.

Practice Questions

Beginner

  1. Show six colored squares — red, orange, yellow, green, blue, purple — in a two-column GridView.count.
  2. Show four colored squares in two columns with 8 pixels of spacing and padding.
  3. Show four subject Cards (Math, Science, English, Urdu) in two columns, twice as wide as tall.

Application

  1. Show the numbers 1 to 20 in a four-column GridView.builder.
  2. Show ten teal tiles with a maximum cell width of 150 pixels.
  3. Show ten 120-pixel boxes, Box 1 to Box 10, in alternating amber shades, scrollable with a SingleChildScrollView.
  4. Store {'Lahore': 30, 'Karachi': 34, 'Quetta': 18, 'Murree': 12} and show each city and its temperature in a two-column grid of cards.

Challenge

  1. Build a store page that scrolls as one: a 100-pixel banner Free delivery this week above the six-product grid from section 6.

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: GridView.count(
        crossAxisCount: 2,
        children: [
          Container(color: Colors.red),
          Container(color: Colors.orange),
          Container(color: Colors.yellow),
          Container(color: Colors.green),
          Container(color: Colors.blue),
          Container(color: Colors.purple),
        ],
      ),
    );
  }
}
Six colored squares in two columns
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: GridView.count(
        crossAxisCount: 2,
        padding: const EdgeInsets.all(8),
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
        children: [
          Container(color: Colors.red),
          Container(color: Colors.orange),
          Container(color: Colors.yellow),
          Container(color: Colors.green),
        ],
      ),
    );
  }
}
Four colored squares with gaps
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: GridView.count(
        crossAxisCount: 2,
        childAspectRatio: 2,
        padding: const EdgeInsets.all(8),
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
        children: const [
          Card(child: Center(child: Text('Math'))),
          Card(child: Center(child: Text('Science'))),
          Card(child: Center(child: Text('English'))),
          Card(child: Center(child: Text('Urdu'))),
        ],
      ),
    );
  }
}
Four wide cards with subject names
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 HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Question 4')),
      body: GridView.builder(
        itemCount: 20,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 4,
        ),
        itemBuilder: (context, index) => Center(child: Text('${index + 1}')),
      ),
    );
  }
}
Numbers 1 to 20 in a four-column grid
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 HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Question 5')),
      body: GridView.builder(
        padding: const EdgeInsets.all(8),
        itemCount: 10,
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 150,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
        ),
        itemBuilder: (context, index) => Container(color: Colors.teal.shade200),
      ),
    );
  }
}
Ten teal tiles in three columns
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) {
    return Scaffold(
      appBar: AppBar(title: const Text('Question 6')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            for (int i = 1; i <= 10; i++)
              Container(
                height: 120,
                color: i.isEven ? Colors.amber.shade100 : Colors.amber.shade300,
                alignment: Alignment.center,
                child: Text('Box $i'),
              ),
          ],
        ),
      ),
    );
  }
}
Boxes 4 to 9 in alternating amber after scrolling
Question 6, scrolled

Question 7 — 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) {
    Map<String, int> temperatures = {
      'Lahore': 30,
      'Karachi': 34,
      'Quetta': 18,
      'Murree': 12,
    };
    List<String> cities = temperatures.keys.toList();

    return Scaffold(
      appBar: AppBar(title: const Text('Question 7')),
      body: GridView.builder(
        padding: const EdgeInsets.all(12),
        itemCount: cities.length,
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          mainAxisSpacing: 12,
          crossAxisSpacing: 12,
        ),
        itemBuilder: (context, index) => Card(
          child: Center(
            child: Text(
              '${cities[index]}\n${temperatures[cities[index]]}°C',
              textAlign: TextAlign.center,
              style: const TextStyle(fontSize: 20),
            ),
          ),
        ),
      ),
    );
  }
}
Four city cards with temperatures
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.deepPurple),
      home: const StoreScreen(),
    );
  }
}

class StoreScreen extends StatelessWidget {
  const StoreScreen({super.key});

  @override
  Widget build(BuildContext context) {
    List<Product> products = const [
      Product('Laptop', 185000, 'https://picsum.photos/id/1/400/400'),
      Product('Smartphone', 95000, 'https://picsum.photos/id/160/400/400'),
      Product('Camera', 120000, 'https://picsum.photos/id/250/400/400'),
      Product('Controller', 12000, 'https://picsum.photos/id/96/400/400'),
      Product('Coffee Mug', 1200, 'https://picsum.photos/id/30/400/400'),
      Product('E-reader', 38000, 'https://picsum.photos/id/367/400/400'),
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Question 8')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          spacing: 16,
          children: [
            Container(
              height: 100,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                color: Colors.deepPurple.shade100,
                borderRadius: BorderRadius.circular(16),
              ),
              child: const Text(
                'Free delivery this week',
                style: TextStyle(fontSize: 18),
              ),
            ),
            GridView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: products.length,
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                childAspectRatio: 0.75,
                mainAxisSpacing: 12,
                crossAxisSpacing: 12,
              ),
              itemBuilder: (context, index) {
                Product product = products[index];
                return Card(
                  clipBehavior: Clip.antiAlias,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Expanded(
                        child: Image.network(
                          product.imageUrl,
                          fit: BoxFit.cover,
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(8),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              product.name,
                              style: const TextStyle(
                                fontWeight: FontWeight.w600,
                              ),
                            ),
                            Text('Rs. ${product.price}'),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

class Product {
  final String name;
  final int price;
  final String imageUrl;

  const Product(this.name, this.price, this.imageUrl);
}
A lilac banner above product cards
Question 8

Quick Revision

  1. What does crossAxisCount set?
  2. Which spacing parameter puts gaps between rows of a vertical grid?
  3. What shape are grid cells when childAspectRatio is not given?
  4. What extra parameter does GridView.builder need compared to ListView.builder?
  5. What does maxCrossAxisExtent decide?
  6. When should you use SingleChildScrollView instead of ListView?
  7. What do shrinkWrap and NeverScrollableScrollPhysics do?
  8. Why does Expanded not work inside a SingleChildScrollView?

Key Takeaways

  • GridView.count(crossAxisCount: n) lays children in n columns and scrolls.
  • mainAxisSpacing, crossAxisSpacing, padding and childAspectRatio shape the grid.
  • GridView.builder + a grid delegate builds cells lazily from data.
  • …WithMaxCrossAxisExtent adapts the column count to the screen width.
  • SingleChildScrollView makes one long child scrollable; Scrollbar shows the position.
  • A grid inside scrolling content needs shrinkWrap: true and NeverScrollableScrollPhysics.

What's Next?

Lecture 009 — Mini Project: Profile & Product Screens. You now know every widget in this course. In the final lecture we build two complete screens from scratch — a profile and a product catalog — and connect them in one app, splitting the code into small reusable widgets.