Skip to content

Build a Flutter E-Commerce UI — Complete Project Tutorial with Dart

DodaTech Updated 2026-06-28 14 min read

In this tutorial, you will learn about Build a Flutter E. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a complete Flutter e-commerce UI using Dart that showcases product listings with categories, a shopping cart with quantity controls, a checkout flow, and persistent state management using Provider and SharedPreferences.

What You Will Learn

  • Designing a multi-screen e-commerce navigation structure
  • Building product listing grids with filtering and sorting
  • Implementing a shopping cart with add, remove, and quantity controls
  • Managing cart state persistently with SharedPreferences
  • Creating a checkout flow with address and payment forms
  • Applying Material You theming and responsive layouts
  • Handling empty states, loading states, and error states

Why It Matters

E-commerce apps represent one of the most complex and common mobile application categories. They combine product catalogs, search, filtering, cart management, user accounts, payments, and order tracking — all in a single app. Building an e-commerce UI teaches you patterns that apply to any data-driven app: list-detail navigation, state management across screens, form validation, and persistent local storage. Doda Browser's shopping assistant feature, which compares prices across retailers, reuses the same grid layout, cart model, and navigation architecture described in this project.

Real-World Use

A local grocery delivery service uses a Flutter e-commerce app where customers browse produce by category, add items to a cart, set delivery preferences, and complete checkout. The cart persists even if the app is closed, so the user can add items throughout the day and check out in one go. Durga Antivirus Pro's license store uses this same cart-and-checkout pattern to sell subscription plans.

Learning Path

flowchart LR
  A[Project Weather App] --> B[Project E-Commerce UI\nYou are here]
  B --> C[Project Game with Flame]
  style B fill:#f90,color:#fff

Project Setup

Create a new Flutter project:

flutter create ecommerce_ui
cd ecommerce_ui

Add dependencies:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.0
  shared_preferences: ^2.2.0
  cached_network_image: ^3.3.0
  google_fonts: ^6.2.0

cached_network_image loads product images with Caching and placeholders. google_fonts provides typography options. shared_preferences persists the cart across app restarts.

Data Models

Define the product and cart models:

// lib/models/product.dart
class Product {
  final String id;
  final String name;
  final String description;
  final double price;
  final String imageUrl;
  final String category;
  final double rating;
  final int reviewCount;
  final bool inStock;

  Product({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
    required this.imageUrl,
    required this.category,
    this.rating = 0.0,
    this.reviewCount = 0,
    this.inStock = true,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'] as String,
      name: json['name'] as String,
      description: json['description'] as String,
      price: (json['price'] as num).toDouble(),
      imageUrl: json['imageUrl'] as String? ?? 'https://via.placeholder.com/150',
      category: json['category'] as String,
      rating: (json['rating'] as num?)?.toDouble() ?? 0.0,
      reviewCount: json['reviewCount'] as int? ?? 0,
      inStock: json['inStock'] as bool? ?? true,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'description': description,
      'price': price,
      'imageUrl': imageUrl,
      'category': category,
      'rating': rating,
      'reviewCount': reviewCount,
      'inStock': inStock,
    };
  }

  Product copyWith({
    String? id,
    String? name,
    String? description,
    double? price,
    String? imageUrl,
    String? category,
    double? rating,
    int? reviewCount,
    bool? inStock,
  }) {
    return Product(
      id: id ?? this.id,
      name: name ?? this.name,
      description: description ?? this.description,
      price: price ?? this.price,
      imageUrl: imageUrl ?? this.imageUrl,
      category: category ?? this.category,
      rating: rating ?? this.rating,
      reviewCount: reviewCount ?? this.reviewCount,
      inStock: inStock ?? this.inStock,
    );
  }
}
// lib/models/cart_item.dart
import 'product.dart';

class CartItem {
  final Product product;
  int quantity;

  CartItem({required this.product, this.quantity = 1});

  double get totalPrice => product.price * quantity;

  Map<String, dynamic> toJson() => {
        'product': product.toJson(),
        'quantity': quantity,
      };

  factory CartItem.fromJson(Map<String, dynamic> json) {
    return CartItem(
      product: Product.fromJson(json['product'] as Map<String, dynamic>),
      quantity: json['quantity'] as int? ?? 1,
    );
  }
}

Cart Provider with Persistence

Manage the cart state and persist it to disk:

// lib/providers/cart_provider.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/product.dart';
import '../models/cart_item.dart';

class CartProvider extends ChangeNotifier {
  List<CartItem> _items = [];
  bool _isLoaded = false;

