Menu
CoddyTech

Магазин: оформление заказа

Часть раздела Проекты путешествия по Flutter на Coddy. Урок 27 из 36.

Последний шаг в любом магазине — оплата. Здесь Checkout благодарит вас, очищает корзину и возвращает вас в магазин. (Настоящий магазин сначала отправил бы заказ на сервер; вы узнали, как это делается, в разделе о сети.)

Порядок действий важен. Сначала прочитайте итоговую сумму, а потом очищайте корзину, иначе в благодарственном сообщении будет указано $0.00:

void _checkout(BuildContext context) {
  final cart = context.read<CartModel>();
  final total = money.format(cart.total);
  cart.clear();
  showDialog(context: context, builder: (context) => AlertDialog(...));
}

showDialog открывает поверх экрана AlertDialog со значком icon, заголовком title, строкой содержимого content и действиями actions. Кнопка должна за один раз закрыть диалоговое окно и очистить корзину. Navigator.popUntil(context, (route) => route.isFirst) закрывает экраны, пока первым не окажется экран магазина.

Кнопка Checkout в пустой корзине не имеет смысла, поэтому в этом случае задайте ей onPressed: null: Flutter делает кнопку без onPressed неактивной.

Это последний урок о магазине. Проверки имитируют настоящий поход за покупками: просматривайте товары, добавляйте их, меняйте содержимое корзины и оформляйте заказ.

challenge icon

Задание

Сложно

Проверьте:

  • добавьте метод clear() в CartModel
  • под итоговой суммой, с отступом 12 пикселей, добавьте кнопку FilledButton на всю ширину с надписью Checkout, неактивную, пока корзина пуста
  • кнопка считывает итоговую сумму, очищает корзину и показывает AlertDialog: зелёный значок Icons.check_circle размером 48, заголовок Thank you!, строку Your order of $57.50 is on its way. (с реальной итоговой суммой) и кнопку TextButton с надписью Back to shop, которая возвращает в магазин

Попробуйте сами

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 Map<Product, int> _quantities = {};

  List<Product> get items => _quantities.keys.toList();
  int quantityOf(Product product) => _quantities[product] ?? 0;
  int get count => _quantities.values.fold(0, (sum, quantity) => sum + quantity);
  double get total => items.fold(0, (sum, product) => sum + product.price * quantityOf(product));

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

  void remove(Product product) {
    if (quantityOf(product) > 1) {
      _quantities[product] = quantityOf(product) - 1;
    } else {
      _quantities.remove(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: cart.items.isEmpty
          ? Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    Icons.shopping_cart_outlined,
                    size: 64,
                    color: Theme.of(context).colorScheme.outline,
                  ),
                  const SizedBox(height: 12),
                  const Text('Your cart is empty'),
                ],
              ),
            )
          : 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),
                    subtitle: Text('${cart.quantityOf(product)} × ${money.format(product.price)}'),
                    trailing: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Text(money.format(product.price * cart.quantityOf(product))),
                        IconButton(
                          onPressed: () => cart.remove(product),
                          icon: const Icon(Icons.remove_circle_outline),
                        ),
                      ],
                    ),
                  ),
              ],
            ),
    );
  }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Проекты

Потренируйтесь самостоятельно: Онлайн-компилятор Flutter