On this page21 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. The Plan
  5. 2. A Widget That Takes Values
  6. 3. More Reusable Pieces: ProfileHeader and MenuTile
  7. 4. The Profile Screen
  8. 5. Passing Objects: Product and ProductCard
  9. 6. Optional Parameters: CategoryChip
  10. 7. The Shop Screen
  11. 8. Two Screens, One App: Tabs
  12. 9. The Complete App
  13. Code Examples
  14. Common Beginner Mistakes
  15. Best Practices
  16. Mini Challenge
  17. Practice Questions
  18. Practice Answers
  19. Quick Revision
  20. Key Takeaways
  21. What's Next?

Lecture 009 — Mini Project: Profile & Product Screens

Category5 — Mini Project
Lecture009 of 009
Estimated video length25 min
TargetFlutter 3.44 · Dart 3.12
LevelBeginner

Eight lectures gave you the pieces. This one builds a real app from them — a profile screen and a shop screen in one app with tabs — and shows how to cut a big screen into small widgets you write once and reuse.

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.


Prerequisites

This lecture uses everything from Lectures 001–008:

  • Scaffold, AppBar, Container, BoxDecoration and TextStyle
  • Row, Column, Expanded, Stack and Positioned
  • Image.network, CircleAvatar, icons, buttons and SnackBar
  • ListTile, ListView.separated, GridView.builder and SingleChildScrollView
  • from the Dart course: classes with final fields, named parameters, required and default values

Connection With Previous Lectures

The real-world examples so far were each one screen, written as one long build() method. Look back at Ali's result card in Lecture 004: four almost identical Rows, copied and edited by hand. In a real app the same pieces — a stat box, a product card, a menu row — appear again and again. Copying them makes the code long and every change slow. We need to turn a piece of screen into our own widget that takes values, the way Text takes a string.


Learning Objectives

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

  • Plan a screen as a tree of small widgets before writing code
  • Write a StatelessWidget that receives values through its constructor
  • Use required parameters, optional parameters with defaults, and nullable parameters
  • Pass whole objects (a Product) into a widget
  • Build a complete profile screen and a product catalog from reusable widgets
  • Put two screens in one app with DefaultTabController, TabBar and TabBarView
  • Add a widget to a list only when a condition is true, with collection if

1. The Plan

Sara is opening an online shop. Her app has two tabs:

  • Profile — a cover with her photo, her name and city, three statistics, two buttons and a menu.
  • Shop — a search bar, a row of categories and a grid of products.

Before writing code, split each screen into pieces, and notice which pieces repeat:

output
HomeScreen (tabs)
├── ProfileScreen
│   ├── ProfileHeader        cover + avatar          (Stack)
│   ├── name, city           Text, Row
│   ├── StatCard × 3         ← repeats, different values
│   ├── buttons              FilledButton, OutlinedButton
│   └── MenuTile × 3         ← repeats, different icon and title
└── ShopScreen
    ├── search bar           Container + Row
    ├── CategoryChip × 7     ← repeats             (horizontal ListView)
    └── ProductCard × 6      ← repeats, one per product (GridView)

Analogy — a cookie cutter. You design the shape once (StatCard), then press out as many cookies as you need, each with a different topping (value, label).


2. A Widget That Takes Values

Every repeated piece becomes a class. What changes between copies becomes a field, filled in through the constructor — exactly like the Dart classes you wrote in the Dart course:

dart
class StatCard extends StatelessWidget {
  final String value;
  final String label;

  const StatCard({super.key, required this.value, required this.label});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 12),
      decoration: BoxDecoration(
        color: Colors.indigo.shade50,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            value,
            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          Text(label, style: const TextStyle(color: Colors.black54)),
        ],
      ),
    );
  }
}
  • The fields are final: a widget never changes after it is created. (A StatelessWidget must have only final fields — the editor warns you otherwise.)
  • required this.value makes the caller pass a value; forgetting it is a compile error.
  • build() uses the fields like any variable.

Now the widget is used like any built-in one:

dart
body: const Padding(
  padding: EdgeInsets.all(16),
  child: Row(
    spacing: 12,
    children: [
      Expanded(
        child: StatCard(value: '2', label: 'Courses'),
      ),
      Expanded(
        child: StatCard(value: '149', label: 'Lectures'),
      ),
      Expanded(
        child: StatCard(value: '1', label: 'Certificate'),
      ),
    ],
  ),
),
Three equal light-indigo boxes: 2 Courses, 149 Lectures, 1 Certificate
Three StatCards

Three boxes from one class. Change the design inside StatCard and all three change. The Column uses MainAxisSize.min so a card is only as tall as its text.


3. More Reusable Pieces: ProfileHeader and MenuTile

