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
- No external dependencies - Built into Flutter
- Surgical rebuilds - Only
ValueListenableBuilderrebuilds, not the entire widget tree - Simple API - Just read
.valueand set.value - Type-safe - Generic type ensures compile-time safety
When to Use ValueNotifier
- Single values that change independently
- UI state like loading flags, visibility toggles
- Simple counters or form field states
- Theme preferences
- Filter selections
When NOT to Use It
- Complex state with multiple related values
- State that needs middleware or side effects
- State shared across many distant widgets
- Application-wide state management
Try It Yourself
Build a simple task list where:
- Use
ValueNotifier<List<String>>to hold tasks - Add a
TextFieldand button to add new tasks - Use
ValueListenableBuilderto display the list - 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!