Menu
CoddyTech

Tienda: Cantidades

Parte de la sección Proyectos del Journey de Flutter de Coddy. Lección 25 de 36.

Si agregas la taza dos veces, el carrito la muestra dos veces. Los carritos reales la muestran una sola vez, con «2 ×». El carrito debería almacenar cuántas unidades hay de cada producto, lo cual es un Map de producto a cantidad:

final Map<Product, int> _quantities = {};

int quantityOf(Product product) => _quantities[product] ?? 0;

void add(Product product) {
  _quantities[product] = quantityOf(product) + 1;
  notifyListeners();
}

Al leer una clave que el mapa no tiene, se obtiene null, así que ?? 0 convierte «no está en el carrito» en 0. Un mapa recuerda el orden en que se agregaron las claves, así que las filas mantienen el orden en que compraste.

Los getters también cambian. items pasa a ser las claves del mapa, count suma las cantidades y total multiplica cada precio por su cantidad:

List<Product> get items => _quantities.keys.toList();
int get count => _quantities.values.fold(0, (sum, quantity) => sum + quantity);

La pantalla del carrito sigue recorriendo cart.items. Como los getters conservaron sus nombres, solo cambian el modelo y la fila.

challenge icon

Desafío

Difícil

Cuenta cada producto una vez:

  • en CartModel, reemplaza la lista por el mapa _quantities, quantityOf y los nuevos add, items y count
  • haz que total sume product.price * quantityOf(product) por cada artículo
  • en cada fila del carrito, muestra 2 × $12.00 como subtitle y el total de la propia fila (precio por cantidad) como trailing

Pruébalo tú mismo

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;
  double get total => _items.fold(0, (sum, product) => sum + product.price);

  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: 8),
            child: IconButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const CartScreen()),
                );
              },
              icon: 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'),
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final cart = context.watch<CartModel>();
    return Scaffold(
      appBar: AppBar(title: const Text('Your Cart')),
      bottomNavigationBar: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              Text('Total', style: Theme.of(context).textTheme.titleLarge),
              const Spacer(),
              Text(
                money.format(cart.total),
                style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
              ),
            ],
          ),
        ),
      ),
      body: ListView(
        children: [
          for (Product product in cart.items)
            ListTile(
              leading: CircleAvatar(
                backgroundColor: product.color.shade50,
                child: Icon(product.icon, color: product.color),
              ),
              title: Text(product.name),
              trailing: Text(money.format(product.price)),
            ),
        ],
      ),
    );
  }
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Proyectos

Practica por tu cuenta: Compilador de Flutter online