Back to Posts

Flutter for E-Commerce Applications

18 min read

Building e-commerce applications with Flutter requires careful consideration of user experience, performance, and security. This guide will walk you through creating a complete e-commerce solution, from product listings to checkout.

Core Features Implementation

1. Product Management

class Product {
  final String id;
  final String name;
  final String description;
  final double price;
  final List<String> images;
  final int stock;
  final Map<String, dynamic> attributes;

  Product({
    required this.id,
    required this.name,
    required this.description,
    required this.price,
    required this.images,
    required this.stock,
    required this.attributes,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['id'],
      name: json['name'],
      description: json['description'],
      price: json['price'],
      images: List<String>.from(json['images']),
      stock: json['stock'],
      attributes: json['attributes'],
    );
  }
}

class ProductRepository {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;

  Stream<List<Product>> getProducts() {
    return _firestore
        .collection('products')
        .snapshots()
        .map((snapshot) => snapshot.docs
            .map((doc) => Product.fromJson(doc.data()))
            .toList());
  }

  Future<void> updateStock(String productId, int newStock) async {
    await _firestore
        .collection('products')
        .doc(productId)
        .update({'stock': newStock});
  }
}

2. Shopping Cart Implementation

class CartItem {
  final Product product;
  int quantity;

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

  double get total => product.price * quantity;
}

class CartProvider extends ChangeNotifier {
  final List<CartItem> _items = [];

  List<CartItem> get items => _items;
  double get total => _items.fold(0, (sum, item) => sum + item.total);

  void addItem(Product product) {
    final existingItem = _items.firstWhere(
      (item) => item.product.id == product.id,
      orElse: () => CartItem(product: product),
    );

    if (existingItem.quantity > 0) {
      existingItem.quantity++;
    } else {
      _items.add(existingItem);
    }

    notifyListeners();
  }

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

  void updateQuantity(Product product, int quantity) {
    final item = _items.firstWhere((item) => item.product.id == product.id);
    item.quantity = quantity;
    notifyListeners();
  }

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

3. Product Listing UI

class ProductGrid extends StatelessWidget {
  final List<Product> products;

  const ProductGrid({required this.products});

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 0.75,
        crossAxisSpacing: 10,
        mainAxisSpacing: 10,
      ),
      itemCount: products.length,
      itemBuilder: (context, index) {
        final product = products[index];
        return ProductCard(product: product);
      },
    );
  }
}

class ProductCard extends StatelessWidget {
  final Product product;

