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?
-
ValueNotifier Creation:
ValueNotifier<int>(0)creates a notifier with an initial value of 0. The<int>generic ensures type safety. -
ValueListenableBuilder: This widget listens to the ValueNotifier and rebuilds its child whenever the value changes.
-
Value Update: Setting
counter.value = newValueupdates the value and notifies all listeners. -
Automatic Rebuild: When the value changes, the ValueListenableBuilder automatically rebuilds the Text widget.
-
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?
-
Custom User Class: An immutable class with final fields and a copyWith method for updates.
-
ValueNotifier with User: The ValueNotifier holds a User object and notifies when the object is replaced.
-
Immutable Updates: Using copyWith creates a new object instead of mutating the existing one.
-
Rebuild Triggers: Setting
userNotifier.valueto a new User object triggers the rebuild. -
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?
-
List Management: ValueNotifier holds a List
with immutable updates. -
Immutable Updates: Every modification creates a new list instead of mutating the existing one.
-
Add Item: Creates a new list with the added item and replaces the ValueNotifier value.
-
Remove Item: Creates a new list without the removed item and updates the ValueNotifier.
-
Clear List: Replaces the entire list with an empty list.
-
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?
-
Single Source of Truth: One ValueNotifier serves as the single source of truth.
-
Multiple Listeners: Three different ValueListenableBuilder widgets listen to the same notifier.
-
Different Display Logic: Each listener displays the data differently:
- Listener 1: Shows raw value
- Listener 2: Shows doubled value
-
Listener 3: Shows conditional UI based on value
-
Synchronous Updates: One value update triggers all listeners simultaneously.
-
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?
- setState Approach:
- Uses StatefulWidget
- Rebuilds the entire widget when setState() is called
- Simple but can be inefficient for large widgets
-
Good for small, self-contained widgets
-
ValueNotifier Approach:
- Uses StatelessWidget
- Only rebuilds ValueListenableBuilder children
- More efficient for complex widgets
-
Better for shared state
-
Performance Comparison:
- setState: Rebuilds everything
- ValueNotifier: Rebuilds only listeners
- 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
- ChangeNotifier - More complex state management
- Listenable - Low-level notification system
- ValueListenableBuilder - Detailed widget usage
- Provider - Advanced state management pattern
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