Flutter difference between set state and provider ?
#flutter#state#provider
Answer
Overview
setState and Provider are both state management solutions, but they work at different levels and are suited for different scenarios.
setState
Definition: A method in StatefulWidget that rebuilds the widget when local state changes.
How It Works
dartclass CounterPage extends StatefulWidget { _CounterPageState createState() => _CounterPageState(); } class _CounterPageState extends State<CounterPage> { int _counter = 0; // Local state void _increment() { setState(() { _counter++; // Update state and rebuild }); } Widget build(BuildContext context) { return Scaffold( body: Center(child: Text('$_counter')), floatingActionButton: FloatingActionButton( onPressed: _increment, child: Icon(Icons.add), ), ); } }
Characteristics
- Scope: Single widget (local state)
- Rebuilds: Only the widget that calls setState
- State location: Inside the State class
- State sharing: Difficult (requires passing callbacks)
- Use case: Simple, widget-specific state
Provider
Definition: A state management package that provides state across the widget tree using InheritedWidget.
Installation
yaml# pubspec.yaml dependencies: provider: ^6.1.1
How It Works
dart// 1. Create model class class Counter extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); // Notify widgets listening to this } } // 2. Provide at root void main() { runApp( ChangeNotifierProvider( create: (context) => Counter(), child: MyApp(), ), ); } // 3. Consume in widgets class CounterPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: Center( // Listen to changes child: Consumer<Counter>( builder: (context, counter, child) { return Text('${counter.count}'); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { // Access without rebuilding context.read<Counter>().increment(); }, child: Icon(Icons.add), ), ); } }
Characteristics
- Scope: App-wide or subtree (shared state)
- Rebuilds: Only widgets that listen to changes
- State location: Separate model class
- State sharing: Easy (access from any descendant)
- Use case: Shared state across multiple widgets
Key Differences
| Feature | setState | Provider |
|---|---|---|
| Scope | Single widget | Multiple widgets/app-wide |
| State location | Inside State class | Separate model class |
| Rebuilds | Entire widget | Only listeners (Consumer/Selector) |
| State sharing | Hard (pass callbacks) | Easy (access from context) |
| Boilerplate | Low | Medium |
| Testability | Medium | High (separate business logic) |
| Architecture | No separation | Clean separation (MVVM) |
| Learning curve | Easy | Moderate |
| Best for | Simple UI state | App state, shared data |
When to Use setState
✅ Use when:
- State is local to a single widget
- Simple UI interactions (toggle, expand/collapse)
- Form validation
- Animation controllers
- Temporary UI state
Examples
dart// Example 1: Toggle visibility class ToggleWidget extends StatefulWidget { _ToggleWidgetState createState() => _ToggleWidgetState(); } class _ToggleWidgetState extends State<ToggleWidget> { bool _isVisible = false; Widget build(BuildContext context) { return Column( children: [ ElevatedButton( onPressed: () => setState(() => _isVisible = !_isVisible), child: Text('Toggle'), ), if (_isVisible) Text('Visible content'), ], ); } } // Example 2: Text field class TextInputWidget extends StatefulWidget { _TextInputWidgetState createState() => _TextInputWidgetState(); } class _TextInputWidgetState extends State<TextInputWidget> { String _text = ''; Widget build(BuildContext context) { return Column( children: [ TextField(onChanged: (value) => setState(() => _text = value)), Text('You typed: $_text'), ], ); } }
When to Use Provider
✅ Use when:
- State needs to be shared across multiple widgets
- Managing app-level state (auth, theme, settings)
- Complex business logic
- Need separation of concerns
- Testing business logic independently
Examples
dart// Example 1: Shopping cart (shared state) class CartModel extends ChangeNotifier { final List<Product> _items = []; List<Product> get items => _items; int get totalItems => _items.length; void addItem(Product product) { _items.add(product); notifyListeners(); } void removeItem(Product product) { _items.remove(product); notifyListeners(); } } // Provide at root MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => CartModel()), ], child: MyApp(), ) // Use in multiple widgets class ProductCard extends StatelessWidget { final Product product; ProductCard(this.product); Widget build(BuildContext context) { return Card( child: ElevatedButton( onPressed: () { context.read<CartModel>().addItem(product); }, child: Text('Add to Cart'), ), ); } } class CartBadge extends StatelessWidget { Widget build(BuildContext context) { return Consumer<CartModel>( builder: (context, cart, child) { return Badge( label: Text('${cart.totalItems}'), child: Icon(Icons.shopping_cart), ); }, ); } } // Example 2: Authentication state class AuthModel extends ChangeNotifier { User? _user; bool get isAuthenticated => _user != null; User? get user => _user; Future<void> login(String email, String password) async { _user = await authService.login(email, password); notifyListeners(); } void logout() { _user = null; notifyListeners(); } }
Performance Comparison
setState - Rebuilds Entire Widget
dartclass BadPerformance extends StatefulWidget { _BadPerformanceState createState() => _BadPerformanceState(); } class _BadPerformanceState extends State<BadPerformance> { int _counter = 0; Widget build(BuildContext context) { return Column( children: [ ExpensiveWidget(), // Rebuilds unnecessarily! Text('$_counter'), ElevatedButton( onPressed: () => setState(() => _counter++), child: Text('Increment'), ), ], ); } }
Provider - Rebuilds Only Listeners
dartclass GoodPerformance extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ ExpensiveWidget(), // Does NOT rebuild Consumer<Counter>( builder: (context, counter, child) { return Text('${counter.count}'); // Only this rebuilds }, ), ElevatedButton( onPressed: () => context.read<Counter>().increment(), child: Text('Increment'), ), ], ); } }
Combining Both
You can use both together:
dartclass MyWidget extends StatefulWidget { _MyWidgetState createState() => _MyWidgetState(); } class _MyWidgetState extends State<MyWidget> { bool _isExpanded = false; // Local UI state (setState) Widget build(BuildContext context) { return Consumer<UserModel>( // Global state (Provider) builder: (context, userModel, child) { return Column( children: [ Text('User: ${userModel.name}'), ElevatedButton( onPressed: () => setState(() => _isExpanded = !_isExpanded), child: Text('Toggle'), ), if (_isExpanded) Text('Expanded content'), ], ); }, ); } }
Migration Example
Before (setState only)
dartclass HomePage extends StatefulWidget { _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { List<Product> _cart = []; void _addToCart(Product product) { setState(() => _cart.add(product)); } Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Cart: ${_cart.length}'), ), body: ProductList(onAddToCart: _addToCart), ); } }
Problems:
- Hard to share cart state with other screens
- Need to pass callbacks down the tree
- Business logic mixed with UI
After (Provider)
dart// Model class CartModel extends ChangeNotifier { List<Product> _cart = []; List<Product> get cart => _cart; int get itemCount => _cart.length; void addToCart(Product product) { _cart.add(product); notifyListeners(); } } // UI class HomePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Consumer<CartModel>( builder: (context, cart, child) { return Text('Cart: ${cart.itemCount}'); }, ), ), body: ProductList(), ); } } class ProductList extends StatelessWidget { Widget build(BuildContext context) { return ListView( children: products.map((product) { return ListTile( title: Text(product.name), trailing: IconButton( icon: Icon(Icons.add_shopping_cart), onPressed: () { context.read<CartModel>().addToCart(product); }, ), ); }).toList(), ); } }
Benefits:
- Cart accessible from any widget
- Business logic separated
- Easy to test
- No callback drilling
Best Practices
| Practice | setState | Provider |
|---|---|---|
| Use for | Local UI state | Shared app state |
| Keep state | In State class | In model class |
| Minimize rebuilds | Split widgets | Use Consumer/Selector |
| Testing | Test widget | Test model separately |
Key Takeaways
setState: Simple, local state for single widget. Call
to rebuild.textsetState(() { })
Provider: Shared state across widgets. Use
+textChangeNotifierpattern.textConsumer
Use both: Local UI state with setState, global app state with Provider.