  List<CartItem> get items => List.unmodifiable(_items);
  int get itemCount => _items.fold(0, (sum, item) => sum + item.quantity);
  double get subtotal => _items.fold(0.0, (sum, item) => sum + item.totalPrice);
  double get tax => subtotal * 0.08;
  double get shipping => subtotal > 50 ? 0 : 5.99;
  double get total => subtotal + tax + shipping;
  bool get isEmpty => _items.isEmpty;
  bool get isLoaded => _isLoaded;

  Future<void> loadCart() async {
    final prefs = await SharedPreferences.getInstance();
    final cartJson = prefs.getString('cart');
    if (cartJson != null) {
      final list = jsonDecode(cartJson) as List<dynamic>;
      _items = list.map((e) => CartItem.fromJson(e as Map<String, dynamic>)).toList();
    }
    _isLoaded = true;
    notifyListeners();
  }

  Future<void> _saveCart() async {
    final prefs = await SharedPreferences.getInstance();
    final cartJson = jsonEncode(_items.map((e) => e.toJson()).toList());
    await prefs.setString('cart', cartJson);
  }

  void addItem(Product product) {
    final existing = _items.where((item) => item.product.id == product.id).firstOrNull;
    if (existing != null) {
      existing.quantity++;
    } else {
      _items.add(CartItem(product: product));
    }
    notifyListeners();
    _saveCart();
  }

  void removeItem(String productId) {
    _items.removeWhere((item) => item.product.id == productId);
    notifyListeners();
    _saveCart();
  }

  void updateQuantity(String productId, int quantity) {
    final item = _items.where((item) => item.product.id == productId).firstOrNull;
    if (item == null) return;

    if (quantity <= 0) {
      removeItem(productId);
      return;
    }

    item.quantity = quantity;
    notifyListeners();
    _saveCart();
  }

  void clearCart() {
    _items.clear();
    notifyListeners();
    _saveCart();
  }
}

The cart is saved to SharedPreferences as a JSON string every time it changes. When the app starts, loadCart() restores the previous state.

Product Repository

Provide sample product data and filtering:

// lib/data/products.dart
import '../models/product.dart';

class ProductRepository {
  static final List<Product> _allProducts = [
    Product(
      id: '1',
      name: 'Wireless Headphones',
      description: 'Premium noise-cancelling wireless headphones with 30-hour battery life and comfortable over-ear design.',
      price: 79.99,
      imageUrl: 'https://via.placeholder.com/300x300?text=Headphones',
      category: 'Electronics',
      rating: 4.5,
      reviewCount: 234,
    ),
    Product(
      id: '2',
      name: 'Running Shoes',
      description: 'Lightweight running shoes with responsive cushioning and breathable mesh upper for daily training.',
      price: 129.99,
      imageUrl: 'https://via.placeholder.com/300x300?text=Shoes',
      category: 'Sports',
      rating: 4.3,
      reviewCount: 189,
    ),
    Product(
      id: '3',
      name: 'Cotton T-Shirt',
      description: 'Classic fit cotton t-shirt available in multiple colors. Machine washable and pre-shrunk.',
      price: 24.99,
      imageUrl: 'https://via.placeholder.com/300x300?text=TShirt',
      category: 'Clothing',
      rating: 4.1,
      reviewCount: 567,
    ),
    Product(
      id: '4',
      name: 'Coffee Maker',
      description: '12-cup programmable coffee maker with built-in grinder, thermal carafe, and auto-shutoff.',
      price: 89.99,
      imageUrl: 'https://via.placeholder.com/300x300?text=Coffee',
      category: 'Home',
      rating: 4.6,
      reviewCount: 412,
    ),
    Product(
      id: '5',
      name: 'Backpack',
      description: 'Durable 35L backpack with padded laptop compartment, water bottle pockets, and ergonomic straps.',
      price: 59.99,
      imageUrl: 'https://via.placeholder.com/300x300?text=Backpack',
      category: 'Accessories',
      rating: 4.4,
      reviewCount: 328,
    ),
    Product(
      id: '6',
      name: 'Smart Watch',
      description: 'Fitness smart watch with heart rate monitoring, GPS tracking, and 7-day battery life.',
      price: 199.99,
      imageUrl: 'https://via.placeholder.com/300x300?text=Watch',
      category: 'Electronics',
      rating: 4.2,
      reviewCount: 876,
    ),
  ];

  static List<Product> getAll() => List.unmodifiable(_allProducts);

  static List<String> getCategories() {
    return _allProducts.map((p) => p.category).toSet().toList()..sort();
  }

