Skip to content

ValueNotifier

Understand how to manage simple reactive values with ValueNotifier.


What is it?

ValueNotifier is a simple, lightweight way to manage a single value that can change over time. It's a special type of ChangeNotifier that holds a single value and notifies listeners when that value changes. ValueNotifier is perfect for managing simple state like booleans, integers, strings, or custom objects where you only need to track one value.

Key Characteristics:

  • Single Value Focus: Designed specifically for managing one value at a time
  • Reactive: Automatically notifies listeners when the value changes
  • Lightweight: Minimal overhead compared to full state management solutions
  • Built-in: Part of Flutter's core framework, no additional dependencies needed
  • Type Safe: Fully supports generics for type safety

When to Use ValueNotifier:

Perfect for: - Simple UI state (toggles, switches, visibility) - Counters and numeric values - Form field values - Theme mode (dark/light) - Authentication status - Loading states - Progress indicators - Any single-value state that multiple widgets need to observe

Not ideal for: - Complex nested state - Multiple related values that need to update together - State that requires complex business logic - Large-scale application state


Why does it exist?

ValueNotifier exists to solve several important problems in Flutter development:

1. Manage Single Values Reactively

Provides a clean way to manage and observe changes to a single value without the complexity of full state management solutions.

2. Provide Lightweight State Management

Offers a minimal, focused solution that doesn't add unnecessary overhead to your application.

3. Enable Automatic UI Updates

Works seamlessly with ValueListenableBuilder to automatically rebuild UI when values change.

4. Reduce Boilerplate Code

Eliminates the need for manual listener management and state update logic.

5. Work with ValueListenableBuilder

Integrates perfectly with Flutter's ValueListenableBuilder widget for efficient UI updates.

6. Support Custom Objects

Can manage any type of value, including custom classes and complex objects.

7. Simplify State Management

Makes reactive state management accessible and easy to understand.


Basic ValueNotifier

Creating and using ValueNotifier.

This example demonstrates the fundamental usage of ValueNotifier with a simple counter application.

Code Example:

import 'package:flutter/material.dart';

