On this page21 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. The Plan
- 2. A Widget That Takes Values
- 3. More Reusable Pieces: ProfileHeader and MenuTile
- 4. The Profile Screen
- 5. Passing Objects: Product and ProductCard
- 6. Optional Parameters: CategoryChip
- 7. The Shop Screen
- 8. Two Screens, One App: Tabs
- 9. The Complete App
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 009 — Mini Project: Profile & Product Screens
| Category | 5 — Mini Project |
| Lecture | 009 of 009 |
| Estimated video length | 25 min |
| Target | Flutter 3.44 · Dart 3.12 |
| Level | Beginner |
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,BoxDecorationandTextStyleRow,Column,Expanded,StackandPositionedImage.network,CircleAvatar, icons, buttons andSnackBarListTile,ListView.separated,GridView.builderandSingleChildScrollView- from the Dart course: classes with
finalfields, named parameters,requiredand 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
StatelessWidgetthat receives values through its constructor - Use
requiredparameters, 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,TabBarandTabBarView - 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:
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:
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. (AStatelessWidgetmust have onlyfinalfields — the editor warns you otherwise.) required this.valuemakes 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:
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 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:
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:
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:
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'),
],
),
);
}
}
SingleChildScrollViewlets the screen scroll on small phones.SizedBox(height: 56)leaves room for the half of the avatar that hangs below the cover.- Because every
StatCardandMenuTilegets only fixed values, they can all beconst.
5. Passing Objects: Product and ProductCard
Products have five values each. Instead of five parameters, pass one Product object:
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',
),
];The list is a top-level const — written outside any class, so every screen can read it.
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 =:
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
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]),
),
),
],
);
}
}
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:
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 TabBar sits in the AppBar's bottom slot. Tap a tab — or swipe the page sideways — to
switch:


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.
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.
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)),
],
),
);
}
}
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.
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!)),
],
),
);
}
}
Example 3 — ContactTile With a Default Value
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,
),
);
}
}
Example 4 — Three Tabs
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))),
],
),
),
),
);
}
}
Example 5 — Shopping Cart Rows
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}'),
);
}
}
Example 6 — A Default Color
color defaults to indigo; the savings card passes teal.
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,
),
),
],
),
);
}
}
Common Beginner Mistakes
1. Forgetting a required argument
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');
}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_argumentThe editor tells you exactly which value is missing: PriceTag(price: 120).
2. A field that is not final
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');
}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_immutableWidgets 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
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');
}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_parameterIf 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
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')]),
);
}
}
══╡ 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
Productwhen it uses most fields, single values otherwise. - Give every reusable widget a
constconstructor andfinalfields. - 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
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),
),
),
),
],
);
}
}
How it works
final int? salePrice;with norequiredmakes the sale price optional.- The text uses the conditional operator:
salePrice == null ? … : …. if (salePrice != null) Positioned(...)adds the label to theStackonly for deals.
Practice Questions
Beginner
- Write a
GreetingTextwidget that takes anameand showsHello, <name>!in 24-pixel text. Show it for Ali, Sara and Zayan. - Write a
ColorBoxwidget that takes acolorand alabel. Show red, green and blue boxes, 100 pixels tall, in aRowof equal widths. - Add a required
iconparameter toStatCardand show it above the value.
Application
- Write a
PriceTagwidget that takes anint priceand showsRs. <price>in a green pill. Show it for 120, 1200 and 95000. - Write a
StudentTilewidget that takes a wholeStudentobject. Use it in aListView.builderfor Ali 240, Sara 275 and Zayan 198. - Write a
WeatherCardwidget with acityandcelsius. Cards of 25 °C and above are orange with a sun; colder ones blue with a snowflake. Show four in a grid. - Add a third tab,
Cart(withIcons.shopping_cart), that showsYour cart is empty.
Challenge
- Rebuild Zayan's bank screen from reusable widgets: a
BalanceCard(owner, balance), threeActionButtons (icon, label) and aPaymentTile(aPaymentobject) in a list. Calculate the balance from the payments.
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: 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));
}
}
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: 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)),
);
}
}
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: 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)),
],
),
);
}
}
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: 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,
),
),
);
}
}
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 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'),
);
}
}
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 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),
),
],
),
);
}
}
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.
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')),
],
),
),
),
);
}
}
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.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),
),
);
}
}
Quick Revision
- When should a piece of a screen become its own widget?
- Why must a
StatelessWidget's fields befinal? - What does
requireddo in a constructor? - Name three ways to make a constructor parameter optional or safe to leave out.
- Why pass a whole
ProducttoProductCardinstead of five values? - Which three widgets make tabs work, and what does
lengthmean? - How do you add a widget to
childrenonly when a condition is true? - 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
StatelessWidgetwithfinalfields and aconstconstructor. requiredmakes a value mandatory;= valuegives a default;Type?allowsnull.- Widgets can receive whole objects, such as a
Product. DefaultTabController+TabBar+TabBarViewput several screens in one app.- Collection
ifadds 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.