The header is the cover-and-avatar Stack from Lecture 005. It has no parameters, but giving it its own class keeps ProfileScreen short and readable:

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      clipBehavior: Clip.none,
      alignment: Alignment.bottomCenter,
      children: [
        Container(
          height: 140,
          decoration: const BoxDecoration(
            gradient: LinearGradient(colors: [Colors.indigo, Colors.purple]),
          ),
        ),
        Positioned(
          bottom: -48,
          child: Container(
            padding: const EdgeInsets.all(4),
            decoration: const BoxDecoration(
              color: Colors.white,
              shape: BoxShape.circle,
            ),
            child: const CircleAvatar(
              radius: 48,
              backgroundImage: NetworkImage(
                'https://picsum.photos/id/64/300/300',
              ),
            ),
          ),
        ),
      ],
    );
  }
}

MenuTile wraps a ListTile. A field can hold any type — here IconData, the type of Icons.settings:

dart
class MenuTile extends StatelessWidget {
  final IconData icon;
  final String title;

  const MenuTile({super.key, required this.icon, required this.title});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      trailing: const Icon(Icons.chevron_right),
      onTap: () {},
    );
  }
}

4. The Profile Screen

With the pieces ready, the screen reads almost like the plan:

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

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          const ProfileHeader(),
          const SizedBox(height: 56),
          const Text(
            'Sara Khan',
            style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
          ),
          const Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.location_on, size: 16, color: Colors.black54),
              Text('Lahore · Flutter student'),
            ],
          ),
          const Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              spacing: 12,
              children: [
                Expanded(
                  child: StatCard(value: '2', label: 'Courses'),
                ),
                Expanded(
                  child: StatCard(value: '149', label: 'Lectures'),
                ),
                Expanded(
                  child: StatCard(value: '1', label: 'Certificate'),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              spacing: 12,
              children: [
                Expanded(
                  child: FilledButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.edit),
                    label: const Text('Edit profile'),
                  ),
                ),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.share),
                    label: const Text('Share'),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 8),
          const MenuTile(icon: Icons.shopping_bag_outlined, title: 'My orders'),
          const MenuTile(icon: Icons.favorite_border, title: 'Wishlist'),
          const MenuTile(icon: Icons.settings_outlined, title: 'Settings'),
        ],
      ),
    );
  }
}
A purple cover with Sara's round photo overlapping it, Sara Khan, Lahore · Flutter student, three stat boxes, Edit profile and Share buttons, and My orders, Wishlist and Settings rows
The profile screen
  • SingleChildScrollView lets the screen scroll on small phones.
  • SizedBox(height: 56) leaves room for the half of the avatar that hangs below the cover.
  • Because every StatCard and MenuTile gets only fixed values, they can all be const.

5. Passing Objects: Product and ProductCard

Products have five values each. Instead of five parameters, pass one Product object:

dart
class Product {
  final String name;
  final String category;
  final int price;
  final double rating;
  final String imageUrl;

  const Product(
    this.name,
    this.category,
    this.price,
    this.rating,
    this.imageUrl,
  );
}
dart
const List<Product> products = [
  Product(
    'Laptop',
    'Laptops',
    185000,
    4.8,
    'https://picsum.photos/id/1/400/400',
  ),
  Product(
    'Smartphone',
    'Phones',
    95000,
    4.6,
    'https://picsum.photos/id/160/400/400',
  ),
  Product(
    'Camera',
    'Cameras',
    120000,
    4.7,
    'https://picsum.photos/id/250/400/400',
  ),
  Product(
    'Controller',
    'Games',
    12000,
    4.3,
    'https://picsum.photos/id/96/400/400',
  ),
  Product(
    'Coffee Mug',
    'Home',
    1200,
    4.9,
    'https://picsum.photos/id/30/400/400',
  ),
  Product(
    'E-reader',
    'Books',
    38000,
    4.5,
    'https://picsum.photos/id/367/400/400',
  ),
];

The list is a top-level const — written outside any class, so every screen can read it.

dart
class ProductCard extends StatelessWidget {
  final Product product;

  const ProductCard({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(child: Image.network(product.imageUrl, fit: BoxFit.cover)),
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 8, 4, 4),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: const TextStyle(fontWeight: FontWeight.w600),
                ),
                Row(
                  children: [
                    const Icon(Icons.star, size: 16, color: Colors.amber),
                    Text(' ${product.rating}'),
                  ],
                ),
                Row(
                  children: [
                    Expanded(
                      child: Text(
                        'Rs. ${product.price}',
                        style: const TextStyle(color: Colors.indigo),
                      ),
                    ),
                    IconButton(
                      onPressed: () {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text('${product.name} added to cart'),
                          ),
                        );
                      },
                      icon: const Icon(Icons.add_shopping_cart),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

ProductCard reads product.name, product.price and so on. Its cart button shows a SnackBar that names the product — each card knows which product it shows.


6. Optional Parameters: CategoryChip

A category chip needs a label, and maybe a highlight. Parameters without required are optional; give them a default with =:

dart
class CategoryChip extends StatelessWidget {
  final String label;
  final bool selected;

  const CategoryChip({super.key, required this.label, this.selected = false});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: selected ? Colors.indigo : Colors.indigo.shade50,
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        label,
        style: TextStyle(color: selected ? Colors.white : Colors.indigo),
      ),
    );
  }
}

