← Articles

How to Handle State in Flutter with Provider

By Mark · 29 June 20260 views

How to Handle State in Flutter with Provider

State management is one of the most discussed topics in Flutter development. For small apps, setState is perfectly adequate. But as your app grows — multiple screens sharing data, async operations updating the UI, widgets deep in the tree needing global state — you need a more scalable solution. Provider is Flutter's officially recommended state management package for most use cases. This guide explains how to use it effectively.

What Is Provider?

Provider is a wrapper around Flutter's InheritedWidget that makes it dramatically easier to use. At its core, it solves one problem: how do you share data between widgets without passing it down through every constructor (prop drilling)?

Provider places data above the widget tree and lets any descendant widget subscribe to it. When the data changes, only the subscribing widgets rebuild — not the entire tree.

Installing Provider

Add it to your pubspec.yaml:

dependencies:
  provider: ^6.1.2

The Core Pattern: ChangeNotifier

The most common Provider pattern uses ChangeNotifier:

  1. Create a class that extends ChangeNotifier
  2. Expose data as fields and mutate them through methods
  3. Call notifyListeners() after mutations to trigger UI rebuilds
class CartModel extends ChangeNotifier {
  final List<Item> _items = [];

  List<Item> get items => List.unmodifiable(_items);
  int get totalPrice => _items.fold(0, (sum, item) => sum + item.price);

  void addItem(Item item) {
    _items.add(item);
    notifyListeners();
  }

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

Providing the Model

Wrap your app (or the subtree that needs access) with ChangeNotifierProvider:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CartModel(),
      child: const MyApp(),
    ),
  );
}

The create callback receives a BuildContext and returns the notifier. Provider manages the notifier's lifecycle — it disposes it automatically when the provider is removed from the tree.

Consuming the Model

There are three ways to read from a provider:

context.watch — rebuilds on change

class CartBadge extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cart = context.watch<CartModel>();
    return Badge(count: cart.items.length);
  }
}

context.read — one-time read, no rebuild

ElevatedButton(
  onPressed: () {
    context.read<CartModel>().addItem(selectedItem);
  },
  child: const Text('Add to Cart'),
)

Use context.read in callbacks, not in build methods. Using it in build means the widget will not rebuild when the value changes.

context.select — rebuild on partial change

final totalPrice = context.select<CartModel, int>(
  (cart) => cart.totalPrice,
);

select is a performance optimization — the widget only rebuilds when totalPrice changes, not when other fields in CartModel change.

Multiple Providers

For apps with multiple pieces of shared state, use MultiProvider:

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => CartModel()),
    ChangeNotifierProvider(create: (_) => UserModel()),
    ChangeNotifierProvider(create: (_) => ThemeModel()),
  ],
  child: const MyApp(),
)

Handling Async with Provider

class ProductsModel extends ChangeNotifier {
  List<Product> _products = [];
  bool _loading = false;
  String? _error;

  List<Product> get products => _products;
  bool get loading => _loading;
  String? get error => _error;

  Future<void> fetchProducts() async {
    _loading = true;
    _error = null;
    notifyListeners();

    try {
      _products = await api.getProducts();
    } catch (e) {
      _error = e.toString();
    } finally {
      _loading = false;
      notifyListeners();
    }
  }
}

In the widget:

@override
void initState() {
  super.initState();
  context.read<ProductsModel>().fetchProducts();
}

When to Move Beyond Provider

Provider handles most use cases well. Consider alternatives when:

  • Your state logic is very complex with many interdependencies — Riverpod or Bloc offer better structure
  • You need code generation for boilerplate reduction — Riverpod with @riverpod annotations
  • You want strict separation of events and state — Bloc with event/state classes

Provider is not wrong at scale — many production apps use it. But Riverpod addresses several of Provider's edge cases (reading context outside widgets, testing without BuildContext) and is worth learning as a next step.

Conclusion

Provider gives you a clean, testable way to share and react to state changes across your Flutter widget tree. The ChangeNotifier + ChangeNotifierProvider + context.watch/read/select trio covers the vast majority of real-world state management needs. Start with Provider, learn its patterns thoroughly, and you will have a solid foundation for understanding more advanced solutions when you need them.

Sign in to like, dislike, or report.

Comments

No comments yet. Be the first!

Sign in to leave a comment.

How to Handle State in Flutter with Provider — ANN Tech