Project: E-commerce App
សាងសង់ E-commerce App
បង្កើត Online Shop app មួយពេញលេញដោយប្រើ Provider state management។ Project នេះបង្ហាញពី advanced architecture patterns, shopping cart logic, product catalog, និង checkout flow។
�️ E-commerce Architecture Theory
App Architecture (MVVM + Provider):
E-commerce App Architecture
┌──────────────────────────────────────────┐
│ View Layer (Screens) │
│ - ProductsScreen (Grid/List view) │
│ - ProductDetailScreen │
│ - CartScreen (Shopping cart) │
│ - CheckoutScreen (Payment flow) │
│ - OrderHistoryScreen │
└─────────────┬────────────────────────────┘
│
↓ User Actions & UI Updates
│
┌─────────────┴────────────────────────────┐
│ ViewModel Layer (Providers) │
│ - Cart (ChangeNotifier) │
│ • addItem(), removeItem() │
│ • updateQuantity() │
│ • calculateTotal() │
│ - Products (ChangeNotifier) │
│ • fetchProducts() │
│ • searchProducts() │
│ • filterByCategory() │
└─────────────┬────────────────────────────┘
│
↓ Business Logic
│
┌─────────────┴────────────────────────────┐
│ Model Layer (Data) │
│ - Product (id, name, price, image) │
│ - CartItem (product, quantity) │
│ - Order (items, total, date) │
│ - Category, Review, etc. │
└─────────────┬────────────────────────────┘
│
↓ Data Persistence
│
┌─────────────┴────────────────────────────┐
│ Data Layer (Storage/API) │
│ - API Service (HTTP requests) │
│ - Local Storage (Cart persistence) │
│ - Image Cache │
└──────────────────────────────────────────┘
Why Provider for E-commerce?
Feature | Provider Advantage | E-commerce Use Case |
---|---|---|
Global State | Share cart across all screens | Cart badge updates automatically everywhere |
ChangeNotifier | Automatic UI updates on state change | Add to cart → UI rebuilds instantly |
Performance | Selective widget rebuilds | Only cart badge rebuilds, not entire screen |
Simplicity | No boilerplate code | Easy to add/remove products from cart |
Persistence | Easy integration with storage | Save cart to SharedPreferences |
Shopping Cart State Flow:
Cart State Management Flow
User taps "Add to Cart"
↓
ProductCard calls: cart.addItem(product)
↓
Cart Provider checks:
- Product already in cart?
├── YES → Increment quantity
└── NO → Add new CartItem
↓
notifyListeners() called
↓
All listening widgets rebuild:
- Cart badge (shows count)
- Total price (updates)
- Cart screen (if open)
↓
Save cart to SharedPreferences
↓
Show SnackBar confirmation
Performance: Only widgets using
Provider.of(context) rebuild!
Others stay as-is (efficient!)
�🎯 មុខងារ (Features)
- 📱 Product Catalog: បង្ហាញផលិតផលជា Grid/List view
- 🔍 Search & Filter: ស្វែងរក និង ត្រងតាម category
- 🛒 Shopping Cart: បន្ថែមទៅ Cart, កែប្រែ quantity, លុប items
- 💰 Price Calculation: គណនា subtotal, tax, shipping, total
- ✅ Checkout Process: Form validation, order confirmation
- 📜 Order History: ប្រវត្តិការទិញទាំងអស់
- 💾 Cart Persistence: រក្សាទុក cart នៅពេល close app
- 🔔 Real-time Updates: Cart badge updates automatically
📝 Product Model
class Product {
final String id;
final String name;
final String description;
final double price;
final String imageUrl;
final String category;
Product({
required this.id,
required this.name,
required this.description,
required this.price,
required this.imageUrl,
required this.category,
});
// JSON serialization for API/storage
Map toJson() {
return {
'id': id,
'name': name,
'description': description,
'price': price,
'imageUrl': imageUrl,
'category': category,
};
}
factory Product.fromJson(Map json) {
return Product(
id: json['id'],
name: json['name'],
description: json['description'],
price: json['price'].toDouble(),
imageUrl: json['imageUrl'],
category: json['category'],
);
}
}
🛒 Cart Model (State Management with Provider)
ChangeNotifier Pattern:
Cart extends ChangeNotifier
Why ChangeNotifier?
✅ Built into Flutter (no extra packages)
✅ notifyListeners() triggers UI updates
✅ Works perfectly with Provider
✅ Simple and lightweight
Pattern:
1. Extend ChangeNotifier
2. Make state private (_items)
3. Provide getters for read access
4. Modify state in methods
5. Call notifyListeners() after changes
class CartItem {
final Product product;
int quantity;
CartItem({
required this.product,
this.quantity = 1,
});
double get totalPrice => product.price * quantity;
}
class Cart extends ChangeNotifier {
List<CartItem> _items = [];
List<CartItem> get items => _items;
double get total {
return _items.fold(0, (sum, item) => sum + item.totalPrice);
}
void addItem(Product product) {
final index = _items.indexWhere((item) => item.product.id == product.id);
if (index >= 0) {
_items[index].quantity++;
} else {
_items.add(CartItem(product: product));
}
notifyListeners();
}
void removeItem(String productId) {
_items.removeWhere((item) => item.product.id == productId);
notifyListeners();
}
void updateQuantity(String productId, int quantity) {
final index = _items.indexWhere((item) => item.product.id == productId);
if (index >= 0) {
if (quantity > 0) {
_items[index].quantity = quantity;
} else {
_items.removeAt(index);
}
notifyListeners();
}
}
void clear() {
_items.clear();
notifyListeners();
}
int get itemCount {
return _items.fold(0, (sum, item) => sum + item.quantity);
}
// Persistence methods
Future saveToStorage() async {
final prefs = await SharedPreferences.getInstance();
final cartData = _items.map((item) => {
'productId': item.product.id,
'quantity': item.quantity,
}).toList();
await prefs.setString('cart', jsonEncode(cartData));
}
Future loadFromStorage(List allProducts) async {
final prefs = await SharedPreferences.getInstance();
final cartJson = prefs.getString('cart');
if (cartJson != null) {
List cartData = jsonDecode(cartJson);
_items = cartData.map((item) {
final product = allProducts.firstWhere(
(p) => p.id == item['productId'],
);
return CartItem(
product: product,
quantity: item['quantity'],
);
}).toList();
notifyListeners();
}
}
}
Cart Logic Explanation:
Method | Logic | Why? |
---|---|---|
addItem() |
Check if exists → Increment quantity OR Add new | Prevent duplicate products, just increase count |
removeItem() |
Find by ID and remove from list | Complete removal from cart |
updateQuantity() |
If > 0 update, if = 0 remove | Flexible quantity control |
total |
Reduce/fold to sum all item totals | Calculate total price efficiently |
itemCount |
Sum all quantities (not items length!) | Show correct badge count |
🏠 Product Grid Screen
Provider Setup (main.dart):
// main.dart
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Cart()),
ChangeNotifierProvider(create: (_) => ProductsProvider()),
],
child: MyApp(),
),
);
}
class ProductsScreen extends StatelessWidget {
final List<Product> products = [
// Sample products
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ផលិតផល'),
actions: [
// Consumer for selective rebuild
Consumer(
builder: (context, cart, child) {
return IconButton(
icon: Badge(
label: Text(cart.itemCount.toString()),
child: Icon(Icons.shopping_cart),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CartScreen()),
);
},
);
},
),
],
),
body: GridView.builder(
padding: EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: products.length,
itemBuilder: (context, index) {
return ProductCard(product: products[index]);
},
),
);
}
}
🎴 Product Card
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({required this.product});
@override
Widget build(BuildContext context) {
final cart = Provider.of<Cart>(context, listen: false);
return Card(
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
image: DecorationImage(
image: NetworkImage(product.imageUrl),
fit: BoxFit.cover,
),
),
),
),
Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: TextStyle(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
Text(
'\$' + product.price.toString(),
style: TextStyle(
color: Colors.blue,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: Icon(Icons.add_shopping_cart, size: 18),
label: Text('Add'),
onPressed: () {
cart.addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('បានបន្ថែមទៅ Cart'),
duration: Duration(seconds: 1),
),
);
},
),
),
],
),
),
],
),
);
}
}
Consumer vs Provider.of:
// ❌ Bad: Entire screen rebuilds
class BadExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cart = Provider.of(context); // listen: true by default
return Scaffold(
appBar: AppBar(title: Text('Products')),
// Entire Scaffold rebuilds when cart changes!
body: ProductGrid(),
);
}
}
// ✅ Good: Only badge rebuilds
class GoodExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Products'),
actions: [
Consumer( // Only this widget rebuilds!
builder: (context, cart, child) {
return Badge(
label: Text(cart.itemCount.toString()),
child: Icon(Icons.shopping_cart),
);
},
),
],
),
body: ProductGrid(), // Never rebuilds!
);
}
}
🛒 Cart Screen
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Shopping Cart'),
actions: [
IconButton(
icon: Icon(Icons.delete_outline),
onPressed: () {
final cart = Provider.of(context, listen: false);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Clear Cart?'),
content: Text('Remove all items from cart?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('Cancel'),
),
TextButton(
onPressed: () {
cart.clear();
Navigator.pop(ctx);
},
child: Text('Clear', style: TextStyle(color: Colors.red)),
),
],
),
);
},
),
],
),
body: Consumer(
builder: (context, cart, child) {
if (cart.items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_cart_outlined, size: 100, color: Colors.grey),
SizedBox(height: 16),
Text('Your cart is empty', style: TextStyle(fontSize: 18)),
],
),
);
}
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) {
final item = cart.items[index];
return CartItemWidget(item: item);
},
),
),
_buildCartSummary(cart),
],
);
},
),
);
}
Widget _buildCartSummary(Cart cart) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, -2),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Subtotal:', style: TextStyle(fontSize: 16)),
Text(
'\$' + cart.total.toStringAsFixed(2),
style: TextStyle(fontSize: 16),
),
],
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Tax (10%):', style: TextStyle(fontSize: 16)),
Text(
'\$' + (cart.total * 0.1).toStringAsFixed(2),
style: TextStyle(fontSize: 16),
),
],
),
Divider(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total:',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
Text(
'\$' + (cart.total * 1.1).toStringAsFixed(2),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => CheckoutScreen()),
);
},
child: Text(
'Proceed to Checkout',
style: TextStyle(fontSize: 18),
),
),
),
],
),
);
}
}
class CartItemWidget extends StatelessWidget {
final CartItem item;
const CartItemWidget({required this.item});
@override
Widget build(BuildContext context) {
final cart = Provider.of(context, listen: false);
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
// Product Image
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: NetworkImage(item.product.imageUrl),
fit: BoxFit.cover,
),
),
),
SizedBox(width: 12),
// Product Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.product.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
Text(
'\$' + item.product.price.toStringAsFixed(2),
style: TextStyle(
color: Colors.blue,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
// Quantity Controls
Column(
children: [
Row(
children: [
IconButton(
icon: Icon(Icons.remove_circle_outline),
onPressed: () {
if (item.quantity > 1) {
cart.updateQuantity(
item.product.id,
item.quantity - 1,
);
} else {
cart.removeItem(item.product.id);
}
},
),
Text(
item.quantity.toString(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: Icon(Icons.add_circle_outline),
onPressed: () {
cart.updateQuantity(
item.product.id,
item.quantity + 1,
);
},
),
],
),
Text(
'\$' + item.totalPrice.toStringAsFixed(2),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
);
}
}
💳 Checkout Flow
class CheckoutScreen extends StatefulWidget {
@override
_CheckoutScreenState createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State {
final _formKey = GlobalKey();
String name = '';
String address = '';
String phone = '';
String paymentMethod = 'Credit Card';
@override
Widget build(BuildContext context) {
final cart = Provider.of(context, listen: false);
return Scaffold(
appBar: AppBar(title: Text('Checkout')),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Shipping Information',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
onSaved: (value) => name = value!,
),
SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(
labelText: 'Delivery Address',
border: OutlineInputBorder(),
),
maxLines: 3,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter address';
}
return null;
},
onSaved: (value) => address = value!,
),
SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter phone number';
}
return null;
},
onSaved: (value) => phone = value!,
),
SizedBox(height: 24),
Text(
'Payment Method',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
RadioListTile(
title: Text('Credit Card'),
value: 'Credit Card',
groupValue: paymentMethod,
onChanged: (value) {
setState(() {
paymentMethod = value!;
});
},
),
RadioListTile(
title: Text('Cash on Delivery'),
value: 'COD',
groupValue: paymentMethod,
onChanged: (value) {
setState(() {
paymentMethod = value!;
});
},
),
SizedBox(height: 24),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Total Amount:'),
Text(
'\$' + (cart.total * 1.1).toStringAsFixed(2),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
],
),
),
SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
padding: EdgeInsets.symmetric(vertical: 16),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// Process order
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Order Confirmed!'),
content: Text(
'Thank you, ' + name + '!\n\n' +
'Your order has been placed successfully.\n' +
'Total: \$' + (cart.total * 1.1).toStringAsFixed(2)
),
actions: [
TextButton(
onPressed: () {
cart.clear();
Navigator.of(ctx).pop();
Navigator.of(context).popUntil((route) => route.isFirst);
},
child: Text('OK'),
),
],
),
);
}
},
child: Text(
'Place Order',
style: TextStyle(fontSize: 18),
),
),
),
],
),
),
),
);
}
}
🚀 Enhancement Ideas
Product Features:
- ⭐ Product Reviews & Ratings: User reviews with star ratings
- 🖼️ Image Gallery: Multiple product images with swipe
- ❤️ Wishlist: Save favorites for later
- 🔔 Stock Alerts: Notify when out-of-stock items available
- 🏷️ Discounts & Coupons: Apply promo codes
Advanced Features:
- 🔐 User Authentication: Firebase Auth with email/Google
- ☁️ Cloud Sync: Firestore for cart/orders sync
- 💳 Payment Gateway: Stripe/PayPal integration
- 📦 Order Tracking: Real-time delivery status
- 📧 Email Notifications: Order confirmations
- 📊 Analytics: Track user behavior
💡 Best Practices:
- ✅ Provider for Global State: Cart accessible from anywhere
- ✅ Consumer for Optimization: Only rebuild necessary widgets
- ✅ listen: false: When reading without subscribing to changes
- ✅ ChangeNotifier: Simple state management for medium apps
- ✅ Cart Persistence: Save to SharedPreferences on changes
- ✅ Form Validation: Validate before checkout
- ✅ Empty State UI: Show helpful message when cart empty
- ✅ Confirmation Dialogs: Before clearing cart or placing order
⚠️ Common Mistakes:
- ❌ Using Provider.of with listen: true everywhere (rebuilds entire screen!)
- ❌ Not using Consumer for selective rebuilds
- ❌ Forgetting notifyListeners() after state changes (UI won't update)
- ❌ Not persisting cart (data lost on app restart)
- ❌ No loading states during checkout (poor UX)
- ❌ Not validating quantity (negative numbers, zero stock)
- ❌ Hardcoding prices (should come from backend/API)
✅ លទ្ធផល: អ្នកបានបង្កើត E-commerce app ពេញលេញដោយប្រើ Provider! Project នេះបង្ហាញពី advanced state management, shopping cart logic, checkout flow, និង persistence។ បន្ថែម features ដូចជា payment integration, Firebase authentication, product reviews, wishlist, order tracking។ នេះជា foundation ល្អសម្រាប់ real-world e-commerce applications!