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

1. What You Will Learn


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

TEXT
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/
TEXT
> 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

TEXT
> 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

DART
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.

TEXT
> 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

TEXT
> 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

DART
// 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],
    ));
  }
}
TEXT
> 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

TEXT
> 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

DART
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)),
    ));
  }
}
TEXT
> 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

TEXT
> 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

DART
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')),
                ])),
            ]),
    );
  }
}
TEXT
> 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

TEXT
> 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

DART
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}'))),
      ]))),
    );
  }
}
TEXT
> 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

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

Q With so many project files, import paths are easy to get wrong?
A Use VS Code's relative path auto-completion. Convention: models/ end with model.dart, widgets/ are named after the component.
Q Cart data passed via setState to another page is lost when returning?
A Currently using callback functions. Phase 2 will use Riverpod global state management to solve this.
Q Will a 1000-item GridView lag?
A GridView.builder lazy-loads only visible items — 1000 items is no problem. If each item is complex, consider itemExtent optimization.
Q Mock data images not loading?
A The examples use placeholder URLs. In actual development, replace with placeholder: AssetImage() or Icons.
Q How to pass parameters with Navigator.push navigation and returns?
A Pass via constructor when navigating; return via Navigator.pop(context, result) and receive with await.
Q Can Phase 1 code evolve directly into Phase 2?
A Yes. The data model and Widget layers remain largely unchanged — just replace setState with Riverpod Provider.

📖 Summary


📝 Exercises

  1. Basic (difficulty ⭐): Create the project following the tutorial structure and ensure the HomePage displays 20 product cards.
  2. Intermediate (difficulty ⭐⭐): Add search functionality to the HomePage: enter keywords to filter product names, using setState to refresh the list.
  3. 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.

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