חנות: תשלום
חלק מהיחידה פרויקטים במסלול ה-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.
זהו השיעור האחרון של החנות. הבדיקות מדמות קניות אמיתיות: עיון, הוספה, שינוי העגלה ותשלום.
אתגר
קשהבדוק:
- הוסף מתודה
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),
),
],
),
),
],
),
);
}
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה פרויקטים
תרגלו בעצמכם: קומפיילר Flutter אונליין