Menu
CoddyTech
flag Ar iconالعربيةdown icon

المتجر: إتمام الشراء

جزء من قسم المشاريع في رحلة Flutter على Coddy. الدرس 27 من 36.

الخطوة الأخيرة في أي متجر هي الدفع. هنا تشكرك صفحة الدفع، وتفرغ سلة التسوق، ثم تعيدك إلى المتجر. (أما المتجر الحقيقي فيرسل الطلب أولًا إلى خادم؛ وقد رأيت كيف يحدث ذلك في قسم الشبكة.)

ترتيب الخطوات مهم. اقرأ الإجمالي قبل إفراغ السلة، وإلا فستقول رسالة الشكر $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) الشاشات حتى تصبح الشاشة الأولى، أي المتجر، في المقدمة.

لا معنى لوجود زر الدفع في سلة فارغة، لذا اجعل قيمته 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 عبر الإنترنت