Flutter Daily: ValueNotifier for Lightweight State Management

ValueNotifier for Lightweight State Management

What You’ll Learn

Today we’ll explore ValueNotifier and ValueListenableBuilder - Flutter’s built-in lightweight solution for managing simple state without pulling in external packages. Perfect for when Provider or Riverpod feels like overkill.

Why ValueNotifier?

When you have a single value that changes (like a counter, toggle state, or loading flag), you don’t always need a full state management solution. ValueNotifier is part of Flutter’s foundation library and provides simple, efficient state updates with automatic widget rebuilding.

The Basics

ValueNotifier<T> is a special type of ChangeNotifier that holds a single value. When you change this value, it notifies all its listeners automatically.

Example: Counter with ValueNotifier

import 'package:flutter/material.dart';

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

class _CounterScreenState extends State<CounterScreen> {
  // Create a ValueNotifier that holds an integer
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);

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

  void _incrementCounter() {
    _counter.value++; // This triggers a rebuild
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ValueNotifier Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            // Only this widget rebuilds when counter changes
            ValueListenableBuilder<int>(
              valueListenable: _counter,
              builder: (context, value, child) {
                return Text(
                  '$value',
                  style: Theme.of(context).textTheme.headlineLarge,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ),
    );
  }
}

Real-World Example: Toggle Dark Mode

class ThemeController {
  final ValueNotifier<bool> isDarkMode = ValueNotifier<bool>(false);

  void toggleTheme() {
    isDarkMode.value = !isDarkMode.value;
  }

  void dispose() {
    isDarkMode.dispose();
  }
}

class MyApp extends StatelessWidget {
  final ThemeController themeController = ThemeController();

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<bool>(
      valueListenable: themeController.isDarkMode,
      builder: (context, isDark, child) {
        return MaterialApp(
          theme: isDark ? ThemeData.dark() : ThemeData.light(),
          home: HomeScreen(themeController: themeController),
        );
      },
    );
  }
}

Key Advantages

  1. No external dependencies - Built into Flutter
  2. Surgical rebuilds - Only ValueListenableBuilder rebuilds, not the entire widget tree
  3. Simple API - Just read .value and set .value
  4. Type-safe - Generic type ensures compile-time safety

When to Use ValueNotifier

When NOT to Use It

Try It Yourself

Build a simple task list where:

  1. Use ValueNotifier<List<String>> to hold tasks
  2. Add a TextField and button to add new tasks
  3. Use ValueListenableBuilder to display the list
  4. Add a counter showing total tasks

Challenge: Make the list update smoothly when tasks are added. Remember that with Lists, you need to create a new list instance to trigger the notifier:

// This WON'T trigger rebuild:
tasks.value.add('New task');

// This WILL trigger rebuild:
tasks.value = [...tasks.value, 'New task'];

Tip of the Day

Always dispose your ValueNotifier instances in the dispose() method to prevent memory leaks. If you forget, your app will keep listeners alive even after the widget is removed from the tree. Use lint rules like always_dispose to catch these automatically.

For complex objects, consider extending ValueNotifier<T> to create custom notifiers with helper methods that encapsulate state changes:

class CounterNotifier extends ValueNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => value++;
  void decrement() => value--;
  void reset() => value = 0;
}

This keeps your UI code clean and business logic encapsulated!