© Khmer Angkor Academy - sophearithput168

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!