  static List<Product> getByCategory(String category) {
    return _allProducts.where((p) => p.category == category).toList();
  }

  static Product? getById(String id) {
    return _allProducts.where((p) => p.id == id).firstOrNull;
  }

  static List<Product> search(String query) {
    if (query.isEmpty) return _allProducts;
    final lower = query.toLowerCase();
    return _allProducts
        .where((p) => p.name.toLowerCase().contains(lower) || p.description.toLowerCase().contains(lower))
        .toList();
  }
}

Main App and Navigation

Set up the app with bottom navigation for home, categories, cart, and profile:

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/cart_provider.dart';
import 'screens/home_screen.dart';
import 'screens/cart_screen.dart';
import 'screens/categories_screen.dart';
import 'screens/profile_screen.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    ChangeNotifierProvider(
      create: (_) => CartProvider()..loadCart(),
      child: MaterialApp(
        title: 'ShopFlutter',
        theme: ThemeData(
          colorSchemeSeed: Colors.teal,
          useMaterial3: true,
        ),
        home: MainScreen(),
      ),
    ),
  );
}

class MainScreen extends StatefulWidget {
  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;

  final screens = [
    HomeScreen(),
    CategoriesScreen(),
    CartScreen(),
    ProfileScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: screens[_currentIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) => setState(() => _currentIndex = index),
        destinations: [
          NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
          NavigationDestination(icon: Icon(Icons.category_outlined), selectedIcon: Icon(Icons.category), label: 'Categories'),
          NavigationDestination(
            icon: Badge(
              isLabelVisible: context.watch<CartProvider>().itemCount > 0,
              label: Text('${context.watch<CartProvider>().itemCount}'),
              child: Icon(Icons.shopping_cart_outlined),
            ),
            selectedIcon: Badge(
              isLabelVisible: context.watch<CartProvider>().itemCount > 0,
              label: Text('${context.watch<CartProvider>().itemCount}'),
              child: Icon(Icons.shopping_cart),
            ),
            label: 'Cart',
          ),
          NavigationDestination(icon: Icon(Icons.person_outlined), selectedIcon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

Home Screen with Product Grid

Build the product listing:

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import '../data/products.dart';
import '../models/product.dart';
import '../widgets/product_card.dart';
import 'product_detail_screen.dart';

class HomeScreen extends StatefulWidget {
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _searchController = TextEditingController();
  String _searchQuery = '';
  String _selectedCategory = 'All';

  List<Product> get _filteredProducts {
    var products = ProductRepository.getAll();
    if (_selectedCategory != 'All') {
      products = products.where((p) => p.category == _selectedCategory).toList();
    }
    if (_searchQuery.isNotEmpty) {
      products = products
          .where((p) => p.name.toLowerCase().contains(_searchQuery.toLowerCase()))
          .toList();
    }
    return products;
  }

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final categories = ['All', ...ProductRepository.getCategories()];

    return Scaffold(
      appBar: AppBar(title: Text('ShopFlutter')),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'Search products...',
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(borderRadius: BorderRadius.circular(24)),
              ),
              onChanged: (query) => setState(() => _searchQuery = query),
            ),
          ),
          SizedBox(
            height: 48,
            child: ListView(
              scrollDirection: Axis.horizontal,
              padding: EdgeInsets.symmetric(horizontal: 16),
              children: categories.map((category) {
                final isSelected = category == _selectedCategory;
                return Padding(
                  padding: EdgeInsets.only(right: 8),
                  child: FilterChip(
                    label: Text(category),
                    selected: isSelected,
                    onSelected: (_) => setState(() => _selectedCategory = category),
                  ),
                );
              }).toList(),
            ),
          ),
          SizedBox(height: 8),
          Expanded(
            child: _filteredProducts.isEmpty
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(Icons.search_off, size: 64, color: Colors.grey),
                        SizedBox(height: 16),
                        Text('No products found'),
                      ],
                    ),
                  )
                : GridView.builder(
                    padding: EdgeInsets.all(16),
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2,
                      childAspectRatio: 0.7,
                      crossAxisSpacing: 12,
                      mainAxisSpacing: 12,
                    ),
                    itemCount: _filteredProducts.length,
                    itemBuilder: (context, index) {
                      return ProductCard(
                        product: _filteredProducts[index],
                        onTap: () => Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (_) => ProductDetailScreen(product: _filteredProducts[index]),
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

Product card widget:

// lib/widgets/product_card.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:provider/provider.dart';
import '../models/product.dart';
import '../providers/cart_provider.dart';

class ProductCard extends StatelessWidget {
  final Product product;
  final VoidCallback onTap;

  const ProductCard({required this.product, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: InkWell(
        onTap: onTap,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Expanded(
              child: CachedNetworkImage(
                imageUrl: product.imageUrl,
                fit: BoxFit.cover,
                width: double.infinity,
                placeholder: (_, __) => Center(child: CircularProgressIndicator()),
                errorWidget: (_, __, ___) => Center(child: Icon(Icons.image)),
              ),
            ),
            Padding(
              padding: EdgeInsets.all(8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    style: Theme.of(context).textTheme.titleSmall,
                  ),
                  SizedBox(height: 4),
                  Row(
                    children: [
                      Icon(Icons.star, size: 14, color: Colors.amber),
                      SizedBox(width: 4),
                      Text('${product.rating}'),
                    ],
                  ),
                  SizedBox(height: 4),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        '\$${product.price.toStringAsFixed(2)}',
                        style: Theme.of(context).textTheme.titleMedium?.copyWith(
                              color: Theme.of(context).colorScheme.primary,
                              fontWeight: FontWeight.bold,
                            ),
                      ),
                      IconButton(
                        icon: Icon(Icons.add_shopping_cart, size: 20),
                        onPressed: () {
                          context.read<CartProvider>().addItem(product);
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(
                              content: Text('${product.name} added to cart'),
                              duration: Duration(seconds: 1),
                            ),
                          );
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Cart Screen

Build the shopping cart view with quantity controls:

// lib/screens/cart_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import '../widgets/cart_item_card.dart';
import 'checkout_screen.dart';

class CartScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Shopping Cart')),
      body: Consumer<CartProvider>(
        builder: (context, cart, child) {
          if (!cart.isLoaded) {
            return Center(child: CircularProgressIndicator());
          }

          if (cart.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.shopping_cart_outlined, size: 64, color: Colors.grey),
                  SizedBox(height: 16),
                  Text('Your cart is empty', style: Theme.of(context).textTheme.headlineSmall),
                  SizedBox(height: 8),
                  Text('Browse products and add items to get started'),
                  SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () => Navigator.pop(context),
                    child: Text('Start Shopping'),
                  ),
                ],
              ),
            );
          }

          return Column(
            children: [
              Expanded(
                child: ListView.builder(
                  padding: EdgeInsets.all(16),
                  itemCount: cart.items.length,
                  itemBuilder: (context, index) {
                    return CartItemCard(cartItem: cart.items[index]);
                  },
                ),
              ),
              Container(
                padding: EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.surface,
                  boxShadow: [BoxShadow(blurRadius: 4, color: Colors.black26)],
                ),
                child: Column(
                  children: [
                    _OrderSummaryRow(label: 'Subtotal', value: '\$${cart.subtotal.toStringAsFixed(2)}'),
                    _OrderSummaryRow(label: 'Tax (8%)', value: '\$${cart.tax.toStringAsFixed(2)}'),
                    _OrderSummaryRow(label: 'Shipping', value: cart.shipping == 0 ? 'Free' : '\$${cart.shipping.toStringAsFixed(2)}'),
                    Divider(),
                    _OrderSummaryRow(label: 'Total', value: '\$${cart.total.toStringAsFixed(2)}', isBold: true),
                    SizedBox(height: 16),
                    FilledButton.icon(
                      onPressed: () => Navigator.push(
                        context,
                        MaterialPageRoute(builder: (_) => CheckoutScreen()),
                      ),
                      icon: Icon(Icons.lock),
                      label: Text('Proceed to Checkout'),
                      style: FilledButton.styleFrom(
                        minimumSize: Size(double.infinity, 56),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

class _OrderSummaryRow extends StatelessWidget {
  final String label;
  final String value;
  final bool isBold;

  const _OrderSummaryRow({required this.label, required this.value, this.isBold = false});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: isBold ? TextStyle(fontWeight: FontWeight.bold) : null),
          Text(value, style: isBold ? TextStyle(fontWeight: FontWeight.bold) : null),
        ],
      ),
    );
  }
}

Cart item card with quantity controls:

// lib/widgets/cart_item_card.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/cart_item.dart';
import '../providers/cart_provider.dart';

class CartItemCard extends StatelessWidget {
  final CartItem cartItem;

  const CartItemCard({required this.cartItem});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: EdgeInsets.all(12),
        child: Row(
          children: [
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.network(
                cartItem.product.imageUrl,
                width: 80,
                height: 80,
                fit: BoxFit.cover,
              ),
            ),
            SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(cartItem.product.name, style: Theme.of(context).textTheme.titleSmall),
                  SizedBox(height: 4),
                  Text('\$${cartItem.product.price.toStringAsFixed(2)}'),
                  SizedBox(height: 8),
                  Row(
                    children: [
                      IconButton(
                        icon: Icon(Icons.remove_circle_outline),
                        onPressed: () {
                          context.read<CartProvider>().updateQuantity(
                                cartItem.product.id,
                                cartItem.quantity - 1,
                              );
                        },
                        iconSize: 20,
                      ),
                      Text('${cartItem.quantity}'),
                      IconButton(
                        icon: Icon(Icons.add_circle_outline),
                        onPressed: () {
                          context.read<CartProvider>().updateQuantity(
                                cartItem.product.id,
                                cartItem.quantity + 1,
                              );
                        },
                        iconSize: 20,
                      ),
                    ],
                  ),
                ],
              ),
            ),
            Column(
              children: [
                Text('\$${cartItem.totalPrice.toStringAsFixed(2)}', style: TextStyle(fontWeight: FontWeight.bold)),
                IconButton(
                  icon: Icon(Icons.delete_outline, color: Colors.red),
                  onPressed: () => context.read<CartProvider>().removeItem(cartItem.product.id),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Common Mistakes

  1. Storing complex objects in SharedPreferences: SharedPreferences is designed for simple key-value pairs. For complex data like cart items, serialize to JSON first. For large datasets, consider sqflite instead.

  2. Not using immutable state in provider: Mutating a CartItem's quantity directly without calling notifyListeners() causes the UI to display stale data. Always call notifyListeners() after state mutations.

  3. Hard-coding product data: Product data in this tutorial is static for simplicity. In production, products come from an API. Abstract the ProductRepository behind an interface so you can swap the implementation without changing the UI code.

  4. Forgetting to dispose controllers: Every TextEditingController must be disposed. Failing to do so causes memory leaks that slow down the app over time.

  5. Ignoring empty and error states: An empty cart or failed product load should show a helpful state rather than a blank screen. Always handle loading, empty, and error states for every data-driven widget.

  6. Not using placeholder images: Product images that fail to load leave broken image icons. Use cached_network_image with errorWidget and placeholder parameters to handle both loading and error states gracefully.

  7. Blocking the UI during cart persistence: SharedPreferences operations are asynchronous in Flutter. Call await and show a loading indicator if the cart is large enough to cause noticeable delay.

Practice Questions

  1. Why is the cart data persisted to SharedPreferences as a JSON string rather than as individual key-value pairs?
  2. How does the Consumer<CartProvider> widget prevent unnecessary rebuilds in the navigation bar badge?
  3. What changes would be needed to support multiple currency formats?
  4. How would you implement a wishlist feature alongside the cart?
  5. Challenge: Add a coupon code system. Create a CouponProvider that validates coupon codes against a list of active coupons, calculates discounts, and applies them to the cart total before checkout. Handle expired and invalid coupon codes with appropriate error messages.

Mini Project

Extend the e-commerce UI with these features:

  • Product detail screen with image gallery (swipeable), size/color selection, reviews list, and related products
  • Order history screen showing past orders with status tracking
  • Address management screen with Google Maps address autocomplete
  • Dark mode toggle persisted in SharedPreferences
  • Search with debounced text input and recent search suggestions
  • Animated cart badge that scales when items are added using TweenAnimationBuilder

FAQ

How do I connect this to a real backend?

Replace ProductRepository with an HTTP-based service class that calls your product API. Inject the service via the provider and call it in the provider's load method. The UI code remains unchanged because the provider interface stays the same.

Can I use this code for a food delivery app?

Yes. Replace the Product model with a MenuItem model, add customization options (size, toppings), implement a restaurant selection flow, and replace the checkout with delivery address and time slot selection.

How do I handle payment processing?

Integrate Stripe, PayPal, or Razorpay using their Flutter SDKs. The checkout screen collects payment details and calls the SDK's payment method. Never handle credit card data directly — always use a payment provider's client-side SDK.

Why use cached_network_image instead of Image.network?

Image.network fetches the image from the network every time the widget rebuilds. cached_network_image caches images to disk, so previously loaded images display instantly even offline. This dramatically improves scroll performance in product grids.

How do I implement push notifications for order updates?

Use Firebase Cloud Messaging with the firebase_messaging package. When the order status changes on the backend, send a push notification via FCM. The Flutter app receives it via onMessage and updates the order history UI.

What is Next

Proceed to Project Game with Flame to build a 2D game using the Flame game engine, applying animation, collision detection, and input handling skills. Then revisit State Management for more advanced patterns used in complex apps.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro