Provider: ChangeNotifier and Consumer Patterns
Provider is a sophisticated, high-level wrapper around the InheritedWidget mechanism. Originally created by Remi Rousselet and adopted as a standard recommendation by Google, it significantly reduces the boilerplate code required to implement reactive data propagation and dependency injection (DI) in Flutter.
1. The Core Architecture
The Provider pattern revolves around three foundational concepts:
A. ChangeNotifier: The Data Layer
Your business logic and state are encapsulated in a class that extends ChangeNotifier. Whenever you mutate the data, you invoke notifyListeners(). This informs the framework that any widget observing this model needs to be marked as "dirty" for the next frame.
B. ChangeNotifierProvider: The Injection Layer
This widget is responsible for holding the ChangeNotifier instance and providing it to the subtree.
- Lazy Initialization: By default, the
createfunction is only executed when a descendant first attempts to read the data, potentially saving memory and CPU for unvisited pages. - MultiProvider: In production apps, you often need multiple data sources. Use
MultiProviderto avoid the "Widget Nesting Hell" of multiple individual providers.
C. Consumption Modes: Rebuilding the UI
context.watch<T>(): The simplest method. It establishes a subscription that rebuilds the entire widget whenever the provider notifies. Use this only for small, focused widgets.Consumer<T>: Provides more granular control. Only the widget returned by thebuilderfunction is rebuilt. It also includes achildparameter to pass a pre-built expensive subtree that should not be rebuilt.context.read<T>(): Retrieves the current value without establishing a subscription. This is mandatory for event handlers likeonPressedwhere you want to execute logic but don't need the button itself to rebuild when data changes.
2. Optimization: The Selector Pattern
A standard Consumer rebuilds whenever any field in the ChangeNotifier triggers a notification. For complex models (e.g., a User object with 50 fields), this can cause "Over-Rebuilding."
Selector allows you to "pluck" a single field from a model. The builder only executes if that specific field's value changes, significantly reducing jank in complex UIs.
Selector<CartModel, int>(
selector: (_, cart) => cart.itemCount, // Only watch 'itemCount'
builder: (_, count, __) => Text("Items: $count"),
)
3. Dependency Management: ProxyProvider
Modern apps often have services that depend on other services (e.g., a TodoRepository that needs an AuthToken). ProxyProvider is used to build a model that depends on the values of one or more other providers. Whenever the underlying dependencies change, the ProxyProvider is automatically updated.
4. Engineering Comparison: create vs. value
| Strategy | When to Use | Memory Lifecycle |
|---|---|---|
.create |
When the Provider instantiates the object. | Provider automatically calls .dispose() when the widget is unmounted. |
.value |
When you are passing an existing object (e.g., from a list). | Provider assumes the object is managed elsewhere and never calls .dispose(). |
Warning: Using
.valueto instantiate a new object in theValueconstructor is a common cause of memory leaks because the objects will never be cleaned up.