/// A StatelessWidget that demonstrates basic ValueNotifier usage.
/// This example shows how to create a ValueNotifier, update its value,
/// and listen to changes using ValueListenableBuilder.
class ValueNotifierExample extends StatelessWidget {
  const ValueNotifierExample({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // 1. CREATE VALUENOTIFIER
    // ============================================================
    // Create a ValueNotifier with an initial value of 0
    // The <int> generic ensures type safety
    final counter = ValueNotifier<int>(0);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ValueNotifier'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // ============================================================
              // 2. VALUE LISTENER BUILDER
              // ============================================================
              // ValueListenableBuilder listens to the ValueNotifier and
              // rebuilds its child whenever the value changes
              ValueListenableBuilder<int>(
                // The ValueNotifier to listen to
                valueListenable: counter,
                // Builder function called when value changes
                // Parameters: context, current value, child (for optimization)
                builder: (context, value, child) {
                  return Text(
                    'Count: $value', // Display the current count
                    style: const TextStyle(fontSize: 24),
                  );
                },
              ),
              const SizedBox(height: 16),
              // ============================================================
              // 3. CONTROL BUTTONS
              // ============================================================
              // These buttons update the ValueNotifier value
              // When value changes, all listeners are notified
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // Decrement button
                  ElevatedButton(
                    onPressed: () {
                      // Update the value directly
                      // This triggers rebuild of all ValueListenableBuilders
                      counter.value--;
                    },
                    child: const Text('-'),
                  ),
                  const SizedBox(width: 8),
                  // Increment button
                  ElevatedButton(
                    onPressed: () {
                      // Updating value notifies all listeners
                      counter.value++;
                    },
                    child: const Text('+'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

What's happening here?

  1. ValueNotifier Creation: ValueNotifier<int>(0) creates a notifier with an initial value of 0. The <int> generic ensures type safety.

  2. ValueListenableBuilder: This widget listens to the ValueNotifier and rebuilds its child whenever the value changes.

  3. Value Update: Setting counter.value = newValue updates the value and notifies all listeners.

  4. Automatic Rebuild: When the value changes, the ValueListenableBuilder automatically rebuilds the Text widget.

  5. Reactive UI: The UI stays in sync with the state without manual calls to setState().

Key Points:

  • ValueNotifier holds a single value
  • ValueListenableBuilder listens for changes
  • Updating value triggers rebuild
  • Simple and efficient
  • No setState() needed

ValueNotifier with Custom Objects

Managing custom objects with ValueNotifier.

This example shows how to use ValueNotifier with custom classes and immutable objects.

Code Example:

import 'package:flutter/material.dart';

// ============================================================
// 1. CUSTOM MODEL CLASS
// ============================================================
// A simple data class representing a user
// Using immutable fields with copyWith method for updates

/// Represents a user with name, age, and email
/// This class is immutable - all fields are final
class User {
  final String name;
  final int age;
  final String email;

  const User({
    required this.name,
    required this.age,
    required this.email,
  });

  // ============================================================
  // COPY WITH METHOD
  // ============================================================
  // Creates a new User with updated fields
  // This maintains immutability while allowing partial updates
  User copyWith({
    String? name,
    int? age,
    String? email,
  }) {
    return User(
      name: name ?? this.name,
      age: age ?? this.age,
      email: email ?? this.email,
    );
  }

  @override
  String toString() => 'User(name: $name, age: $age, email: $email)';
}

/// A StatelessWidget that demonstrates using ValueNotifier with custom objects
class CustomObjectNotifier extends StatelessWidget {
  const CustomObjectNotifier({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // 2. VALUENOTIFIER WITH CUSTOM OBJECT
    // ============================================================
    // Create a ValueNotifier that holds a User object
    // The initial user is created with default values
    final userNotifier = ValueNotifier<User>(
      const User(
        name: 'John Doe',
        age: 25,
        email: 'john@example.com',
      ),
    );

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Custom ValueNotifier'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              // ============================================================
              // 3. DISPLAY USER INFORMATION
              // ============================================================
              // ValueListenableBuilder listens for changes to the User object
              // When the object changes, the UI rebuilds
              ValueListenableBuilder<User>(
                valueListenable: userNotifier,
                builder: (context, user, child) {
                  return Card(
                    child: Padding(
                      padding: const EdgeInsets.all(16),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text('Name: ${user.name}'),
                          Text('Age: ${user.age}'),
                          Text('Email: ${user.email}'),
                        ],
                      ),
                    ),
                  );
                },
              ),
              const SizedBox(height: 16),
              // ============================================================
              // 4. UPDATE BUTTONS
              // ============================================================
              // These buttons demonstrate different ways to update custom objects
              Row(
                children: [
                  // Update name using copyWith
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () {
                        // IMPORTANT: Use copyWith to create a new object
                        // This maintains immutability and triggers rebuild
                        userNotifier.value = userNotifier.value.copyWith(
                          name: 'Jane Doe',
                        );
                      },
                      child: const Text('Change Name'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  // Update age using copyWith
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () {
                        // Partial update using copyWith
                        userNotifier.value = userNotifier.value.copyWith(
                          age: 30,
                        );
                      },
                      child: const Text('Change Age'),
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 8),
              // Complete replacement of object
              ElevatedButton(
                onPressed: () {
                  // Replace entire object with a new one
                  // This also triggers a rebuild
                  userNotifier.value = const User(
                    name: 'Alice Smith',
                    age: 28,
                    email: 'alice@example.com',
                  );
                },
                child: const Text('Reset User'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

What's happening here?

  1. Custom User Class: An immutable class with final fields and a copyWith method for updates.

  2. ValueNotifier with User: The ValueNotifier holds a User object and notifies when the object is replaced.

  3. Immutable Updates: Using copyWith creates a new object instead of mutating the existing one.

  4. Rebuild Triggers: Setting userNotifier.value to a new User object triggers the rebuild.

  5. Complete Replacement: Can replace the entire object with a new instance.

Key Points:

  • Custom User class with immutable fields
  • copyWith method for partial updates
  • ValueNotifier holds the entire object
  • Rebuilds when object changes
  • Maintains immutability

ValueNotifier with Lists

Managing lists with ValueNotifier.

This example demonstrates managing list data with ValueNotifier and immutable updates.

Code Example:

import 'package:flutter/material.dart';

/// A StatelessWidget that demonstrates using ValueNotifier with lists
/// Shows how to add, remove, and clear items in a reactive list
class ListNotifierExample extends StatelessWidget {
  const ListNotifierExample({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // 1. LIST VALUENOTIFIER
    // ============================================================
    // ValueNotifier that holds a List<String>
    // Initialized with an empty list
    final itemsNotifier = ValueNotifier<List<String>>([]);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('List ValueNotifier'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              // ============================================================
              // 2. LIST DISPLAY
              // ============================================================
              // ValueListenableBuilder listens for changes to the list
              // Rebuilds the ListView whenever the list changes
              Expanded(
                child: ValueListenableBuilder<List<String>>(
                  valueListenable: itemsNotifier,
                  builder: (context, items, child) {
                    // Show empty state when no items
                    if (items.isEmpty) {
                      return const Center(
                        child: Text('No items yet'),
                      );
                    }
                    // Display the list of items
                    return ListView.builder(
                      itemCount: items.length,
                      itemBuilder: (context, index) {
                        final item = items[index];
                        return ListTile(
                          title: Text(item),
                          trailing: IconButton(
                            icon: const Icon(Icons.delete),
                            onPressed: () {
                              // ============================================
                              // 3. DELETE ITEM (IMMUTABLE UPDATE)
                              // ============================================
                              // IMPORTANT: Create a new list without the item
                              // This maintains immutability
                              final newList = List<String>.from(items)
                                ..removeAt(index);
                              // Replace the entire list with the new one
                              itemsNotifier.value = newList;
                            },
                          ),
                        );
                      },
                    );
                  },
                ),
              ),
              // ============================================================
              // 4. INPUT AND CONTROLS
              // ============================================================
              Row(
                children: [
                  // Add item from text field
                  Expanded(
                    child: TextField(
                      onSubmitted: (value) {
                        if (value.isNotEmpty) {
                          // ==============================================
                          // 5. ADD ITEM (IMMUTABLE UPDATE)
                          // ==============================================
                          // Create a new list with the item added
                          // This doesn't modify the existing list
                          final newList = List<String>.from(itemsNotifier.value)
                            ..add(value);
                          // Replace the list with the new one
                          itemsNotifier.value = newList;
                        }
                      },
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        hintText: 'Add item',
                      ),
                    ),
                  ),
                  const SizedBox(width: 8),
                  // Clear all items
                  ElevatedButton(
                    onPressed: () {
                      // ==============================================
                      // 6. CLEAR LIST
                      // ==============================================
                      // Replace with empty list
                      itemsNotifier.value = [];
                    },
                    child: const Text('Clear'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

What's happening here?

  1. List Management: ValueNotifier holds a List with immutable updates.

  2. Immutable Updates: Every modification creates a new list instead of mutating the existing one.

  3. Add Item: Creates a new list with the added item and replaces the ValueNotifier value.

  4. Remove Item: Creates a new list without the removed item and updates the ValueNotifier.

  5. Clear List: Replaces the entire list with an empty list.

  6. Automatic Rebuild: The ValueListenableBuilder rebuilds whenever the list changes.

Key Points:

  • List stored in ValueNotifier
  • Immutable updates (create new list)
  • Efficient list management
  • Automatic UI updates
  • No need for setState()

ValueNotifier with Multiple Listeners

Multiple widgets listening to one ValueNotifier.

This example shows how multiple widgets can listen to the same ValueNotifier and display different views of the data.

Code Example:

import 'package:flutter/material.dart';

/// A StatelessWidget that demonstrates multiple listeners
/// Shows how different widgets can respond to the same ValueNotifier
class MultiListenerExample extends StatelessWidget {
  const MultiListenerExample({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // 1. SINGLE VALUENOTIFIER
    // ============================================================
    // One ValueNotifier that multiple widgets will listen to
    final counter = ValueNotifier<int>(0);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Multiple Listeners'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // ============================================================
              // 2. LISTENER 1 - BASIC COUNTER
              // ============================================================
              // Displays the raw counter value
              ValueListenableBuilder<int>(
                valueListenable: counter,
                builder: (context, value, child) {
                  return Text(
                    'Counter 1: $value',
                    style: const TextStyle(fontSize: 20),
                  );
                },
              ),

              // ============================================================
              // 3. LISTENER 2 - MODIFIED VALUE
              // ============================================================
              // Displays twice the counter value
              ValueListenableBuilder<int>(
                valueListenable: counter,
                builder: (context, value, child) {
                  return Text(
                    'Counter 2: ${value * 2}', // Different display logic
                    style: const TextStyle(fontSize: 20),
                  );
                },
              ),

              // ============================================================
              // 4. LISTENER 3 - CONDITIONAL UI
              // ============================================================
              // Changes color and text based on value
              ValueListenableBuilder<int>(
                valueListenable: counter,
                builder: (context, value, child) {
                  return Container(
                    padding: const EdgeInsets.all(8),
                    // Even: green, Odd: red
                    color: value % 2 == 0 ? Colors.green : Colors.red,
                    child: Text(
                      value % 2 == 0 ? 'Even' : 'Odd',
                      style: const TextStyle(color: Colors.white),
                    ),
                  );
                },
              ),

              const SizedBox(height: 16),

              // ============================================================
              // 5. CONTROL BUTTONS
              // ============================================================
              // These update the ValueNotifier for all listeners
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: () {
                      // One update triggers all listeners
                      counter.value--;
                    },
                    child: const Text('-'),
                  ),
                  const SizedBox(width: 8),
                  ElevatedButton(
                    onPressed: () {
                      // All listeners will rebuild with the new value
                      counter.value++;
                    },
                    child: const Text('+'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

What's happening here?

  1. Single Source of Truth: One ValueNotifier serves as the single source of truth.

  2. Multiple Listeners: Three different ValueListenableBuilder widgets listen to the same notifier.

  3. Different Display Logic: Each listener displays the data differently:

  4. Listener 1: Shows raw value
  5. Listener 2: Shows doubled value
  6. Listener 3: Shows conditional UI based on value

  7. Synchronous Updates: One value update triggers all listeners simultaneously.

  8. Efficient Rebuilds: Only the listeners rebuild, not the entire widget tree.

Key Points:

  • Multiple widgets listening to same ValueNotifier
  • Different display logic per widget
  • All update when value changes
  • Efficient single source of truth
  • Only listeners rebuild

ValueNotifier vs setState

Comparing ValueNotifier with setState.

This example compares the two approaches to state management, showing their differences and use cases.

Code Example:

import 'package:flutter/material.dart';

/// A StatelessWidget that compares setState and ValueNotifier approaches
class ComparisonExample extends StatelessWidget {
  const ComparisonExample({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DefaultTabController(
        length: 2,
        child: Scaffold(
          appBar: AppBar(
            bottom: const TabBar(
              tabs: [
                Tab(text: 'setState'),
                Tab(text: 'ValueNotifier'),
              ],
            ),
          ),
          body: TabBarView(
            children: [
              // ============================================================
              // 1. SETSTATE APPROACH
              // ============================================================
              // Uses StatefulWidget and setState() for state management
              const SetStateWidget(),

              // ============================================================
              // 2. VALUENOTIFIER APPROACH
              // ============================================================
              // Uses ValueNotifier and ValueListenableBuilder
              const ValueNotifierWidget(),
            ],
          ),
        ),
      ),
    );
  }
}

// ============================================================
// SETSTATE WIDGET
// ============================================================
// Uses traditional setState() approach
// Good for simple local state within a single widget

/// A StatefulWidget that uses setState() for state management
class SetStateWidget extends StatefulWidget {
  const SetStateWidget({super.key});

  @override
  State<SetStateWidget> createState() => _SetStateWidgetState();
}

class _SetStateWidgetState extends State<SetStateWidget> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // SETSTATE BEHAVIOR
    // ============================================================
    // setState() rebuilds the entire widget tree
    // This is fine for small widgets but inefficient for large ones
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Count: $_counter', style: const TextStyle(fontSize: 24)),
          ElevatedButton(
            onPressed: () {
              // setState triggers rebuild of the entire widget
              setState(() {
                _counter++;
              });
            },
            child: const Text('Increment'),
          ),
        ],
      ),
    );
  }
}

// ============================================================
// VALUENOTIFIER WIDGET
// ============================================================
// Uses ValueNotifier and ValueListenableBuilder
// Only rebuilds the parts that depend on the value

/// A StatelessWidget that uses ValueNotifier for state management
class ValueNotifierWidget extends StatelessWidget {
  const ValueNotifierWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // VALUENOTIFIER BEHAVIOR
    // ============================================================
    // ValueNotifier only rebuilds widgets that listen to it
    // More efficient than setState for complex widgets
    final counter = ValueNotifier<int>(0);

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // Only this builder rebuilds when value changes
          ValueListenableBuilder<int>(
            valueListenable: counter,
            builder: (context, value, child) {
              return Text(
                'Count: $value',
                style: const TextStyle(fontSize: 24),
              );
            },
          ),
          // This button doesn't rebuild with the value changes
          // More efficient than setState approach
          ElevatedButton(
            onPressed: () {
              // Update value triggers rebuild of listeners only
              counter.value++;
            },
            child: const Text('Increment'),
          ),
        ],
      ),
    );
  }
}

What's happening here?

  1. setState Approach:
  2. Uses StatefulWidget
  3. Rebuilds the entire widget when setState() is called
  4. Simple but can be inefficient for large widgets
  5. Good for small, self-contained widgets

  6. ValueNotifier Approach:

  7. Uses StatelessWidget
  8. Only rebuilds ValueListenableBuilder children
  9. More efficient for complex widgets
  10. Better for shared state

  11. Performance Comparison:

  12. setState: Rebuilds everything
  13. ValueNotifier: Rebuilds only listeners
  14. ValueNotifier is more efficient for large widgets

Key Points:

  • setState: Rebuilds entire widget
  • ValueNotifier: Rebuilds only listeners
  • ValueNotifier is more efficient for large widgets
  • Both approaches are valid
  • Choose based on use case

Real-World Examples

Common patterns with ValueNotifier.

This section demonstrates practical applications of ValueNotifier in real-world scenarios.

1. Theme Toggle Example

import 'package:flutter/material.dart';

/// A StatelessWidget that uses ValueNotifier for theme management
/// Shows how to implement dark/light mode toggle
class ThemeToggleExample extends StatelessWidget {
  const ThemeToggleExample({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // 1. THEME STATE
    // ============================================================
    // ValueNotifier tracks dark mode state
    final isDarkMode = ValueNotifier<bool>(false);

    return MaterialApp(
      // ============================================================
      // 2. DYNAMIC THEME
      // ============================================================
      // Theme switches based on ValueNotifier value
      theme: isDarkMode.value ? ThemeData.dark() : ThemeData.light(),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Theme Toggle'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // ============================================================
              // 3. THEME DISPLAY
              // ============================================================
              // Shows current theme mode
              ValueListenableBuilder<bool>(
                valueListenable: isDarkMode,
                builder: (context, value, child) {
                  return Text(
                    value ? 'Dark Mode' : 'Light Mode',
                    style: const TextStyle(fontSize: 20),
                  );
                },
              ),
              const SizedBox(height: 16),
              // ============================================================
              // 4. THEME SWITCH
              // ============================================================
              // Toggles dark mode
              Switch(
                value: isDarkMode.value,
                onChanged: (value) {
                  isDarkMode.value = value; // Triggers theme rebuild
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

What's happening here? - ValueNotifier tracks dark/light mode - ThemeData dynamically switches based on value - Switch toggles the value - App theme updates automatically


2. Form Validation Example

import 'package:flutter/material.dart';

/// A StatelessWidget that uses ValueNotifier for form validation
/// Shows how to create reactive form validation
class FormValidationExample extends StatelessWidget {
  const FormValidationExample({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // 1. FORM STATE
    // ============================================================
    // ValueNotifiers for form fields
    final email = ValueNotifier<String>('');
    final password = ValueNotifier<String>('');
    final isValid = ValueNotifier<bool>(false);

    // ============================================================
    // 2. VALIDATION LOGIC
    // ============================================================
    // Validates form and updates isValid
    void validateForm() {
      isValid.value = email.value.isNotEmpty && password.value.length >= 6;
    }

    // ============================================================
    // 3. ADD LISTENERS
    // ============================================================
    // Listen to form field changes for real-time validation
    email.addListener(validateForm);
    password.addListener(validateForm);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Form Validation'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              // ============================================================
              // 4. EMAIL FIELD
              // ============================================================
              // Reactive email field with validation
              ValueListenableBuilder<String>(
                valueListenable: email,
                builder: (context, value, child) {
                  return TextField(
                    onChanged: (value) => email.value = value,
                    decoration: InputDecoration(
                      labelText: 'Email',
                      border: const OutlineInputBorder(),
                      errorText: value.isEmpty ? 'Email is required' : null,
                    ),
                  );
                },
              ),
              const SizedBox(height: 16),

              // ============================================================
              // 5. PASSWORD FIELD
              // ============================================================
              // Reactive password field with validation
              ValueListenableBuilder<String>(
                valueListenable: password,
                builder: (context, value, child) {
                  return TextField(
                    onChanged: (value) => password.value = value,
                    obscureText: true,
                    decoration: InputDecoration(
                      labelText: 'Password',
                      border: const OutlineInputBorder(),
                      errorText: value.length < 6 ? 
                        'Password must be at least 6 characters' : null,
                    ),
                  );
                },
              ),
              const SizedBox(height: 16),

              // ============================================================
              // 6. SUBMIT BUTTON
              // ============================================================
              // Disabled until form is valid
              ValueListenableBuilder<bool>(
                valueListenable: isValid,
                builder: (context, value, child) {
                  return ElevatedButton(
                    onPressed: value ? () {} : null, // Enabled only when valid
                    child: const Text('Submit'),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

What's happening here? - ValueNotifiers track email and password - Real-time validation with listeners - Submit button enabled based on validity - Reactive error messages


3. Shopping Cart Counter Example

import 'package:flutter/material.dart';

/// A StatelessWidget that uses ValueNotifier for shopping cart
/// Shows how to manage a cart with multiple UI elements
class CartCounterExample extends StatelessWidget {
  const CartCounterExample({super.key});

  @override
  Widget build(BuildContext context) {
    // ============================================================
    // 1. CART STATE
    // ============================================================
    // ValueNotifier holds the list of cart items
    final cartItems = ValueNotifier<List<String>>([]);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Cart'),
          actions: [
            // ============================================================
            // 2. CART COUNTER IN APP BAR
            // ============================================================
            // Displays item count in the app bar
            ValueListenableBuilder<List<String>>(
              valueListenable: cartItems,
              builder: (context, items, child) {
                return Container(
                  padding: const EdgeInsets.all(8),
                  child: Text(
                    'Items: ${items.length}',
                    style: const TextStyle(color: Colors.white),
                  ),
                );
              },
            ),
          ],
        ),
        body: Column(
          children: [
            // ============================================================
            // 3. CART LIST
            // ============================================================
            // Displays all items in the cart
            Expanded(
              child: ValueListenableBuilder<List<String>>(
                valueListenable: cartItems,
                builder: (context, items, child) {
                  if (items.isEmpty) {
                    return const Center(child: Text('Cart is empty'));
                  }
                  return ListView.builder(
                    itemCount: items.length,
                    itemBuilder: (context, index) {
                      return ListTile(
                        title: Text(items[index]),
                        trailing: IconButton(
                          icon: const Icon(Icons.remove),
                          onPressed: () {
                            // ============================================
                            // 4. REMOVE ITEM
                            // ============================================
                            // Create new list without the item
                            final newItems = List<String>.from(items)
                              ..removeAt(index);
                            cartItems.value = newItems;
                          },
                        ),
                      );
                    },
                  );
                },
              ),
            ),
            // ============================================================
            // 5. CART CONTROLS
            // ============================================================
            // Add and clear buttons
            Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () {
                        // ==============================================
                        // 6. ADD ITEM
                        // ==============================================
                        // Create new list with item added
                        final newItems = List<String>.from(cartItems.value)
                          ..add('Item ${cartItems.value.length + 1}');
                        cartItems.value = newItems;
                      },
                      child: const Text('Add Item'),
                    ),
                  ),
                  const SizedBox(width: 8),
                  // Clear cart
                  ElevatedButton(
                    onPressed: () {
                      // ==============================================
                      // 7. CLEAR CART
                      // ==============================================
                      // Replace with empty list
                      cartItems.value = [];
                    },
                    child: const Text('Clear'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

What's happening here? - ValueNotifier tracks cart items - Multiple UI elements update together - Cart counter in app bar updates automatically - Add and remove items with immutable updates


Best Practices

1. Use ValueNotifier for Single Values

// ✅ Good - Single value
final counter = ValueNotifier<int>(0);

// ✅ Good - Single boolean
final isLoggedIn = ValueNotifier<bool>(false);

// ❌ Bad - Complex state with multiple values
// Use ChangeNotifier or a state management solution instead
final state = ValueNotifier<Map<String, dynamic>>({});

2. Use Immutable Updates

// ✅ Good - Immutable update with copyWith
userNotifier.value = userNotifier.value.copyWith(name: 'New Name');

// ✅ Good - Creating new list
final newList = List<String>.from(itemsNotifier.value)..add('New Item');
itemsNotifier.value = newList;

// ❌ Bad - Mutating existing object
final user = userNotifier.value;
user.name = 'New Name'; // Won't trigger rebuild
userNotifier.value = user;

// ❌ Bad - Mutating list
itemsNotifier.value.add('New Item'); // Won't trigger rebuild

3. Dispose When Not Needed

// ✅ Good - Dispose in StatefulWidget
class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final ValueNotifier<int> counter = ValueNotifier<int>(0);

  @override
  void dispose() {
    counter.dispose(); // Prevent memory leaks
    super.dispose();
  }
}

// ✅ Good - Dispose in StatefulWidget with multiple notifiers
class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final ValueNotifier<String> name = ValueNotifier<String>('John');
  final ValueNotifier<int> age = ValueNotifier<int>(25);

  @override
  void dispose() {
    name.dispose();
    age.dispose();
    super.dispose();
  }
}

4. Use ValueListenableBuilder for UI Updates

// ✅ Good - Using ValueListenableBuilder
ValueListenableBuilder<int>(
  valueListenable: counter,
  builder: (context, value, child) {
    return Text('Count: $value');
  },
)

// ❌ Bad - Manual listeners (more complex)
counter.addListener(() {
  setState(() {}); // Not the ValueNotifier way
});

5. Consider Performance with Large Objects

// ✅ Good - Use const for static objects
const defaultUser = User(name: 'Default', age: 0);

// ✅ Good - Use factory constructors for complex objects
class User {
  final String name;
  final int age;

  const User(this.name, this.age);

  factory User.defaultUser() => const User('Default', 0);
}

// ❌ Bad - Creating new objects unnecessarily
userNotifier.value = User('Default', 0); // Consider const

Common Mistakes

1. Mutating Objects Directly

// ❌ WRONG - Mutating without creating new object
final user = userNotifier.value;
user.age = 30; // Changes the object but may not trigger rebuild
userNotifier.value = user; // This notifies listeners

// ✅ CORRECT - Immutable update
userNotifier.value = userNotifier.value.copyWith(age: 30);

2. Forgetting to Dispose

// ❌ WRONG - Memory leak
class MyWidget extends StatefulWidget {
  final ValueNotifier<int> counter = ValueNotifier<int>(0);
  // No dispose - memory leak!
}

// ✅ CORRECT - Dispose properly
class MyWidget extends StatefulWidget {
  final ValueNotifier<int> counter = ValueNotifier<int>(0);

  @override
  void dispose() {
    counter.dispose();
    super.dispose();
  }
}

3. Updating Value Outside of UI Thread

// ❌ WRONG - Updating from background thread
void backgroundTask() {
  // This is dangerous if not handled properly
  counter.value = 10; // Might cause issues in async operations
}

// ✅ CORRECT - Use WidgetsBinding to ensure UI thread
void backgroundTask() {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    counter.value = 10; // Safe update on UI thread
  });
}

4. Not Using const for Initial Values

// ❌ WRONG - Creating new object each time
final userNotifier = ValueNotifier<User>(
  User(name: 'John', age: 25), // Creates new object
);

// ✅ CORRECT - Using const for better performance
final userNotifier = ValueNotifier<User>(
  const User(name: 'John', age: 25), // Reusable object
);

5. Too Many Listeners

// ❌ WRONG - Adding too many listeners (performance issue)
for (int i = 0; i < 1000; i++) {
  counter.addListener(() {
    // This can cause performance problems
    print('Value changed');
  });
}

// ✅ CORRECT - Use fewer listeners or batch updates
counter.addListener(() {
  // Process updates efficiently
  batchUpdate();
});

Summary

ValueNotifier provides simple reactive state management for single values. Use ValueNotifier with ValueListenableBuilder for efficient UI updates.

Key Takeaways:

Simple and lightweight - Minimal setup, no boilerplate ✅ Reactive - Automatically updates UI when values change ✅ Type-safe - Works with generics for any value type ✅ Efficient - Only rebuilds widgets that depend on the value ✅ Flexible - Works with simple types, custom objects, and lists ✅ Built-in - Part of Flutter framework, no dependencies needed

When to Use:

  • ✅ Simple UI state (toggles, switches, visibility)
  • ✅ Counters and numeric values
  • ✅ Form field values and validation
  • ✅ Theme mode (dark/light)
  • ✅ Loading states and progress indicators
  • ✅ Any single-value state that multiple widgets need to observe

When to Avoid:

  • ❌ Complex nested state (use ChangeNotifier or a state management solution)
  • ❌ Multiple related values that need to update together
  • ❌ State that requires complex business logic
  • ❌ Large-scale application state (use Provider, Riverpod, BLoC, etc.)

Next Steps


Did You Know?

  • 💡 ValueNotifier is a subclass of ChangeNotifier
  • 💡 ValueListenableBuilder only rebuilds when the value changes
  • 💡 ValueNotifier is lightweight and built into Flutter
  • 💡 Works with any value type (int, String, custom objects)
  • 💡 Supports multiple listeners for the same value
  • 💡 Immutable updates are recommended for custom objects
  • 💡 Always dispose ValueNotifier to prevent memory leaks
  • 💡 ValueNotifier can be used with ListenableBuilder for custom UI updates