Ahmet Balaman LogoAhmet Balaman

Flutter: State Management with Provider

personAhmet Balaman
calendar_today
FlutterProviderState ManagementChangeNotifierConsumer

Provider is one of the most popular and Google-recommended packages for state management in Flutter. It is used to share data across the widget tree and listen to changes.

What is State and Why Should It Be Managed?

State is the data condition your application has at any given moment. For example:

  • Whether the user is logged in
  • Number of items in the cart
  • Theme preference (light/dark)
  • Values in form fields
  • Data from APIs

Without state management, sharing this data between widgets and reflecting updates becomes very difficult.

Limitations of setState

Flutter uses setState for simple state management. However, as your app grows, you face serious problems:

1. Prop Drilling Problem

// You have to pass data 5 levels down
GrandParent(
  child: Parent(
    child: Child(
      child: GrandChild(
        child: GreatGrandChild(
          userData: userData, // Must be passed all the way!
        ),
      ),
    ),
  ),
)

2. Unnecessary Rebuilds

When you use setState, the entire widget rebuilds - not just the changed part.

3. State Loss

State can be lost when navigating the widget tree. Data resets when you change pages and return.

4. Untestability

Writing unit tests becomes difficult when UI and business logic are intertwined.

5. Code Repetition

You need to constantly pass parameters to use the same data in multiple places.

How Provider Solves These Problems

✅ Centralized State Management

Data is managed from a single place, accessible from everywhere.

✅ Efficient Rebuilds

Only changed widgets are rebuilt.

✅ Separation of Concerns

Business logic and UI are separated from each other.

✅ Easy Testability

You can easily write tests by mocking Providers.

✅ No Prop Drilling

Widgets that need data read directly from Provider.

When Should You Use Provider?

Situation setState Provider
Simple counter in single widget
Form validation
User info across multiple pages
Cart management
Theme/Language settings
Data from APIs
Complex form states

General Rule: If state is used by multiple widgets or pages, consider Provider.

Why Provider?

Feature setState Provider
Scope Single widget Entire widget tree
Complexity Simple Medium
Scalability Hard Easy
Testability Hard Easy
Prop drilling Required Not needed

⚠️ Important Note: Provider is an external package and does not work in DartPad. To run the examples on your computer, follow the installation steps below.

Installation

Add to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.1

Then run in terminal:

flutter pub get

Core Concepts

1. ChangeNotifier

Class that holds state and notifies changes:

import 'package:flutter/foundation.dart';

class CounterProvider extends ChangeNotifier {
  int _count = 0;
  
  int get count => _count;
  
  void increment() {
    _count++;
    notifyListeners(); // Notify listeners
  }
  
  void decrement() {
    _count--;
    notifyListeners();
  }
  
  void reset() {
    _count = 0;
    notifyListeners();
  }
}

2. ChangeNotifierProvider

Adding Provider to widget tree:

import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterProvider(),
      child: MyApp(),
    ),
  );
}

3. Consumer

Listening to state and updating UI:

Consumer<CounterProvider>(
  builder: (context, counter, child) {
    return Text(
      '${counter.count}',
      style: TextStyle(fontSize: 48),
    );
  },
)

4. context.read and context.watch

// Read (doesn't listen to changes) - Use in buttons
context.read<CounterProvider>().increment();

// Watch (listens to changes) - Use in build method
final count = context.watch<CounterProvider>().count;

Full Example: Counter App

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// 1. State class
class CounterProvider extends ChangeNotifier {
  int _count = 0;
  
  int get count => _count;
  
  void increment() {
    _count++;
    notifyListeners();
  }
  
  void decrement() {
    _count--;
    notifyListeners();
  }
}

// 2. Main
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CounterProvider(),
      child: MaterialApp(
        home: CounterPage(),
      ),
    ),
  );
}

// 3. UI
class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Counter Value:'),
            Consumer<CounterProvider>(
              builder: (context, counter, child) {
                return Text(
                  '${counter.count}',
                  style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
                );
              },
            ),
            SizedBox(height: 32),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FloatingActionButton(
                  onPressed: () => context.read<CounterProvider>().decrement(),
                  child: Icon(Icons.remove),
                ),
                SizedBox(width: 16),
                FloatingActionButton(
                  onPressed: () => context.read<CounterProvider>().increment(),
                  child: Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

MultiProvider

Using multiple providers:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => CounterProvider()),
        ChangeNotifierProvider(create: (_) => ThemeProvider()),
        ChangeNotifierProvider(create: (_) => UserProvider()),
      ],
      child: MyApp(),
    ),
  );
}

Real World Example: Theme Switching

// Theme Provider
class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.light;
  
  ThemeMode get themeMode => _themeMode;
  
  bool get isDarkMode => _themeMode == ThemeMode.dark;
  
  void toggleTheme() {
    _themeMode = isDarkMode ? ThemeMode.light : ThemeMode.dark;
    notifyListeners();
  }
}

