State Management with ValueNotifier and ChangeNotifier

State Management with ValueNotifier and ChangeNotifier

What You’ll Learn

Discover Flutter’s built-in lightweight state management using ValueNotifier and ChangeNotifier. These powerful tools let you manage app state efficiently without external packages, perfect for small to medium-sized apps.

Why ValueNotifier and ChangeNotifier?

Before diving into complex state management solutions like Provider or Riverpod, master these built-in Flutter tools. They’re the foundation many packages are built on.

When to use:

Benefits:

ValueNotifier: For Simple Values

ValueNotifier wraps a single value and notifies listeners when it changes. Think of it as a reactive variable.

Basic Example:

class CounterScreen extends StatefulWidget {
  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  // Create a ValueNotifier with initial value
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);

  @override
  void dispose() {
    _counter.dispose(); // Always dispose to prevent memory leaks
    super.dispose();
  }

  void _increment() {
    _counter.value++; // Updates value and notifies listeners automatically
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ValueNotifier Counter')),
      body: Center(
        // ValueListenableBuilder rebuilds only when value changes
        child: ValueListenableBuilder<int>(
          valueListenable: _counter,
          builder: (context, count, child) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  'Count: $count',
                  style: TextStyle(fontSize: 48),
                ),
                // This child widget is NOT rebuilt (performance optimization)
                if (child != null) child,
              ],
            );
          },
          // Static child passed to builder (won't rebuild)
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Text('This text never rebuilds!'),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Key points:

ChangeNotifier: For Complex State

When you need to manage multiple values or complex state, use ChangeNotifier. It’s like ValueNotifier but handles multiple properties.

Example: Shopping Cart

// 1. Create a model that extends ChangeNotifier
class ShoppingCart extends ChangeNotifier {
  final List<CartItem> _items = [];

  // Getters
  List<CartItem> get items => List.unmodifiable(_items);
  int get itemCount => _items.length;

  double get totalPrice {
    return _items.fold(0, (sum, item) => sum + item.price);
  }

  // Methods that modify state
  void addItem(CartItem item) {
    _items.add(item);
    notifyListeners(); // Notify all listeners about the change
  }

  void removeItem(String itemId) {
    _items.removeWhere((item) => item.id == itemId);
    notifyListeners();
  }

  void clear() {
    _items.clear();
    notifyListeners();
  }

  bool contains(String itemId) {
    return _items.any((item) => item.id == itemId);
  }
}

class CartItem {
  final String id;
  final String name;
  final double price;

  CartItem({required this.id, required this.name, required this.price});
}

// 2. Use it in your widget
class ShoppingCartScreen extends StatefulWidget {
  @override
  State<ShoppingCartScreen> createState() => _ShoppingCartScreenState();
}

class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
  final ShoppingCart _cart = ShoppingCart();

  @override
  void dispose() {
    _cart.dispose(); // ChangeNotifier needs disposal
    super.dispose();
  }

  void _addSampleItem() {
    final item = CartItem(
      id: DateTime.now().toString(),
      name: 'Product ${_cart.itemCount + 1}',
      price: 10.0 + (_cart.itemCount * 5),
    );
    _cart.addItem(item);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping Cart'),
        actions: [
          // Listen to cart changes
          AnimatedBuilder(
            animation: _cart,
            builder: (context, child) {
              return Center(
                child: Padding(
                  padding: EdgeInsets.symmetric(horizontal: 16),
                  child: Text(
                    '\$${_cart.totalPrice.toStringAsFixed(2)}',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                ),
              );
            },
          ),
        ],
      ),
      body: AnimatedBuilder(
        animation: _cart,
        builder: (context, child) {
          if (_cart.items.isEmpty) {
            return Center(child: Text('Cart is empty'));
          }

          return ListView.builder(
            itemCount: _cart.items.length,
            itemBuilder: (context, index) {
              final item = _cart.items[index];
              return ListTile(
                title: Text(item.name),
                subtitle: Text('\$${item.price.toStringAsFixed(2)}'),
                trailing: IconButton(
                  icon: Icon(Icons.delete, color: Colors.red),
                  onPressed: () => _cart.removeItem(item.id),
                ),
              );
            },
          );
        },
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'add',
            onPressed: _addSampleItem,
            child: Icon(Icons.add_shopping_cart),
          ),
          SizedBox(height: 8),
          FloatingActionButton(
            heroTag: 'clear',
            onPressed: _cart.clear,
            child: Icon(Icons.clear),
          ),
        ],
      ),
    );
  }
}

ValueNotifier vs ChangeNotifier

FeatureValueNotifierChangeNotifier
Best forSingle valueMultiple values/complex state
WidgetValueListenableBuilderAnimatedBuilder or ListenableBuilder
BoilerplateMinimalSlightly more
Use caseToggles, counters, selectionsModels, controllers, services

Combining Multiple Notifiers

Need to listen to multiple ValueNotifiers? Use Listenable.merge:

final name = ValueNotifier<String>('');
final age = ValueNotifier<int>(0);

// Listen to both
ValueListenableBuilder(
  valueListenable: Listenable.merge([name, age]),
  builder: (context, child) {
    return Text('${name.value}, Age: ${age.value}');
  },
)

Try It Yourself

Build a theme switcher using ValueNotifier:

  1. Create a ValueNotifier<bool> for dark mode state
  2. Use ValueListenableBuilder to rebuild MaterialApp with theme
  3. Add a toggle switch to change themes
  4. Persist the preference using SharedPreferences

Challenge: Build a multi-step form wizard with ChangeNotifier that:

Tip of the Day

Performance trick: Use ValueListenableBuilder’s child parameter for widgets that don’t depend on the value. They’ll be passed through without rebuilding, saving precious CPU cycles.

Debugging: Override toString() in your ChangeNotifier classes to see state in Flutter DevTools. Add debugPrint() in notifyListeners() to track when rebuilds happen.

Bridge to Provider: Once comfortable with ChangeNotifier, learning Provider is easy—it’s just ChangeNotifier with dependency injection built in!