Menu
CoddyTech

Cửa hàng: Màn hình giỏ hàng

Thuộc phần Dự án trong hành trình Flutter của Coddy. Bài học 23 trên 36.

Huy hiệu cho biết có bao nhiêu món hàng trong giỏ. Khi chạm vào huy hiệu, màn hình giỏ hàng sẽ hiển thị các món hàng đó.

Biến huy hiệu thành nút: đặt Badge bên trong IconButton làm icon, rồi mở CartScreen trong onPressed. IconButton tự thêm 8 pixel quanh biểu tượng, nên phần đệm bên phải giảm xuống còn 8.

Màn hình giỏ hàng không cần dữ liệu trong hàm khởi tạo: giỏ hàng đã được chia sẻ. Màn hình theo dõi model và tạo một hàng cho mỗi món hàng:

final cart = context.watch<CartModel>();
...
for (Product product in cart.items)
  ListTile(
    leading: CircleAvatar(...),
    title: Text(product.name),
    trailing: Text(money.format(product.price)),
  ),

Đây là ưu điểm của model dùng chung: trang sản phẩm ghi dữ liệu vào giỏ hàng, trang lưới và màn hình giỏ hàng đọc dữ liệu, và không màn hình nào trong ba màn hình truyền thông tin cho màn hình khác.

challenge icon

Thử thách

Trung bình

Hiển thị giỏ hàng:

  • viết một CartScreen có AppBar với tiêu đề Your Cart và ListView chứa một ListTile cho mỗi mặt hàng trong giỏ hàng
  • mỗi hàng: một CircleAvatar có màu shade50 của sản phẩm, biểu tượng mang màu của sản phẩm, tên và giá ở vị trí trailing
  • trên thanh trên cùng của lưới, đặt Badge bên trong IconButton để mở CartScreen, cách mép phải 8 pixel

Tự làm thử

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

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

final money = NumberFormat.currency(locale: 'en_US', symbol: '\$');

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
      ),
      home: const ShopScreen(),
    );
  }
}

class Product {
  const Product({
    required this.name,
    required this.price,
    required this.icon,
    required this.color,
    required this.description,
  });

  final String name;
  final double price;
  final IconData icon;
  final MaterialColor color;
  final String description;
}

const List<Product> products = [
  Product(
    name: 'Headphones',
    price: 59,
    icon: Icons.headphones,
    color: Colors.indigo,
    description: 'Wireless headphones with 30 hours of battery.',
  ),
  Product(
    name: 'Watch',
    price: 129,
    icon: Icons.watch,
    color: Colors.teal,
    description: 'A smart watch that counts your steps.',
  ),
  Product(
    name: 'Backpack',
    price: 45.5,
    icon: Icons.backpack,
    color: Colors.orange,
    description: 'Room for a laptop, a lunch and a jacket.',
  ),
  Product(
    name: 'Mug',
    price: 12,
    icon: Icons.coffee,
    color: Colors.brown,
    description: 'Holds a big coffee and keeps it warm.',
  ),
  Product(
    name: 'Camera',
    price: 1299,
    icon: Icons.photo_camera,
    color: Colors.blueGrey,
    description: 'A mirrorless camera for sharp photos.',
  ),
  Product(
    name: 'Sneakers',
    price: 89.99,
    icon: Icons.directions_run,
    color: Colors.red,
    description: 'Light running shoes for every day.',
  ),
];

class CartModel extends ChangeNotifier {
  final List<Product> _items = [];

  List<Product> get items => _items;
  int get count => _items.length;

  void add(Product product) {
    _items.add(product);
    notifyListeners();
  }
}

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

  @override
  Widget build(BuildContext context) {
    final count = context.watch<CartModel>().count;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Coddy Shop'),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 24),
            child: Badge(
              isLabelVisible: count > 0,
              label: Text('$count'),
              child: const Icon(Icons.shopping_cart),
            ),
          ),
        ],
      ),
      body: GridView.count(
        crossAxisCount: 2,
        padding: const EdgeInsets.all(16),
        mainAxisSpacing: 12,
        crossAxisSpacing: 12,
        childAspectRatio: 0.8,
        children: [
          for (Product product in products) ProductCard(product: product),
        ],
      ),
    );
  }
}

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

  final Product product;

  @override
  Widget build(BuildContext context) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: () {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => ProductScreen(product: product),
            ),
          );
        },
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Expanded(
              child: Container(
                width: double.infinity,
                alignment: Alignment.center,
                color: product.color.shade50,
                child: Icon(product.icon, size: 56, color: product.color),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    money.format(product.price),
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Theme.of(context).colorScheme.primary,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ProductScreen extends StatelessWidget {
  const ProductScreen({super.key, required this.product});

  final Product product;

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Scaffold(
      appBar: AppBar(title: Text(product.name)),
      body: ListView(
        padding: const EdgeInsets.all(24),
        children: [
          Container(
            height: 240,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: product.color.shade50,
              borderRadius: BorderRadius.circular(24),
            ),
            child: Icon(product.icon, size: 120, color: product.color),
          ),
          const SizedBox(height: 24),
          Text(
            money.format(product.price),
            style: textTheme.headlineSmall?.copyWith(
              fontWeight: FontWeight.bold,
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
          const SizedBox(height: 12),
          Text(product.description, style: textTheme.bodyLarge),
          const SizedBox(height: 32),
          FilledButton.icon(
            onPressed: () {
              context.read<CartModel>().add(product);
              ScaffoldMessenger.of(context).hideCurrentSnackBar();
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Added ${product.name} to your cart')),
              );
            },
            icon: const Icon(Icons.add_shopping_cart),
            label: const Text('Add to cart'),
          ),
        ],
      ),
    );
  }
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Dự án

Tự luyện tập: Trình biên dịch Flutter online