CategoryChip(label: 'Phones') is not selected; CategoryChip(label: 'All', selected: true) is.


7. The Shop Screen

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

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          margin: const EdgeInsets.all(16),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          decoration: BoxDecoration(
            color: Colors.grey.shade200,
            borderRadius: BorderRadius.circular(30),
          ),
          child: const Row(
            spacing: 8,
            children: [
              Icon(Icons.search, color: Colors.black54),
              Text('Search products', style: TextStyle(color: Colors.black54)),
            ],
          ),
        ),
        SizedBox(
          height: 40,
          child: ListView.separated(
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.symmetric(horizontal: 16),
            itemCount: categories.length,
            separatorBuilder: (context, index) => const SizedBox(width: 8),
            itemBuilder: (context, index) =>
                CategoryChip(label: categories[index], selected: index == 0),
          ),
        ),
        Expanded(
          child: GridView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: products.length,
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 0.68,
              mainAxisSpacing: 12,
              crossAxisSpacing: 12,
            ),
            itemBuilder: (context, index) =>
                ProductCard(product: products[index]),
          ),
        ),
      ],
    );
  }
}
A grey search bar, a row of category chips with All highlighted, and a two-column grid of product cards with photos, ratings, prices and cart buttons
The shop screen

The search bar is only a styled Container for now — typing into a real text field needs TextField and state, which come after this course. The grid is Expanded, so it scrolls in the space under the chips.


8. Two Screens, One App: Tabs

DefaultTabController connects a TabBar (the tab buttons) with a TabBarView (the pages). Its length must match the number of tabs and pages:

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

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Sara Shop'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.person), text: 'Profile'),
              Tab(icon: Icon(Icons.storefront), text: 'Shop'),
            ],
          ),
        ),
        body: const TabBarView(children: [ProfileScreen(), ShopScreen()]),
      ),
    );
  }
}
The Sara Shop app bar with Profile and Shop tabs; the Profile tab is selected and shows the profile screen
The finished app — Profile tab

The TabBar sits in the AppBar's bottom slot. Tap a tab — or swipe the page sideways — to switch:

The Shop tab selected, showing the search bar, categories and products
The Shop tab
The Shop tab with the message Laptop added to cart at the bottom
After tapping the cart button on the Laptop card

DefaultTabController remembers the selected tab for you, which is why this works in a StatelessWidget.


9. The Complete App

Here is the whole project in one file. In a bigger project you would move each widget into its own file under lib/ (for example lib/widgets/product_card.dart) and import it.

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,
      title: 'Sara Shop',
      theme: ThemeData(colorSchemeSeed: Colors.indigo),
      home: const HomeScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Sara Shop'),
          bottom: const TabBar(
            tabs: [
              Tab(icon: Icon(Icons.person), text: 'Profile'),
              Tab(icon: Icon(Icons.storefront), text: 'Shop'),
            ],
          ),
        ),
        body: const TabBarView(children: [ProfileScreen(), ShopScreen()]),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          const ProfileHeader(),
          const SizedBox(height: 56),
          const Text(
            'Sara Khan',
            style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold),
          ),
          const Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.location_on, size: 16, color: Colors.black54),
              Text('Lahore · Flutter student'),
            ],
          ),
          const Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              spacing: 12,
              children: [
                Expanded(
                  child: StatCard(value: '2', label: 'Courses'),
                ),
                Expanded(
                  child: StatCard(value: '149', label: 'Lectures'),
                ),
                Expanded(
                  child: StatCard(value: '1', label: 'Certificate'),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              spacing: 12,
              children: [
                Expanded(
                  child: FilledButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.edit),
                    label: const Text('Edit profile'),
                  ),
                ),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: () {},
                    icon: const Icon(Icons.share),
                    label: const Text('Share'),
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 8),
          const MenuTile(icon: Icons.shopping_bag_outlined, title: 'My orders'),
          const MenuTile(icon: Icons.favorite_border, title: 'Wishlist'),
          const MenuTile(icon: Icons.settings_outlined, title: 'Settings'),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      clipBehavior: Clip.none,
      alignment: Alignment.bottomCenter,
      children: [
        Container(
          height: 140,
          decoration: const BoxDecoration(
            gradient: LinearGradient(colors: [Colors.indigo, Colors.purple]),
          ),
        ),
        Positioned(
          bottom: -48,
          child: Container(
            padding: const EdgeInsets.all(4),
            decoration: const BoxDecoration(
              color: Colors.white,
              shape: BoxShape.circle,
            ),
            child: const CircleAvatar(
              radius: 48,
              backgroundImage: NetworkImage(
                'https://picsum.photos/id/64/300/300',
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class MenuTile extends StatelessWidget {
  final IconData icon;
  final String title;

  const MenuTile({super.key, required this.icon, required this.title});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      trailing: const Icon(Icons.chevron_right),
      onTap: () {},
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Container(
          margin: const EdgeInsets.all(16),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          decoration: BoxDecoration(
            color: Colors.grey.shade200,
            borderRadius: BorderRadius.circular(30),
          ),
          child: const Row(
            spacing: 8,
            children: [
              Icon(Icons.search, color: Colors.black54),
              Text('Search products', style: TextStyle(color: Colors.black54)),
            ],
          ),
        ),
        SizedBox(
          height: 40,
          child: ListView.separated(
            scrollDirection: Axis.horizontal,
            padding: const EdgeInsets.symmetric(horizontal: 16),
            itemCount: categories.length,
            separatorBuilder: (context, index) => const SizedBox(width: 8),
            itemBuilder: (context, index) =>
                CategoryChip(label: categories[index], selected: index == 0),
          ),
        ),
        Expanded(
          child: GridView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: products.length,
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              childAspectRatio: 0.68,
              mainAxisSpacing: 12,
              crossAxisSpacing: 12,
            ),
            itemBuilder: (context, index) =>
                ProductCard(product: products[index]),
          ),
        ),
      ],
    );
  }
}