// Main
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => ThemeProvider(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return MaterialApp(
          themeMode: themeProvider.themeMode,
          theme: ThemeData.light(),
          darkTheme: ThemeData.dark(),
          home: HomePage(),
        );
      },
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = context.watch<ThemeProvider>();
    
    return Scaffold(
      appBar: AppBar(title: Text('Theme Settings')),
      body: Center(
        child: SwitchListTile(
          title: Text('Dark Mode'),
          value: themeProvider.isDarkMode,
          onChanged: (_) => themeProvider.toggleTheme(),
        ),
      ),
    );
  }
}

Real World Example: Shopping Cart

// Product model
class Product {
  final String id;
  final String name;
  final double price;
  
  Product({required this.id, required this.name, required this.price});
}

// Cart Provider
class CartProvider extends ChangeNotifier {
  final List<Product> _items = [];
  
  List<Product> get items => List.unmodifiable(_items);
  
  int get itemCount => _items.length;
  
  double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
  
  void addItem(Product product) {
    _items.add(product);
    notifyListeners();
  }
  
  void removeItem(String productId) {
    _items.removeWhere((item) => item.id == productId);
    notifyListeners();
  }
  
  void clearCart() {
    _items.clear();
    notifyListeners();
  }
}

// Usage
class ProductCard extends StatelessWidget {
  final Product product;
  
  ProductCard({required this.product});
  
  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(product.name),
        subtitle: Text('\$${product.price}'),
        trailing: IconButton(
          icon: Icon(Icons.add_shopping_cart),
          onPressed: () {
            context.read<CartProvider>().addItem(product);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('${product.name} added to cart')),
            );
          },
        ),
      ),
    );
  }
}

// Cart icon (in AppBar)
class CartIcon extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        IconButton(
          icon: Icon(Icons.shopping_cart),
          onPressed: () {
            // Navigate to cart page
          },
        ),
        Positioned(
          right: 0,
          top: 0,
          child: Consumer<CartProvider>(
            builder: (context, cart, child) {
              return cart.itemCount > 0
                  ? CircleAvatar(
                      radius: 10,
                      backgroundColor: Colors.red,
                      child: Text(
                        '${cart.itemCount}',
                        style: TextStyle(fontSize: 12, color: Colors.white),
                      ),
                    )
                  : SizedBox.shrink();
            },
          ),
        ),
      ],
    );
  }
}

Optimization with Selector

Listen to specific changes only:

// Listens to all changes (inefficient)
Consumer<CartProvider>(
  builder: (context, cart, child) {
    return Text('${cart.itemCount} items');
  },
)

// Rebuilds only when itemCount changes (efficient)
Selector<CartProvider, int>(
  selector: (context, cart) => cart.itemCount,
  builder: (context, itemCount, child) {
    return Text('$itemCount items');
  },
)

Provider Types

Type Usage
Provider Immutable values
ChangeNotifierProvider Mutable state (most common)
FutureProvider Async data
StreamProvider Stream data
ProxyProvider Dependent providers

FutureProvider Example

final userProvider = FutureProvider<User>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  return User.fromJson(jsonDecode(response.body));
});

// Usage
FutureProvider<List<Product>>(
  create: (_) => fetchProducts(),
  initialData: [],
  child: ProductList(),
)

Best Practices

1. Define Provider as high as possible

// ✅ Correct - In main
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => MyProvider(),
      child: MyApp(),
    ),
  );
}

// ❌ Wrong - Deep in tree
class SomePage extends StatelessWidget {
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MyProvider(), // New instance on every build!
      child: ...,
    );
  }
}

2. context.read vs context.watch

// ✅ Use watch in build method
Widget build(BuildContext context) {
  final count = context.watch<CounterProvider>().count;
  return Text('$count');
}

// ✅ Use read in event handlers
onPressed: () {
  context.read<CounterProvider>().increment();
}

// ❌ Don't use read in build method
Widget build(BuildContext context) {
  final count = context.read<CounterProvider>().count; // Won't update
  return Text('$count');
}

3. Keep Consumer as narrow as possible

// ✅ Correct - Only necessary part rebuilds
Scaffold(
  appBar: AppBar(title: Text('Page')),
  body: Consumer<CounterProvider>(
    builder: (context, counter, child) {
      return Text('${counter.count}');
    },
  ),
)

// ❌ Wrong - Entire page rebuilds
Consumer<CounterProvider>(
  builder: (context, counter, child) {
    return Scaffold(
      appBar: AppBar(title: Text('Page')),
      body: Text('${counter.count}'),
    );
  },
)

Summary

  • Provider: Recommended state management solution for Flutter
  • ChangeNotifier: Class that holds and notifies state
  • notifyListeners(): Called to update UI
  • Consumer: Listens to state changes
  • context.watch: Use in build (listens)
  • context.read: Use in events (doesn't listen)
  • MultiProvider: Multiple providers
  • Selector: Performance optimization

Provider is a powerful and flexible state management solution that can be used in Flutter projects of all sizes.

Comments