DEV Community

Cover image for 🤯Still Confused About Flutter Provider? Let’s Break It Down!
Hitesh Meghwal
Hitesh Meghwal

Posted on

🤯Still Confused About Flutter Provider? Let’s Break It Down!

State management in Flutter can feel overwhelming, especially when you're starting out. You've probably heard about Provider, BLoC, Riverpod, and GetX, but Provider remains one of the most popular choices for good reason. If you're still confused about how Provider works, you're not alone!

In this comprehensive guide, I'll break down Flutter Provider from the ground up, show you practical examples, and help you understand when and why to use it. By the end of this post, you'll have a solid understanding of Provider and be ready to use it in your own projects.

What is Flutter Provider?

Provider is a state management library that acts as a wrapper around InheritedWidget. It allows you to share data across your widget tree without passing it down through constructors. Think of it as a way to make data available globally in your app while keeping it reactive and efficient.

Key Benefits:

  • Simple to understand and implement
  • Minimal boilerplate code
  • Excellent performance with automatic optimizations
  • Great for beginners and complex apps alike
  • Officially recommended by the Flutter team

Setting Up Provider

First, add Provider to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.5
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to install the package.

Core Concepts You Need to Know

1. ChangeNotifier

ChangeNotifier is a class that provides change notification to its listeners. When you want to notify widgets that something has changed, you call notifyListeners().

import 'package:flutter/foundation.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners(); // This tells listeners that something changed
  }

  void decrement() {
    _count--;
    notifyListeners();
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

2. ChangeNotifierProvider

This widget provides a ChangeNotifier to its descendants. It automatically handles the lifecycle of your model.

ChangeNotifierProvider(
  create: (context) => CounterModel(),
  child: MyApp(),
)
Enter fullscreen mode Exit fullscreen mode

3. Consumer

Consumer is a widget that listens to changes in your model and rebuilds when notifyListeners() is called.

Consumer<CounterModel>(
  builder: (context, counter, child) {
    return Text('Count: ${counter.count}');
  },
)
Enter fullscreen mode Exit fullscreen mode

4. Provider.of

An alternative way to access your model data. Use listen: false when you don't want to rebuild the widget.

// This will rebuild when the model changes
final counter = Provider.of<CounterModel>(context);

// This won't rebuild when the model changes
final counter = Provider.of<CounterModel>(context, listen: false);
Enter fullscreen mode Exit fullscreen mode

Let's Build a Complete Example

Let's create a simple shopping cart app to see Provider in action:

Step 1: Create the Models

// models/product.dart
class Product {
  final String id;
  final String name;
  final double price;
  final String imageUrl;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
  });
}

// models/cart_item.dart
class CartItem {
  final Product product;
  int quantity;

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

  double get totalPrice => product.price * quantity;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Cart Provider

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

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

  List<CartItem> get items => List.unmodifiable(_items);

  int get itemCount => _items.length;

  double get totalAmount {
    return _items.fold(0.0, (sum, item) => sum + item.totalPrice);
  }

  void addItem(Product product) {
    // Check if item already exists
    final existingIndex = _items.indexWhere(
      (item) => item.product.id == product.id,
    );

    if (existingIndex >= 0) {
      // Increase quantity
      _items[existingIndex].quantity++;
    } else {
      // Add new item
      _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.removeAt(index);
      } else {
        _items[index].quantity = quantity;
      }
      notifyListeners();
    }
  }

  void clearCart() {
    _items.clear();
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Setup the Provider in main.dart

// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/cart_provider.dart';
import 'screens/product_list_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CartProvider(),
      child: MaterialApp(
        title: 'Shopping Cart Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: ProductListScreen(),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create the Product List Screen

// screens/product_list_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.dart';
import '../models/product.dart';
import 'cart_screen.dart';

class ProductListScreen extends StatelessWidget {
  final List<Product> products = [
    Product(id: '1', name: 'Laptop', price: 999.99, imageUrl: 'laptop.jpg'),
    Product(id: '2', name: 'Phone', price: 699.99, imageUrl: 'phone.jpg'),
    Product(id: '3', name: 'Headphones', price: 199.99, imageUrl: 'headphones.jpg'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Products'),
        actions: [
          Consumer<CartProvider>(
            builder: (context, cart, child) {
              return Stack(
                children: [
                  IconButton(
                    icon: Icon(Icons.shopping_cart),
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => CartScreen()),
                      );
                    },
                  ),
                  if (cart.itemCount > 0)
                    Positioned(
                      right: 8,
                      top: 8,
                      child: Container(
                        padding: EdgeInsets.all(2),
                        decoration: BoxDecoration(
                          color: Colors.red,
                          borderRadius: BorderRadius.circular(10),
                        ),
                        constraints: BoxConstraints(
                          minWidth: 16,
                          minHeight: 16,
                        ),
                        child: Text(
                          '${cart.itemCount}',
                          style: TextStyle(
                            color: Colors.white,
                            fontSize: 12,
                          ),
                          textAlign: TextAlign.center,
                        ),
                      ),
                    ),
                ],
              );
            },
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return ProductCard(product: product);
        },
      ),
    );
  }
}

class ProductCard extends StatelessWidget {
  final Product product;