class CategoryChip extends StatelessWidget {
  final String label;
  final bool selected;

  const CategoryChip({super.key, required this.label, this.selected = false});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: selected ? Colors.indigo : Colors.indigo.shade50,
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        label,
        style: TextStyle(color: selected ? Colors.white : Colors.indigo),
      ),
    );
  }
}

class ProductCard extends StatelessWidget {
  final Product product;

  const ProductCard({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Expanded(child: Image.network(product.imageUrl, fit: BoxFit.cover)),
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 8, 4, 4),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: const TextStyle(fontWeight: FontWeight.w600),
                ),
                Row(
                  children: [
                    const Icon(Icons.star, size: 16, color: Colors.amber),
                    Text(' ${product.rating}'),
                  ],
                ),
                Row(
                  children: [
                    Expanded(
                      child: Text(
                        'Rs. ${product.price}',
                        style: const TextStyle(color: Colors.indigo),
                      ),
                    ),
                    IconButton(
                      onPressed: () {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(
                            content: Text('${product.name} added to cart'),
                          ),
                        );
                      },
                      icon: const Icon(Icons.add_shopping_cart),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class StatCard extends StatelessWidget {
  final String value;
  final String label;

  const StatCard({super.key, required this.value, required this.label});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 12),
      decoration: BoxDecoration(
        color: Colors.indigo.shade50,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            value,
            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          Text(label, style: const TextStyle(color: Colors.black54)),
        ],
      ),
    );
  }
}

class Product {
  final String name;
  final String category;
  final int price;
  final double rating;
  final String imageUrl;

  const Product(
    this.name,
    this.category,
    this.price,
    this.rating,
    this.imageUrl,
  );
}

const List<Product> products = [
  Product(
    'Laptop',
    'Laptops',
    185000,
    4.8,
    'https://picsum.photos/id/1/400/400',
  ),
  Product(
    'Smartphone',
    'Phones',
    95000,
    4.6,
    'https://picsum.photos/id/160/400/400',
  ),
  Product(
    'Camera',
    'Cameras',
    120000,
    4.7,
    'https://picsum.photos/id/250/400/400',
  ),
  Product(
    'Controller',
    'Games',
    12000,
    4.3,
    'https://picsum.photos/id/96/400/400',
  ),
  Product(
    'Coffee Mug',
    'Home',
    1200,
    4.9,
    'https://picsum.photos/id/30/400/400',
  ),
  Product(
    'E-reader',
    'Books',
    38000,
    4.5,
    'https://picsum.photos/id/367/400/400',
  ),
];

const List<String> categories = [
  'All',
  'Laptops',
  'Phones',
  'Cameras',
  'Games',
  'Home',
  'Books',
];

Code Examples

Example 1 — InfoRow: One Line of a Report

The four copied Rows of Lecture 004, replaced by one widget.

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Report')),
      body: const Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            InfoRow(label: 'Student', value: 'Ali'),
            InfoRow(label: 'Math', value: '80'),
            InfoRow(label: 'Science', value: '75'),
            InfoRow(label: 'English', value: '85'),
            InfoRow(label: 'Total', value: '240 / 300'),
          ],
        ),
      ),
    );
  }
}

class InfoRow extends StatelessWidget {
  final String label;
  final String value;

  const InfoRow({super.key, required this.label, required this.value});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Expanded(child: Text(label)),
          Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
        ],
      ),
    );
  }
}
Student Ali, Math 80, Science 75, English 85 and Total 240 / 300, labels left and values right
Example 1

Example 2 — An Optional Action With Collection if

action is a nullable String? with no default. if (action != null) inside the children list adds the button only when there is an action — collection if from the Dart course. action! tells Dart the value is not null here.

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('Sections')),
      body: const Column(
        children: [
          SectionTitle(title: 'Popular'),
          SectionTitle(title: 'New arrivals', action: 'See all'),
          SectionTitle(title: 'On sale', action: 'View 12'),
        ],
      ),
    );
  }
}