  const ProductCard({required this.product});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: Image.network(
              product.images.first,
              fit: BoxFit.cover,
            ),
          ),
          Padding(
            padding: EdgeInsets.all(8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  product.name,
                  style: Theme.of(context).textTheme.titleMedium,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                SizedBox(height: 4),
                Text(
                  '\$${product.price.toStringAsFixed(2)}',
                  style: Theme.of(context).textTheme.titleLarge?.copyWith(
                        color: Theme.of(context).primaryColor,
                      ),
                ),
                SizedBox(height: 8),
                ElevatedButton(
                  onPressed: () {
                    context.read<CartProvider>().addItem(product);
                  },
                  child: Text('Add to Cart'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

4. Payment Integration

class PaymentService {
  final Stripe _stripe = Stripe('your_publishable_key');

  Future<PaymentIntent> createPaymentIntent(double amount) async {
    try {
      final response = await _stripe.paymentIntents.create(
        amount: (amount * 100).toInt(), // Convert to cents
        currency: 'usd',
      );
      return response;
    } catch (e) {
      throw Exception('Failed to create payment intent: $e');
    }
  }

  Future<void> processPayment({
    required String paymentMethodId,
    required double amount,
  }) async {
    try {
      final paymentIntent = await createPaymentIntent(amount);
      await _stripe.paymentIntents.confirm(
        paymentIntent.id,
        paymentMethodId: paymentMethodId,
      );
    } catch (e) {
      throw Exception('Payment failed: $e');
    }
  }
}

class CheckoutScreen extends StatefulWidget {
  @override
  _CheckoutScreenState createState() => _CheckoutScreenState();
}

class _CheckoutScreenState extends State<CheckoutScreen> {
  final _paymentService = PaymentService();
  bool _isProcessing = false;

  Future<void> _handlePayment() async {
    setState(() => _isProcessing = true);
    try {
      await _paymentService.processPayment(
        paymentMethodId: 'payment_method_id',
        amount: context.read<CartProvider>().total,
      );
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Payment successful!')),
      );
      context.read<CartProvider>().clear();
      Navigator.of(context).pop();
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Payment failed: $e')),
      );
    } finally {
      setState(() => _isProcessing = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Checkout')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: context.watch<CartProvider>().items.length,
              itemBuilder: (context, index) {
                final item = context.watch<CartProvider>().items[index];
                return ListTile(
                  title: Text(item.product.name),
                  subtitle: Text('Quantity: ${item.quantity}'),
                  trailing: Text('\$${item.total.toStringAsFixed(2)}'),
                );
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.all(16.0),
            child: Column(
              children: [
                Text(
                  'Total: \$${context.watch<CartProvider>().total.toStringAsFixed(2)}',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
                SizedBox(height: 16),
                ElevatedButton(
                  onPressed: _isProcessing ? null : _handlePayment,
                  child: _isProcessing
                      ? CircularProgressIndicator()
                      : Text('Pay Now'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

State Management

1. Using Riverpod for State Management

final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
  return CartNotifier();
});

class CartNotifier extends StateNotifier<List<CartItem>> {
  CartNotifier() : super([]);

  void addItem(Product product) {
    final existingItem = state.firstWhere(
      (item) => item.product.id == product.id,
      orElse: () => CartItem(product: product),
    );

    if (existingItem.quantity > 0) {
      state = state.map((item) {
        if (item.product.id == product.id) {
          return CartItem(
            product: item.product,
            quantity: item.quantity + 1,
          );
        }
        return item;
      }).toList();
    } else {
      state = [...state, existingItem];
    }
  }

  void removeItem(String productId) {
    state = state.where((item) => item.product.id != productId).toList();
  }

  void clear() {
    state = [];
  }
}

Performance Optimization

1. Image Optimization

class OptimizedProductImage extends StatelessWidget {
  final String imageUrl;
  final double? width;
  final double? height;

  const OptimizedProductImage({
    required this.imageUrl,
    this.width,
    this.height,
  });

  @override
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: imageUrl,
      width: width,
      height: height,
      fit: BoxFit.cover,
      placeholder: (context, url) => Container(
        color: Colors.grey[300],
        child: Center(child: CircularProgressIndicator()),
      ),
      errorWidget: (context, url, error) => Icon(Icons.error),
    );
  }
}

2. Lazy Loading

class LazyProductList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 1000, // Large number for infinite scroll
      itemBuilder: (context, index) {
        return FutureBuilder<Product>(
          future: _loadProduct(index),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return Shimmer.fromColors(
                baseColor: Colors.grey[300]!,
                highlightColor: Colors.grey[100]!,
                child: ListTile(
                  title: Container(
                    height: 20,
                    color: Colors.white,
                  ),
                ),
              );
            }
            if (snapshot.hasError) {
              return ListTile(
                title: Text('Error loading product'),
              );
            }
            return ProductTile(product: snapshot.data!);
          },
        );
      },
    );
  }
}

Security Considerations

1. Secure Storage

class SecureStorageService {
  final _storage = FlutterSecureStorage();

  Future<void> savePaymentToken(String token) async {
    await _storage.write(key: 'payment_token', value: token);
  }

  Future<String?> getPaymentToken() async {
    return await _storage.read(key: 'payment_token');
  }

  Future<void> clearPaymentToken() async {
    await _storage.delete(key: 'payment_token');
  }
}

2. Input Validation

class PaymentForm extends StatefulWidget {
  @override
  _PaymentFormState createState() => _PaymentFormState();
}

class _PaymentFormState extends State<PaymentForm> {
  final _formKey = GlobalKey<FormState>();
  final _cardNumberController = TextEditingController();
  final _expiryController = TextEditingController();
  final _cvvController = TextEditingController();

  String? _validateCardNumber(String? value) {
    if (value == null || value.isEmpty) {
      return 'Please enter card number';
    }
    if (!RegExp(r'^[0-9]{16}$').hasMatch(value)) {
      return 'Invalid card number';
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _cardNumberController,
            decoration: InputDecoration(labelText: 'Card Number'),
            validator: _validateCardNumber,
            keyboardType: TextInputType.number,
          ),
          // Add more form fields...
        ],
      ),
    );
  }
}

Testing

1. Unit Tests

void main() {
  group('CartProvider Tests', () {
    late CartProvider cartProvider;
    late Product testProduct;

    setUp(() {
      cartProvider = CartProvider();
      testProduct = Product(
        id: '1',
        name: 'Test Product',
        description: 'Test Description',
        price: 10.0,
        images: ['image_url'],
        stock: 10,
        attributes: {},
      );
    });

    test('Adding item to cart', () {
      cartProvider.addItem(testProduct);
      expect(cartProvider.items.length, 1);
      expect(cartProvider.items.first.product.id, testProduct.id);
    });

    test('Removing item from cart', () {
      cartProvider.addItem(testProduct);
      cartProvider.removeItem(testProduct);
      expect(cartProvider.items.length, 0);
    });
  });
}

2. Widget Tests

void main() {
  group('ProductCard Tests', () {
    testWidgets('ProductCard displays correct information', (tester) async {
      final product = Product(
        id: '1',
        name: 'Test Product',
        description: 'Test Description',
        price: 10.0,
        images: ['image_url'],
        stock: 10,
        attributes: {},
      );

      await tester.pumpWidget(
        MaterialApp(
          home: ProductCard(product: product),
        ),
      );

      expect(find.text('Test Product'), findsOneWidget);
      expect(find.text('\$10.00'), findsOneWidget);
      expect(find.text('Add to Cart'), findsOneWidget);
    });
  });
}

Conclusion

Building e-commerce applications with Flutter involves:

  • Implementing core features like product management and shopping cart
  • Integrating payment systems securely
  • Optimizing performance for better user experience
  • Ensuring proper state management
  • Implementing security measures
  • Writing comprehensive tests

Remember to:

  • Focus on user experience
  • Optimize images and loading
  • Implement proper error handling
  • Secure sensitive data
  • Test thoroughly
  • Follow platform-specific guidelines

With these techniques, you can create powerful and user-friendly e-commerce applications using Flutter!