Menu
CoddyTech

스토어: 장바구니 화면

Coddy Flutter 여정의 프로젝트 섹션에 포함된 레슨. 36개 중 23번째.

배지는 장바구니에 항목이 몇 개 있는지 알려 줍니다. 배지를 탭하면 장바구니 화면에서 항목을 볼 수 있어야 합니다.

배지를 버튼으로 바꾸세요. Badge를 IconButton 안에 넣어 icon으로 지정하고, onPressed에서 CartScreen을 푸시하세요. IconButton은 아이콘 주위에 자체적으로 8픽셀의 여백을 추가하므로 오른쪽 패딩은 8로 줄어듭니다.

장바구니 화면에는 생성자 데이터가 필요하지 않습니다. 장바구니는 이미 공유되고 있기 때문입니다. 모델을 감시하고 항목마다 한 행을 만듭니다.

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

이것이 공유 모델의 장점입니다. 상품 페이지는 장바구니에 쓰고, 그리드와 장바구니 화면은 장바구니를 읽으며, 세 화면 중 어느 것도 다른 화면에 데이터를 전달하지 않습니다.

challenge icon

챌린지

중급

장바구니 표시하기:

  • CartScreen을 작성하고, 제목이 Your Cart인 AppBar와 장바구니의 각 항목에 대한 ListTile이 있는 ListView를 추가하세요
  • 각 행에는 상품의 shade50 색상의 CircleAvatar와 상품 색상의 아이콘, 이름, 그리고 trailing으로 가격을 표시하세요
  • 그리드의 상단 표시줄에서 오른쪽으로 8픽셀 떨어진 곳에 Badge를 IconButton 안에 넣고, 이 버튼을 누르면 CartScreen으로 이동하게 하세요

직접 해보기

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 icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

프로젝트의 모든 레슨

직접 연습해 보세요: 온라인 Flutter 컴파일러