class SectionTitle extends StatelessWidget {
  final String title;
  final String? action;

  const SectionTitle({super.key, required this.title, this.action});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [
          Expanded(
            child: Text(
              title,
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
          ),
          if (action != null)
            TextButton(onPressed: () {}, child: Text(action!)),
        ],
      ),
    );
  }
}
Three section headings; New arrivals and On sale have See all and View 12 buttons
Example 2

Example 3 — ContactTile With a Default Value

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('Contacts')),
      body: ListView(
        children: const [
          ContactTile(name: 'Ali Raza', phone: '0300 1234567'),
          ContactTile(name: 'Sara Khan', phone: '0321 7654321', favorite: true),
          ContactTile(name: 'Zayan Ahmed', phone: '0333 1112223'),
        ],
      ),
    );
  }
}

class ContactTile extends StatelessWidget {
  final String name;
  final String phone;
  final bool favorite;

  const ContactTile({
    super.key,
    required this.name,
    required this.phone,
    this.favorite = false,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CircleAvatar(child: Text(name[0])),
      title: Text(name),
      subtitle: Text(phone),
      trailing: Icon(
        favorite ? Icons.star : Icons.star_border,
        color: favorite ? Colors.amber : null,
      ),
    );
  }
}
Three contacts with initials and phone numbers; Sara's star is filled amber
Example 3

Example 4 — Three Tabs

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.teal),
      home: DefaultTabController(
        length: 3,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('Weather'),
            bottom: const TabBar(
              tabs: [
                Tab(text: 'Lahore'),
                Tab(text: 'Karachi'),
                Tab(text: 'Quetta'),
              ],
            ),
          ),
          body: const TabBarView(
            children: [
              Center(child: Text('30°C', style: TextStyle(fontSize: 48))),
              Center(child: Text('34°C', style: TextStyle(fontSize: 48))),
              Center(child: Text('18°C', style: TextStyle(fontSize: 48))),
            ],
          ),
        ),
      ),
    );
  }
}
A Weather app bar with Lahore, Karachi and Quetta tabs; Lahore selected showing 30°C
Example 4

Example 5 — Shopping Cart Rows

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.green),
      home: const CartScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    int total = 3 * 120 + 5 * 30 + 1 * 450;

    return Scaffold(
      appBar: AppBar(title: const Text('Cart')),
      body: Column(
        children: [
          const CartItemTile(name: 'Notebook', price: 120, quantity: 3),
          const CartItemTile(name: 'Pen', price: 30, quantity: 5),
          const CartItemTile(name: 'Geometry box', price: 450, quantity: 1),
          const Divider(),
          ListTile(
            title: const Text(
              'Total',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            trailing: Text(
              'Rs. $total',
              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
            ),
          ),
        ],
      ),
    );
  }
}

class CartItemTile extends StatelessWidget {
  final String name;
  final int price;
  final int quantity;

  const CartItemTile({
    super.key,
    required this.name,
    required this.price,
    required this.quantity,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CircleAvatar(child: Text('$quantity')),
      title: Text(name),
      subtitle: Text('Rs. $price each'),
      trailing: Text('Rs. ${price * quantity}'),
    );
  }
}
Notebook, Pen and Geometry box rows with quantities and line totals, and a Total Rs. 960 row
Example 5

Example 6 — A Default Color

color defaults to indigo; the savings card passes teal.

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Bank')),
      body: const Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          spacing: 16,
          children: [
            BalanceCard(owner: 'Zayan', balance: 25000),
            BalanceCard(owner: 'Savings', balance: 120000, color: Colors.teal),
          ],
        ),
      ),
    );
  }
}

class BalanceCard extends StatelessWidget {
  final String owner;
  final int balance;
  final Color color;

