스토어: 결제
Coddy Flutter 여정의 프로젝트 섹션에 포함된 레슨. 36개 중 27번째.
모든 상점에서 마지막 단계는 결제입니다. 여기서 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는 화면 위에 icon, title, content 줄, actions가 있는 AlertDialog를 엽니다. 버튼을 누르면 대화상자를 닫고 장바구니도 한 번에 비워야 합니다. Navigator.popUntil(context, (route) => route.isFirst)는 첫 화면인 상점이 맨 위에 표시될 때까지 화면을 닫습니다.
장바구니가 비어 있을 때 Checkout 버튼이 의미가 없으므로, 그때는 onPressed: null로 설정하세요. Flutter는 onPressed가 없는 버튼을 회색으로 표시합니다.
이것으로 상점의 마지막 수업을 마칩니다. 실제로 쇼핑하며 둘러보고, 상품을 추가하고, 장바구니를 변경한 뒤 결제해 보세요.
챌린지
어려움다음 항목을 확인하세요:
CartModel에clear()메서드 추가- 합계 아래에 12픽셀 간격을 두고 전체 너비의
FilledButtonCheckout을 추가하고, 장바구니가 비어 있으면 비활성화 - 합계를 읽고 장바구니를 비운 다음
AlertDialog표시: 크기가 48인 녹색Icons.check_circle, 제목Thank you!, 문구Your order of $57.50 is on its way.(실제 합계 표시), 그리고 상점으로 돌아가는TextButtonBack 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 컴파일러