On this page22 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. GridView.count
- 2. Spacing and Padding
- 3. childAspectRatio
- 4. GridView.builder
- 5. Columns That Adapt: MaxCrossAxisExtent
- 6. A Product Grid
- 7. SingleChildScrollView
- 8. Scrollbar
- 9. A Grid Inside Scrolling Content
- 10. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 008 — GridView & Scroll Helpers
| Category | 4 — Scrolling Widgets |
| Lecture | 008 of 009 |
| Estimated video length | 16 min |
| Target | Flutter 3.44 · Dart 3.12 |
| Level | Beginner |
A
ListViewshows one item per row. A photo gallery or a product catalog shows two, three or four per row — that is aGridView. And when a whole screen is just a little too tall,SingleChildScrollViewmakes 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.networkandBoxFit.coverColumn,Expandedand why aListViewin aColumnneedsExpandedListView.builder,itemCountanditemBuilder- 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.countand 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.builderand 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 aScrollbar - Put a grid inside scrolling content with
shrinkWrapandNeverScrollableScrollPhysics
1. GridView.count
GridView.count takes the number of columns — crossAxisCount — and a list of children:
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'),
),
],
),
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
GridViewis 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:
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'),
),
],
),
3. childAspectRatio
childAspectRatio changes the cell shape — width ÷ height, exactly like AspectRatio in
Lecture 002:
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'),
),
],
),
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.
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)),
),
),
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:
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}'),
),
),
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.
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);
}
childAspectRatio: 0.75makes cells taller than wide.Expandedaround theImagegives the photo all the height the text does not need;BoxFit.covercrops it to fit.clipBehavior: Clip.antiAliason theCardcuts 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:
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'),
),
],
),
),
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:
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'),
),
],
),
),
),
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:
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'),
),
],
),
],
),
),
shrinkWrap: true— the grid is only as tall as its cells need.physics: const NeverScrollableScrollPhysics()— the grid does not scroll on its own; theSingleChildScrollViewscrolls 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.
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);
}
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
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)),
),
],
),
);
}
}
Example 2 — Photo Gallery
Two-pixel gaps make a tight gallery. The photo URL is built from the id in the list.
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,
),
),
);
}
}
Example 3 — Bank Services Menu
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const 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)),
],
),
),
);
}
}
Example 4 — Calculator Keypad
A shrinkWrap grid at the bottom of a Column; the display above it is Expanded.
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)),
),
],
),
],
),
);
}
}
Example 5 — Long Text
A single long Text made scrollable.
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),
),
),
);
}
}
Example 6 — Temperature Tiles
Hot cities (30 °C and above) are orange, the rest blue.
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,
),
),
],
),
);
},
),
);
}
}
Common Beginner Mistakes
1. Forgetting gridDelegate
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: GridView.builder(
itemCount: 6,
itemBuilder: (context, index) => Text('$index'),
),
),
),
);
}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_argumentGridView.builder must know the columns: add a SliverGridDelegateWithFixedCrossAxisCount or
…WithMaxCrossAxisExtent.
2. Content too tall for its cell
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'),
],
),
),
],
),
);
}
}
══╡ 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
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)),
],
),
),
);
}
}
══╡ 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.builderfor data,GridView.countfor a small fixed set of cells. - Prefer
MaxCrossAxisExtentwhen the app may run on tablets or the web. - Choose
childAspectRatiofrom the content: below1for picture-and-text cards, above1for short labels. - Use
SingleChildScrollViewfor one long piece of content,ListView/GridViewfor many repeated items. - Use
shrinkWraponly 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
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),
),
);
},
),
);
}
}
How it works
(marks[index] / 300 * 100).round()turns marks into a whole percentage.passeddecides both the fill and the border color.childAspectRatio: 1.5makes the tiles wider than tall.
Practice Questions
Beginner
- Show six colored squares — red, orange, yellow, green, blue, purple — in a two-column
GridView.count. - Show four colored squares in two columns with 8 pixels of spacing and padding.
- Show four subject
Cards (Math,Science,English,Urdu) in two columns, twice as wide as tall.
Application
- Show the numbers 1 to 20 in a four-column
GridView.builder. - Show ten teal tiles with a maximum cell width of 150 pixels.
- Show ten 120-pixel boxes,
Box 1toBox 10, in alternating amber shades, scrollable with aSingleChildScrollView. - Store
{'Lahore': 30, 'Karachi': 34, 'Quetta': 18, 'Murree': 12}and show each city and its temperature in a two-column grid of cards.
Challenge
- Build a store page that scrolls as one: a 100-pixel banner
Free delivery this weekabove the six-product grid from section 6.
Practice Answers
Question 1 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Question 1')),
body: 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),
],
),
);
}
}
Question 2 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Question 2')),
body: 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),
],
),
);
}
}
Question 3 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Question 3')),
body: 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'))),
],
),
);
}
}
Question 4 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const 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}')),
),
);
}
}
Question 5 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const 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),
),
);
}
}
Question 6 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
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'),
),
],
),
),
);
}
}
Question 7 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
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),
),
),
),
),
);
}
}
Question 8 — Solution
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.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);
}
Quick Revision
- What does
crossAxisCountset? - Which spacing parameter puts gaps between rows of a vertical grid?
- What shape are grid cells when
childAspectRatiois not given? - What extra parameter does
GridView.builderneed compared toListView.builder? - What does
maxCrossAxisExtentdecide? - When should you use
SingleChildScrollViewinstead ofListView? - What do
shrinkWrapandNeverScrollableScrollPhysicsdo? - Why does
Expandednot work inside aSingleChildScrollView?
Key Takeaways
GridView.count(crossAxisCount: n)lays children inncolumns and scrolls.mainAxisSpacing,crossAxisSpacing,paddingandchildAspectRatioshape the grid.GridView.builder+ a grid delegate builds cells lazily from data.…WithMaxCrossAxisExtentadapts the column count to the screen width.SingleChildScrollViewmakes one long child scrollable;Scrollbarshows the position.- A grid inside scrolling content needs
shrinkWrap: trueandNeverScrollableScrollPhysics.
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.