  const BalanceCard({
    super.key,
    required this.owner,
    required this.balance,
    this.color = Colors.indigo,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(owner, style: const TextStyle(color: Colors.white70)),
          Text(
            'Rs. $balance',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 28,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }
}
An indigo Zayan Rs. 25000 card above a teal Savings Rs. 120000 card
Example 6

Common Beginner Mistakes

1. Forgetting a required argument

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

void main() {
  runApp(const MaterialApp(home: Scaffold(body: PriceTag())));
}

class PriceTag extends StatelessWidget {
  final int price;

  const PriceTag({super.key, required this.price});

  @override
  Widget build(BuildContext context) => Text('Rs. $price');
}
output
error • The named parameter 'price' is required, but there's no corresponding argument. Try adding the required argument • lib/main.dart:4:49 • missing_required_argument

The editor tells you exactly which value is missing: PriceTag(price: 120).

2. A field that is not final

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

void main() {
  runApp(const MaterialApp(home: Scaffold(body: PriceTag(price: 120))));
}

class PriceTag extends StatelessWidget {
  int price;

  PriceTag({super.key, required this.price});

  @override
  Widget build(BuildContext context) => Text('Rs. $price');
}
output
error • The constructor being called isn't a const constructor. Try removing 'const' from the constructor invocation • lib/main.dart:4:49 • const_with_non_const
warning • This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: PriceTag.price • lib/main.dart:7:7 • must_be_immutable

Widgets are immutable: they are rebuilt, not changed. Make every field final and keep the constructor const.

3. An optional parameter of a non-nullable type with no default

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

void main() {
  runApp(const MaterialApp(home: Scaffold(body: Greeting())));
}

class Greeting extends StatelessWidget {
  final String name;

  const Greeting({super.key, this.name});

  @override
  Widget build(BuildContext context) => Text('Hello, $name');
}
output
error • A value of type 'Null' can't be assigned to a parameter of type 'String' in a const constructor. Try using a subtype, or removing the keyword 'const' • lib/main.dart:4:49 • const_constructor_param_type_mismatch
error • The parameter 'name' can't have a value of 'null' because of its type, but the implicit default value is 'null'. Try adding either an explicit non-'null' default value or the 'required' modifier • lib/main.dart:10:35 • missing_default_value_for_parameter

If a parameter may be left out, it needs a default (this.name = 'friend'), a nullable type (final String? name;), or required.

4. TabBarView without a DefaultTabController

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: const TabBarView(children: [Text('Profile'), Text('Shop')]),
    );
  }
}
A dark red error screen
The red error screen
output
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞════════════════════════
The following assertion was thrown building
KeyedSubtree-[GlobalKey#a17c5]:
No TabController for TabBarView.
When creating a TabBarView, you must either provide an explicit
TabController using the "controller" property, or you must ensure
that there is a DefaultTabController above the TabBarView.
In this case, there was neither an explicit controller nor a
default controller.

Wrap the Scaffold that holds the TabBar and TabBarView in a DefaultTabController, and make its length match the number of tabs.


Best Practices

  • Make a widget as soon as a piece repeats, or as soon as a build() method no longer fits on your screen.
  • Name widgets after what they are: ProductCard, StatCard, MenuTile.
  • Pass the data a widget needs, not more — a whole Product when it uses most fields, single values otherwise.
  • Give every reusable widget a const constructor and final fields.
  • Keep data (products) separate from widgets, so the same data can feed a list, a grid or a detail page.

Mini Challenge

Write a DealCard widget with a name, a price and an optional salePrice (int?). Show four of them in a two-column grid. Every card is a rounded light-indigo box with the name and price at the bottom-left. When salePrice is given, the card shows Rs. 160000 (was 185000) and a red SALE label in its top-right corner; otherwise just the price. Use: Laptop 185000 → 160000, Camera 120000, Controller 12000 → 9500, Coffee Mug 1200.

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Deals')),
      body: GridView.count(
        crossAxisCount: 2,
        padding: const EdgeInsets.all(16),
        mainAxisSpacing: 12,
        crossAxisSpacing: 12,
        children: const [
          DealCard(name: 'Laptop', price: 185000, salePrice: 160000),
          DealCard(name: 'Camera', price: 120000),
          DealCard(name: 'Controller', price: 12000, salePrice: 9500),
          DealCard(name: 'Coffee Mug', price: 1200),
        ],
      ),
    );
  }
}

class DealCard extends StatelessWidget {
  final String name;
  final int price;
  final int? salePrice;

  const DealCard({
    super.key,
    required this.name,
    required this.price,
    this.salePrice,
  });

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Positioned.fill(
          child: Container(
            padding: const EdgeInsets.all(12),
            alignment: Alignment.bottomLeft,
            decoration: BoxDecoration(
              color: Colors.indigo.shade50,
              borderRadius: BorderRadius.circular(16),
            ),
            child: Text(
              salePrice == null
                  ? '$name\nRs. $price'
                  : '$name\nRs. $salePrice (was $price)',
              style: const TextStyle(fontWeight: FontWeight.w600),
            ),
          ),
        ),
        if (salePrice != null)
          Positioned(
            top: 8,
            right: 8,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
              decoration: BoxDecoration(
                color: Colors.red,
                borderRadius: BorderRadius.circular(8),
              ),
              child: const Text(
                'SALE',
                style: TextStyle(color: Colors.white, fontSize: 12),
              ),
            ),
          ),
      ],
    );
  }
}
Four deal cards; Laptop and Controller have red SALE labels and show their old prices
Mini challenge solution

How it works

  1. final int? salePrice; with no required makes the sale price optional.
  2. The text uses the conditional operator: salePrice == null ? … : ….
  3. if (salePrice != null) Positioned(...) adds the label to the Stack only for deals.

Practice Questions

Beginner

  1. Write a GreetingText widget that takes a name and shows Hello, <name>! in 24-pixel text. Show it for Ali, Sara and Zayan.
  2. Write a ColorBox widget that takes a color and a label. Show red, green and blue boxes, 100 pixels tall, in a Row of equal widths.
  3. Add a required icon parameter to StatCard and show it above the value.

