Flutter: Phase 1 Comprehensive Practice
Learning without practice is like buying ingredients but never cooking — this lesson turns all the knowledge from the first 6 lessons into a full ShopApp feast.
📋 Prerequisites: You need to have completed the following first
- Lesson 1: Flutter Introduction and Environment Setup
- Lesson 2: Dart Language Crash Course
- Lesson 3: Widget Fundamentals
- Lesson 4: Layout System
- Lesson 5: StatefulWidget and Interaction
- Lesson 6: Material Design and Common Components
1. What You Will Learn
- Project structure planning: models / widgets / screens layering under lib/
- Product data model: id / name / price(USD) / imageUrl / rating
- Home page GridView displaying 1000+ product cards (mock data)
- Cart page: StatefulWidget managing product quantities and total price calculation
- Product detail page: SliverAppBar + size selection BottomSheet
2. A Real Story from Zero to One
(1) The Pain: Fragmented Knowledge
Bob finished the first 6 lessons — he can write Widgets, create layouts, and handle interactions. But when assembling this knowledge into a complete application, he didn't know where to start. How to organize files? How to pass data? How to navigate between pages? It was like having puzzle pieces but being unable to complete the picture.
(2) The Project-Based Solution
By building the ShopApp starter edition, fragmented knowledge is connected into a complete chain: data model → list display → interaction → page navigation → state management.
(3) The Result: From "Can Write Components" to "Can Build Projects"
After completing the ShopApp starter edition, Bob gained the ability to independently build complete Flutter applications. Phase 2 only requires replacing the state management and network layers.
3. Project Structure Planning
(1) Directory Structure
shop_app/
├── lib/
│ ├── main.dart # App entry point
│ ├── models/
│ │ └── product.dart # Product data model
│ ├── widgets/
│ │ ├── product_card.dart # Reusable product card
│ │ └── quantity_selector.dart
│ ├── screens/
│ │ ├── home_page.dart # Product grid
│ │ ├── detail_page.dart # Product detail
│ │ └── cart_page.dart # Shopping cart
│ └── data/
│ └── mock_products.dart # Mock data generator
├── pubspec.yaml
└── test/
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
(2) Layering Principles
| Layer | Responsibility | Dependency Direction |
|---|---|---|
| models | Pure data definitions, no UI | No dependencies |
| widgets | Reusable UI components | Depends on models |
| screens | Page-level Widgets | Depends on models + widgets |
| data | Data providers (mock/API) | Depends on models |
| main | App entry + routing | Depends on screens |
4. Data Model and Mock Data
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Product data model
class Product {
final int id;
final String name;
final double price;
final String imageUrl;
final double rating;
final int reviewCount;
final String category;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
this.rating = 0.0,
this.reviewCount = 0,
this.category = 'General',
});
}
This Product class will be used in all code blocks in this lesson.
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: Mock data generator
// Product class definition shown above
class MockProducts {
static final List<String> _names = [
'Pro Laptop 16"', 'Wireless Earbuds', 'Smart Watch Pro',
'Cotton T-Shirt', 'Running Shoes', 'Backpack Ultra',
'Coffee Maker', 'Desk Lamp LED', 'Yoga Mat Premium',
'Novel Collection', 'Wireless Mouse', 'USB-C Hub',
];
static final List<String> _categories = [
'Electronics', 'Clothing', 'Home', 'Books', 'Sports',
];
static List<Product> generate({int count = 1000}) {
return List.generate(count, (i) => Product(
id: i + 1,
name: _names[i % _names.length],
price: (9.99 + (i % 50) * 10).toDouble(),
imageUrl: 'https://cdn.shopapp.com/products/item_${i + 1}.jpg',
rating: (3.0 + (i % 20) * 0.1),
reviewCount: (i % 5) * 100,
category: _categories[i % _categories.length],
));
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
5. Home Page: Product Grid
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: HomePage implementation
import 'package:flutter/material.dart';
// Product class definition shown above
// Simplified class definition
class CartItem {
final Product product;
final int quantity;
const CartItem({required this.product, required this.quantity});
CartItem copyWith({Product? product, int? quantity}) =>
CartItem(product: product ?? this.product, quantity: quantity ?? this.quantity);
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final List<Product> _products = MockProducts.generate(count: 1000);
String _selectedCategory = 'All';
List<Product> get _filtered => _selectedCategory == 'All'
? _products
: _products.where((p) => p.category == _selectedCategory).toList();
@override
Widget build(BuildContext context) {
final categories = ['All', ...MockProducts._categories.toSet()];
return Scaffold(
appBar: AppBar(
title: const Text('ShopApp'),
actions: [
Stack(
alignment: Alignment.center,
children: [
IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () => _navigateToCart()),
if (_cartItems.isNotEmpty)
Positioned(right: 4, top: 4,
child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
child: Text('${_cartItems.length}', style: const TextStyle(fontSize: 9, color: Colors.white)))),
],
),
],
),
body: Column(
children: [
// Category filter chips
SizedBox(
height: 48,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
children: categories.map((cat) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ChoiceChip(
label: Text(cat),
selected: cat == _selectedCategory,
onSelected: (_) => setState(() => _selectedCategory = cat),
),
)).toList(),
),
),
// Product grid
Expanded(
child: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 0.68,
),
itemCount: _filtered.length,
itemBuilder: (context, index) => ProductCard(
key: ValueKey(_filtered[index].id),
product: _filtered[index],
onAddToCart: () => _addToCart(_filtered[index]),
onTap: () => _navigateToDetail(_filtered[index]),
),
),
),
],
),
);
}
final List<CartItem> _cartItems = [];
void _addToCart(Product product) {
setState(() {
final idx = _cartItems.indexWhere((c) => c.product.id == product.id);
if (idx >= 0) {
_cartItems[idx] = _cartItems[idx].copyWith(quantity: _cartItems[idx].quantity + 1);
} else {
_cartItems.add(CartItem(product: product, quantity: 1));
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart'), duration: const Duration(seconds: 1)),
);
}
void _navigateToCart() {
Navigator.push(context, MaterialPageRoute(
builder: (_) => CartPage(items: _cartItems, onUpdate: (items) => setState(() => _cartItems.clear()..addAll(items))),
));
}
void _navigateToDetail(Product product) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => DetailPage(product: product, onAddToCart: () => _addToCart(product)),
));
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
6. Cart Page
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: CartPage implementation
import 'package:flutter/material.dart';
// Product class definition shown above
class CartItem {
final Product product;
final int quantity;
const CartItem({required this.product, required this.quantity});
double get total => product.price * quantity;
CartItem copyWith({Product? product, int? quantity}) =>
CartItem(product: product ?? this.product, quantity: quantity ?? this.quantity);
}
class CartPage extends StatefulWidget {
final List<CartItem> items;
final ValueChanged<List<CartItem>> onUpdate;
const CartPage({super.key, required this.items, required this.onUpdate});
@override
State<CartPage> createState() => _CartPageState();
}
class _CartPageState extends State<CartPage> {
late List<CartItem> _items;
@override
void initState() {
super.initState();
_items = List.from(widget.items);
}
double get _total => _items.fold(0.0, (sum, item) => sum + item.total);
void _updateQty(int index, int qty) {
setState(() {
if (qty <= 0) _items.removeAt(index);
else _items[index] = _items[index].copyWith(quantity: qty);
});
widget.onUpdate(_items);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Cart (${_items.length})')),
body: _items.isEmpty
? const Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.shopping_cart_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('Your cart is empty', style: TextStyle(fontSize: 18, color: Colors.grey)),
]))
: Column(children: [
Expanded(child: ListView.separated(
itemCount: _items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, i) {
final item = _items[i];
return ListTile(
leading: Image.network(item.product.imageUrl, width: 50, height: 50, fit: BoxFit.cover),
title: Text(item.product.name),
subtitle: Text('$${item.total.toStringAsFixed(2)}'),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(icon: const Icon(Icons.remove), onPressed: () => _updateQty(i, item.quantity - 1)),
Text('${item.quantity}'),
IconButton(icon: const Icon(Icons.add), onPressed: () => _updateQty(i, item.quantity + 1)),
]),
);
},
)),
// Total bar
Container(padding: const EdgeInsets.all(16), color: Colors.grey[100],
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text('Total: $${_total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
FilledButton(onPressed: () {}, child: const Text('Checkout')),
])),
]),
);
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
7. Product Detail Page
▶ Example
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
: DetailPage implementation
import 'package:flutter/material.dart';
// Product class definition shown above
class DetailPage extends StatelessWidget {
final Product product;
final VoidCallback onAddToCart;
const DetailPage({super.key, required this.product, required this.onAddToCart});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(slivers: [
SliverAppBar(expandedHeight: 300, pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(product.name, style: const TextStyle(fontSize: 14, shadows: [Shadow(color: Colors.black54)])),
background: Image.network(product.imageUrl, fit: BoxFit.cover),
),
),
SliverToBoxAdapter(child: Padding(padding: const EdgeInsets.all(16), child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.category, style: TextStyle(color: Colors.grey[600])),
const SizedBox(height: 4),
Row(children: [
Text('$${product.price.toStringAsFixed(2)}', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.green)),
const SizedBox(width: 8),
Icon(Icons.star, color: Colors.amber[700], size: 16),
Text(' ${product.rating} (${product.reviewCount})'),
]),
const SizedBox(height: 16),
const Text('Description', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
const Text('Premium quality product. Free shipping on orders over $50. USD settlement supported.'),
],
))),
]),
bottomNavigationBar: SafeArea(child: Padding(padding: const EdgeInsets.all(12), child: Row(children: [
Expanded(child: FilledButton.icon(onPressed: onAddToCart,
icon: const Icon(Icons.shopping_cart), label: const Text('Add to Cart - $${product.price}'))),
]))),
);
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
8. Complete Example: ShopApp Starter Edition main.dart
import 'package:flutter/material.dart';
import 'models/product.dart';
import 'data/mock_products.dart';
import 'screens/home_page.dart';
void main() => runApp(const ShopAppStarter);
const ShopAppStarter = ShopApp();
class ShopApp extends StatelessWidget {
const ShopApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ShopApp Starter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.
❓ FAQ
model.dart, widgets/ are named after the component.placeholder: AssetImage() or Icons.Navigator.pop(context, result) and receive with await.📖 Summary
- Project layering: models (data) / widgets (components) / screens (pages) / data (data sources)
- Mock data for quick UI validation, replace with real API later
- GridView.builder lazy loading supports thousand-level product lists
- Cart managed with StatefulWidget + setState, data passed via callbacks
- SliverAppBar creates immersive detail page experience
📝 Exercises
- Basic (difficulty ⭐): Create the project following the tutorial structure and ensure the HomePage displays 20 product cards.
- Intermediate (difficulty ⭐⭐): Add search functionality to the HomePage: enter keywords to filter product names, using setState to refresh the list.
- Challenge (difficulty ⭐⭐⭐): Implement the complete three-page navigation flow: Home → Detail → Cart, with the AppBar badge number auto-updating when returning from the cart after modifying quantities.