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