  const ProductCard({Key? key, required this.product}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.all(8),
      child: ListTile(
        leading: CircleAvatar(
          child: Text(product.name[0]),
        ),
        title: Text(product.name),
        subtitle: Text('\$${product.price.toStringAsFixed(2)}'),
        trailing: Consumer<CartProvider>(
          builder: (context, cart, child) {
            return ElevatedButton(
              onPressed: () {
                cart.addItem(product);
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('${product.name} added to cart!'),
                    duration: Duration(seconds: 1),
                  ),
                );
              },
              child: Text('Add to Cart'),
            );
          },
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Create the Cart Screen

// screens/cart_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/cart_provider.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.items.isEmpty) {
            return Center(
              child: 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 Card(
                      margin: EdgeInsets.all(8),
                      child: ListTile(
                        leading: CircleAvatar(
                          child: Text(item.product.name[0]),
                        ),
                        title: Text(item.product.name),
                        subtitle: Text(
                          '\$${item.product.price.toStringAsFixed(2)} x ${item.quantity}',
                        ),
                        trailing: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            IconButton(
                              icon: Icon(Icons.remove),
                              onPressed: () {
                                cart.updateQuantity(
                                  item.product.id,
                                  item.quantity - 1,
                                );
                              },
                            ),
                            Text('${item.quantity}'),
                            IconButton(
                              icon: Icon(Icons.add),
                              onPressed: () {
                                cart.updateQuantity(
                                  item.product.id,
                                  item.quantity + 1,
                                );
                              },
                            ),
                            IconButton(
                              icon: Icon(Icons.delete),
                              onPressed: () {
                                cart.removeItem(item.product.id);
                              },
                            ),
                          ],
                        ),
                      ),
                    );
                  },
                ),
              ),
              Container(
                padding: EdgeInsets.all(16),
                decoration: BoxDecoration(
                  border: Border(top: BorderSide(color: Colors.grey)),
                ),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(
                      'Total: \$${cart.totalAmount.toStringAsFixed(2)}',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    ElevatedButton(
                      onPressed: cart.items.isEmpty
                          ? null
                          : () {
                              // Implement checkout logic
                              showDialog(
                                context: context,
                                builder: (context) => AlertDialog(
                                  title: Text('Checkout'),
                                  content: Text('Order placed successfully!'),
                                  actions: [
                                    TextButton(
                                      onPressed: () {
                                        cart.clearCart();
                                        Navigator.pop(context);
                                      },
                                      child: Text('OK'),
                                    ),
                                  ],
                                ),
                              );
                            },
                      child: Text('Checkout'),
                    ),
                  ],
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Provider Patterns

1. MultiProvider

When you need multiple providers, use MultiProvider:

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (context) => CartProvider()),
    ChangeNotifierProvider(create: (context) => UserProvider()),
    ChangeNotifierProvider(create: (context) => ThemeProvider()),
  ],
  child: MyApp(),
)
Enter fullscreen mode Exit fullscreen mode

2. Selector Widget

Use Selector when you only want to rebuild for specific changes:

Selector<CartProvider, int>(
  selector: (context, cart) => cart.itemCount,
  builder: (context, itemCount, child) {
    return Text('Items: $itemCount');
  },
)
Enter fullscreen mode Exit fullscreen mode

3. ProxyProvider

Use ProxyProvider when one provider depends on another:

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (context) => AuthProvider()),
    ChangeNotifierProxyProvider<AuthProvider, CartProvider>(
      create: (context) => CartProvider(),
      update: (context, auth, cart) => cart..updateAuth(auth),
    ),
  ],
  child: MyApp(),
)
Enter fullscreen mode Exit fullscreen mode

Common Mistakes to Avoid

1. Forgetting to Call notifyListeners()

// ❌ Wrong
void increment() {
  _count++;
  // Missing notifyListeners()
}

// âś… Correct
void increment() {
  _count++;
  notifyListeners();
}
Enter fullscreen mode Exit fullscreen mode

2. Using Provider.of with listen: true in Event Handlers

// ❌ Wrong - This can cause issues
ElevatedButton(
  onPressed: () {
    Provider.of<CartProvider>(context).addItem(product);
  },
  child: Text('Add'),
)

// âś… Correct
ElevatedButton(
  onPressed: () {
    Provider.of<CartProvider>(context, listen: false).addItem(product);
  },
  child: Text('Add'),
)
Enter fullscreen mode Exit fullscreen mode

3. Not Disposing Resources

class MyProvider extends ChangeNotifier {
  StreamSubscription? _subscription;

  MyProvider() {
    _subscription = someStream.listen((data) {
      // Handle data
      notifyListeners();
    });
  }

  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

When to Use Provider

Use Provider when:

  • You need simple state management
  • You're building small to medium-sized apps
  • You want minimal boilerplate
  • You're new to Flutter state management

Consider alternatives when:

  • You have complex business logic (consider BLoC)
  • You need time-travel debugging
  • You're building very large applications
  • You need more advanced features like middleware

Performance Tips

  1. Use Consumer strategically - Only wrap widgets that actually need to rebuild
  2. Use Selector for granular updates
  3. Keep your models focused - Don't put everything in one provider
  4. Use const constructors where possible
  5. Consider using Consumer with child parameter for widgets that don't change
Consumer<CartProvider>(
  builder: (context, cart, child) {
    return Column(
      children: [
        Text('Total: ${cart.totalAmount}'),
        child!, // This widget won't rebuild
      ],
    );
  },
  child: ExpensiveWidget(), // Define the static child here
)
Enter fullscreen mode Exit fullscreen mode

Conclusion

Provider is an excellent choice for Flutter state management because it's simple, performant, and officially supported. It strikes a perfect balance between ease of use and power, making it ideal for most Flutter applications.

The key concepts to remember:

  • ChangeNotifier for your data models
  • ChangeNotifierProvider to provide data to your widget tree
  • Consumer to listen to changes and rebuild widgets
  • Always call notifyListeners() when your data changes
  • Use listen: false when you don't need to rebuild

Start with Provider for your next Flutter project, and you'll find that state management doesn't have to be complicated. As your app grows, you can always migrate to more complex solutions if needed.

Have you tried Provider in your Flutter projects? What challenges🏆 did you face? Share your experiences in the comments below! 💬

Top comments (0)