Application

  1. Write a PriceTag widget that takes an int price and shows Rs. <price> in a green pill. Show it for 120, 1200 and 95000.
  2. Write a StudentTile widget that takes a whole Student object. Use it in a ListView.builder for Ali 240, Sara 275 and Zayan 198.
  3. Write a WeatherCard widget with a city and celsius. Cards of 25 °C and above are orange with a sun; colder ones blue with a snowflake. Show four in a grid.
  4. Add a third tab, Cart (with Icons.shopping_cart), that shows Your cart is empty.

Challenge

  1. Rebuild Zayan's bank screen from reusable widgets: a BalanceCard (owner, balance), three ActionButtons (icon, label) and a PaymentTile (a Payment object) in a list. Calculate the balance from the payments.

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: const Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            GreetingText(name: 'Ali'),
            GreetingText(name: 'Sara'),
            GreetingText(name: 'Zayan'),
          ],
        ),
      ),
    );
  }
}

class GreetingText extends StatelessWidget {
  final String name;

  const GreetingText({super.key, required this.name});

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $name!', style: const TextStyle(fontSize: 24));
  }
}
Hello, Ali!, Hello, Sara! and Hello, Zayan! centered
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: const Padding(
        padding: EdgeInsets.all(16),
        child: Row(
          spacing: 8,
          children: [
            Expanded(
              child: ColorBox(color: Colors.red, label: 'Red'),
            ),
            Expanded(
              child: ColorBox(color: Colors.green, label: 'Green'),
            ),
            Expanded(
              child: ColorBox(color: Colors.blue, label: 'Blue'),
            ),
          ],
        ),
      ),
    );
  }
}

class ColorBox extends StatelessWidget {
  final Color color;
  final String label;

  const ColorBox({super.key, required this.color, required this.label});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 100,
      color: color,
      alignment: Alignment.center,
      child: Text(label, style: const TextStyle(color: Colors.white)),
    );
  }
}
Red, green and blue labelled boxes side by side
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: const Padding(
        padding: EdgeInsets.all(16),
        child: Row(
          spacing: 12,
          children: [
            Expanded(
              child: StatCard(icon: Icons.school, value: '2', label: 'Courses'),
            ),
            Expanded(
              child: StatCard(
                icon: Icons.play_circle,
                value: '149',
                label: 'Lectures',
              ),
            ),
            Expanded(
              child: StatCard(
                icon: Icons.workspace_premium,
                value: '1',
                label: 'Certificate',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class StatCard extends StatelessWidget {
  final IconData icon;
  final String value;
  final String label;

  const StatCard({
    super.key,
    required this.icon,
    required this.value,
    required this.label,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 12),
      decoration: BoxDecoration(
        color: Colors.indigo.shade50,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, color: Colors.indigo),
          Text(
            value,
            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          Text(label, style: const TextStyle(color: Colors.black54)),
        ],
      ),
    );
  }
}
Three stat boxes with icons above the numbers
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: const Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          spacing: 12,
          children: [
            PriceTag(price: 120),
            PriceTag(price: 1200),
            PriceTag(price: 95000),
          ],
        ),
      ),
    );
  }
}

class PriceTag extends StatelessWidget {
  final int price;

  const PriceTag({super.key, required this.price});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
      decoration: BoxDecoration(
        color: Colors.green,
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        'Rs. $price',
        style: const TextStyle(
          color: Colors.white,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}
Three green price pills: Rs. 120, Rs. 1200, Rs. 95000
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 StudentsScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    List<Student> students = const [
      Student('Ali', 240),
      Student('Sara', 275),
      Student('Zayan', 198),
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Question 5')),
      body: ListView.builder(
        itemCount: students.length,
        itemBuilder: (context, index) => StudentTile(student: students[index]),
      ),
    );
  }
}

class Student {
  final String name;
  final int marks;

  const Student(this.name, this.marks);
}

class StudentTile extends StatelessWidget {
  final Student student;

  const StudentTile({super.key, required this.student});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: CircleAvatar(child: Text(student.name[0])),
      title: Text(student.name),
      trailing: Text('${student.marks} / 300'),
    );
  }
}
Ali, Sara and Zayan rows with marks out of 300
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 WeatherScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Question 6')),
      body: GridView.count(
        crossAxisCount: 2,
        padding: const EdgeInsets.all(12),
        mainAxisSpacing: 12,
        crossAxisSpacing: 12,
        children: const [
          WeatherCard(city: 'Lahore', celsius: 30),
          WeatherCard(city: 'Karachi', celsius: 34),
          WeatherCard(city: 'Quetta', celsius: 18),
          WeatherCard(city: 'Murree', celsius: 12),
        ],
      ),
    );
  }
}

class WeatherCard extends StatelessWidget {
  final String city;
  final int celsius;

  const WeatherCard({super.key, required this.city, required this.celsius});

