Boutique : passer à la caisse
Fait partie de la section Projets du Journey Flutter de Coddy. Leçon 27 sur 36.
La dernière étape de tout magasin : payer. Ici, Checkout vous remercie, vide le panier et vous ramène à la boutique. (Un vrai magasin enverrait d’abord la commande à un serveur ; vous avez vu comment faire dans la section sur le réseau.)
L’ordre est important. Lisez le total avant de vider le panier, sinon le message de remerciement affiche $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 ouvre une AlertDialog par-dessus l’écran, avec une icon, un title, une ligne content et des actions. Son bouton doit fermer la boîte de dialogue et le panier en une seule fois. Navigator.popUntil(context, (route) => route.isFirst) ferme les écrans jusqu’à ce que le premier, la boutique, soit au premier plan.
Un bouton Checkout dans un panier vide n’a aucun sens : donnez-lui donc onPressed: null. Flutter grise un bouton sans onPressed.
C’est la dernière leçon sur la boutique. Les vérifications permettent de faire ses achats pour de vrai : parcourir la boutique, ajouter des articles, modifier le panier et passer à la caisse.
Défi
DifficileÀ réaliser :
- ajouter une méthode
clear()àCartModel - sous le total, après 12 pixels, ajouter un
FilledButtonCheckoutsur toute la largeur, grisé lorsque le panier est vide - il lit le total, vide le panier et affiche un
AlertDialog: une icôneIcons.check_circleverte de taille 48, le titreThank you!, la ligneYour order of $57.50 is on its way.(avec le total réel) et unTextButtonBack to shopqui revient à la boutique
Essayez vous-même
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),
),
],
),
),
],
),
);
}
}
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Projets
1Projet : Liste de tâches
Liste de tâches : La liste des tâchesListe de tâches : Ajouter une tâche4Projet : Suivi des habitudes
Suivi des habitudes : Vos habitudesSuivi des habitudes : La semaineEntraînez-vous par vous-même : Compilateur Flutter en ligne