Question #60EasyFlutter Basics

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

dart
class 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

FeaturesetStateProvider
ScopeSingle widgetMultiple widgets/app-wide
State locationInside State classSeparate model class
RebuildsEntire widgetOnly listeners (Consumer/Selector)
State sharingHard (pass callbacks)Easy (access from context)
BoilerplateLowMedium
TestabilityMediumHigh (separate business logic)
ArchitectureNo separationClean separation (MVVM)
Learning curveEasyModerate
Best forSimple UI stateApp 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

dart
class 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

dart
class 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:

dart
class 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)

dart
class 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

PracticesetStateProvider
Use forLocal UI stateShared app state
Keep stateIn State classIn model class
Minimize rebuildsSplit widgetsUse Consumer/Selector
TestingTest widgetTest model separately

Key Takeaways

setState: Simple, local state for single widget. Call

text
setState(() { })
to rebuild.

Provider: Shared state across widgets. Use

text
ChangeNotifier
+
text
Consumer
pattern.

Use both: Local UI state with setState, global app state with Provider.


Resources