  @override
  Widget build(BuildContext context) {
    bool hot = celsius >= 25;
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: hot ? Colors.orange.shade100 : Colors.lightBlue.shade100,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Icon(hot ? Icons.wb_sunny : Icons.ac_unit, size: 32),
          Text(city, style: const TextStyle(fontSize: 18)),
          Text(
            '$celsius°C',
            style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }
}
Two orange cards with suns and two blue cards with snowflakes
Question 6

Question 7 — Solution

length changes to 3, and the TabBar and TabBarView each get a third child. In the full app, the third page would be a CartScreen widget.

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: DefaultTabController(
        length: 3,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('Question 7'),
            bottom: const TabBar(
              tabs: [
                Tab(icon: Icon(Icons.person), text: 'Profile'),
                Tab(icon: Icon(Icons.storefront), text: 'Shop'),
                Tab(icon: Icon(Icons.shopping_cart), text: 'Cart'),
              ],
            ),
          ),
          body: const TabBarView(
            children: [
              Center(child: Text('Profile')),
              Center(child: Text('Shop')),
              Center(child: Text('Your cart is empty')),
            ],
          ),
        ),
      ),
    );
  }
}
Profile, Shop and Cart tabs with Cart selected, showing Your cart is empty
Question 7, after tapping Cart

Question 8 — Solution

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.indigo),
      home: const BankScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    List<Payment> payments = const [
      Payment('Salary', 40000, true),
      Payment('Grocery store', 6500, false),
      Payment('Electricity bill', 4200, false),
      Payment('Transfer from Sara', 3000, true),
    ];
    int balance = 0;
    for (Payment payment in payments) {
      balance += payment.isCredit ? payment.amount : -payment.amount;
    }

    return Scaffold(
      appBar: AppBar(title: const Text('Question 8')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: BalanceCard(owner: 'Zayan', balance: balance),
          ),
          const Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ActionButton(icon: Icons.send, label: 'Send'),
              ActionButton(icon: Icons.call_received, label: 'Receive'),
              ActionButton(icon: Icons.receipt_long, label: 'Pay bills'),
            ],
          ),
          const SizedBox(height: 8),
          Expanded(
            child: ListView.builder(
              itemCount: payments.length,
              itemBuilder: (context, index) =>
                  PaymentTile(payment: payments[index]),
            ),
          ),
        ],
      ),
    );
  }
}

class Payment {
  final String title;
  final int amount;
  final bool isCredit;

  const Payment(this.title, this.amount, this.isCredit);
}

class BalanceCard extends StatelessWidget {
  final String owner;
  final int balance;

  const BalanceCard({super.key, required this.owner, required this.balance});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.indigo,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Text(
        '$owner\nRs. $balance',
        style: const TextStyle(
          color: Colors.white,
          fontSize: 24,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}

class ActionButton extends StatelessWidget {
  final IconData icon;
  final String label;

  const ActionButton({super.key, required this.icon, required this.label});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        IconButton.filledTonal(onPressed: () {}, icon: Icon(icon)),
        Text(label),
      ],
    );
  }
}

class PaymentTile extends StatelessWidget {
  final Payment payment;

  const PaymentTile({super.key, required this.payment});

  @override
  Widget build(BuildContext context) {
    Color color = payment.isCredit ? Colors.green : Colors.red;
    return ListTile(
      leading: Icon(
        payment.isCredit ? Icons.arrow_downward : Icons.arrow_upward,
        color: color,
      ),
      title: Text(payment.title),
      trailing: Text(
        '${payment.isCredit ? '+' : '-'} Rs. ${payment.amount}',
        style: TextStyle(color: color),
      ),
    );
  }
}
An indigo Zayan Rs. 32300 card, Send, Receive and Pay bills buttons, and four payments
Question 8

Quick Revision

  1. When should a piece of a screen become its own widget?
  2. Why must a StatelessWidget's fields be final?
  3. What does required do in a constructor?
  4. Name three ways to make a constructor parameter optional or safe to leave out.
  5. Why pass a whole Product to ProductCard instead of five values?
  6. Which three widgets make tabs work, and what does length mean?
  7. How do you add a widget to children only when a condition is true?
  8. Where would you move each widget in a larger project?

Key Takeaways

  • Plan a screen as a tree, and turn every repeated piece into a widget.
  • A reusable widget is a StatelessWidget with final fields and a const constructor.
  • required makes a value mandatory; = value gives a default; Type? allows null.
  • Widgets can receive whole objects, such as a Product.
  • DefaultTabController + TabBar + TabBarView put several screens in one app.
  • Collection if adds a widget only when a condition is true.

What's Next?

Congratulations — you have finished Flutter Essential Widgets! You can now build the layout of almost any screen: boxes and text, rows and columns, layers, images and buttons, scrolling lists and grids, all split into reusable widgets.

Every app in this course shows information; none of them changes while it runs — the heart button never turns red and the search bar cannot be typed into. The next step is state: StatefulWidget and setState, text input with TextField, and moving between screens with navigation. That is where your apps start to react to the user.