Listenable
Understand how to create custom observable objects with Listenable in Flutter.
What is it?
Listenable is an abstract class that provides the foundation for change notification in Flutter. It represents an object that can have listeners which are notified when the object changes. Both ChangeNotifier and ValueNotifier extend Listenable. Understanding Listenable gives you the ability to create custom observable objects with fine-grained control over change notifications.
Why does it exist?
Listenable exists to:
- Provide a foundation for reactive programming
- Enable custom observable objects
- Support change notification patterns
- Integrate with Flutter's widget system
- Allow efficient UI updates
- Create custom state management solutions
- Support listener-based architectures
Listenable Basics
Understanding Listenable and its usage.
// Import required packages
import 'package:flutter/material.dart';
/// A custom Listenable implementation
/// This demonstrates how to create a custom observable object
class CounterListenable extends Listenable {
// Private list of listeners
// This tracks all the listeners that have been added
final List<VoidCallback> _listeners = [];
// Private state
int _count = 0;
// Getter for the count
int get count => _count;
// Method to increment the counter
void increment() {
_count++;
// Notify all listeners about the change
_notifyListeners();
}
// Method to decrement the counter
void decrement() {
_count--;
_notifyListeners();
}
// Method to add a listener
// This is required by the Listenable interface
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
// Method to remove a listener
// This is required by the Listenable interface
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
// Helper method to notify all listeners
void _notifyListeners() {
// Notify each listener by calling their callback
for (final listener in _listeners) {
listener();
}
}
}
/// Application demonstrating custom Listenable usage
class ListenableExample extends StatefulWidget {
const ListenableExample({super.key});
@override
State<ListenableExample> createState() => _ListenableExampleState();
}
class _ListenableExampleState extends State<ListenableExample> {
// Create an instance of our custom Listenable
final CounterListenable _counter = CounterListenable();
@override
void initState() {
super.initState();
// Add a listener to the counter
// This listener will be called whenever the counter changes
_counter.addListener(() {
// This callback will be executed when _counter notifies listeners
// We could perform side effects here
print('Counter changed to: ${_counter.count}');
});
}
@override
void dispose() {
// Clean up resources
// Remove all listeners to prevent memory leaks
// For this example, we'd need to track the listener reference
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Listenable Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Use AnimatedBuilder to listen to the counter
// AnimatedBuilder will rebuild whenever the counter changes
AnimatedBuilder(
animation: _counter,
builder: (context, child) {
return Text(
'Count: ${_counter.count}',
style: const TextStyle(fontSize: 24),
);
},
),
const SizedBox(height: 16),
// Control buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _counter.decrement,
child: const Text('-'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _counter.increment,
child: const Text('+'),
),
],
),
],
),
),
);
}
}
What's happening here? - CounterListenable implements the Listenable interface - addListener and removeListener manage listeners - _notifyListeners triggers all listeners - AnimatedBuilder listens to Listenable objects - Custom observable objects can be created
Listenable vs ChangeNotifier
Comparing Listenable with ChangeNotifier.
/// Custom Listenable implementation
/// More control over listener management
class CustomListenable extends Listenable {
final List<VoidCallback> _listeners = [];
int _value = 0;
int get value => _value;
void setValue(int newValue) {
if (_value != newValue) {
_value = newValue;
_notifyListeners();
}
}
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
void _notifyListeners() {
for (final listener in _listeners) {
listener();
}
}
}
/// Comparison of Listenable and ChangeNotifier
class ListenableComparison extends StatelessWidget {
const ListenableComparison({super.key});
@override
Widget build(BuildContext context) {
// Custom Listenable
final customListenable = CustomListenable();
// ChangeNotifier (pre-built)
final changeNotifier = ChangeNotifier()..addListener(() {});
return Scaffold(
appBar: AppBar(
title: const Text('Listenable Comparison'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Listenable implementation
_buildListenableSection(customListenable),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
// ChangeNotifier implementation
_buildChangeNotifierSection(changeNotifier),
// Advantages of each approach
const SizedBox(height: 24),
const Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Key Differences:',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('• Listenable: Full control, manual listener management'),
Text('• ChangeNotifier: Built-in notifyListeners(), easier to use'),
Text('• Both: Support AnimatedBuilder and ListenableBuilder'),
],
),
),
),
],
),
),
);
}
Widget _buildListenableSection(CustomListenable listenable) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'Custom Listenable',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
AnimatedBuilder(
animation: listenable,
builder: (context, child) {
return Text('Value: ${listenable.value}');
},
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => listenable.setValue(listenable.value - 1),
child: const Text('-'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => listenable.setValue(listenable.value + 1),
child: const Text('+'),
),
],
),
],
),
),
);
}
Widget _buildChangeNotifierSection(ChangeNotifier notifier) {
// Using a ValueNotifier as an example of ChangeNotifier
final valueNotifier = ValueNotifier<int>(0);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'ChangeNotifier (ValueNotifier)',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
ValueListenableBuilder<int>(
valueListenable: valueNotifier,
builder: (context, value, child) {
return Text('Value: $value');
},
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => valueNotifier.value--,
child: const Text('-'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => valueNotifier.value++,
child: const Text('+'),
),
],
),
],
),
),
);
}
}
What's happening here? - CustomListenable: Manual listener management - ChangeNotifier: Built-in notification system - Both work with AnimatedBuilder - Choose based on control vs convenience
Advanced Listenable Usage
Advanced patterns with Listenable.
/// Combined Listenable that listens to multiple sources
/// This demonstrates how to create complex observable objects
class CompositeListenable extends Listenable {
final List<Listenable> _children;
final List<VoidCallback> _listeners = [];
bool _isListening = false;
CompositeListenable(this._children) {
_startListening();
}
void _startListening() {
if (_isListening) return;
_isListening = true;
// Add a listener to each child
for (final child in _children) {
child.addListener(_notifyListeners);
}
}
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
if (!_isListening) {
_startListening();
}
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
// If no listeners, stop listening to children
if (_listeners.isEmpty && _isListening) {
_stopListening();
}
}
void _stopListening() {
if (!_isListening) return;
_isListening = false;
for (final child in _children) {
child.removeListener(_notifyListeners);
}
}
void _notifyListeners() {
for (final listener in _listeners) {
listener();
}
}
}
/// Advanced Listenable usage example
class AdvancedListenableExample extends StatefulWidget {
const AdvancedListenableExample({super.key});
@override
State<AdvancedListenableExample> createState() => _AdvancedListenableExampleState();
}
class _AdvancedListenableExampleState extends State<AdvancedListenableExample> {
// Create two independent Listenables
final ValueNotifier<int> _counter1 = ValueNotifier<int>(0);
final ValueNotifier<int> _counter2 = ValueNotifier<int>(0);
// Create a composite that combines both
late CompositeListenable _combined;
@override
void initState() {
super.initState();
_combined = CompositeListenable([_counter1, _counter2]);
}
@override
void dispose() {
_combined.dispose();
_counter1.dispose();
_counter2.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Advanced Listenable'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// First counter
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Counter 1:'),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => _counter1.value--,
),
AnimatedBuilder(
animation: _counter1,
builder: (context, child) {
return Text('${_counter1.value}');
},
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _counter1.value++,
),
],
),
],
),
),
),
// Second counter
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Counter 2:'),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => _counter2.value--,
),
AnimatedBuilder(
animation: _counter2,
builder: (context, child) {
return Text('${_counter2.value}');
},
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _counter2.value++,
),
],
),
],
),
),
),
// Combined total (updates when either changes)
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Combined Total:',
style: TextStyle(fontWeight: FontWeight.bold),
),
AnimatedBuilder(
animation: _combined,
builder: (context, child) {
return Text(
'${_counter1.value + _counter2.value}',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
);
},
),
],
),
),
),
],
),
),
);
}
}
What's happening here? - CompositeListenable combines multiple Listenables - Starts/stops listening based on listeners - Efficient resource management - Complex observable hierarchies
Real-World Examples
Common patterns with Listenable.
/// Form validation with Listenable
class FormValidationListenable extends Listenable {
final List<ValueNotifier<bool>> _validators = [];
final List<VoidCallback> _listeners = [];
bool _isValid = false;
bool get isValid => _isValid;
void addValidator(ValueNotifier<bool> validator) {
_validators.add(validator);
validator.addListener(_validate);
_validate(); // Initial validation
}
void _validate() {
bool newValid = true;
for (final validator in _validators) {
if (!validator.value) {
newValid = false;
break;
}
}
if (_isValid != newValid) {
_isValid = newValid;
_notifyListeners();
}
}
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
void _notifyListeners() {
for (final listener in _listeners) {
listener();
}
}
void dispose() {
for (final validator in _validators) {
validator.removeListener(_validate);
}
}
}
/// Listenable with debouncing
class DebouncedListenable extends Listenable {
final Listenable _source;
final Duration delay;
final List<VoidCallback> _listeners = [];
Timer? _timer;
DebouncedListenable(this._source, this.delay) {
_source.addListener(_onSourceChanged);
}
void _onSourceChanged() {
// Cancel previous timer
_timer?.cancel();
// Wait for the delay before notifying
_timer = Timer(delay, _notifyListeners);
}
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
void _notifyListeners() {
for (final listener in _listeners) {
listener();
}
}
void dispose() {
_source.removeListener(_onSourceChanged);
_timer?.cancel();
}
}
What's happening here? - Form validation with multiple validators - Debounced notifications for performance - Custom listener management - Real-world observable patterns
Best Practices
Implement Listener Management
// Good - Proper listener management
class MyListenable extends Listenable {
final List<VoidCallback> _listeners = [];
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
}
Notify Efficiently
// Good - Only notify when needed
void update(value) {
if (_value != value) {
_value = value;
_notifyListeners();
}
}
// Bad - Notify even without changes
void update(value) {
_value = value;
_notifyListeners();
}
Clean Up Resources
// Good - Dispose properly
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late MyListenable _listenable;
@override
void initState() {
super.initState();
_listenable = MyListenable();
// Add listeners
}
@override
void dispose() {
// Remove listeners and dispose
_listenable.dispose();
super.dispose();
}
}
Common Mistakes
Forgetting to Remove Listeners
Wrong:
void initState() {
super.initState();
listenable.addListener(_handleChange);
// No removal in dispose
}
Correct:
void initState() {
super.initState();
listenable.addListener(_handleChange);
}
void dispose() {
listenable.removeListener(_handleChange);
super.dispose();
}
Not Implementing RemoveListener
Wrong:
class MyListenable extends Listenable {
@override
void addListener(VoidCallback listener) {}
// Missing removeListener
}
Correct:
class MyListenable extends Listenable {
final List<VoidCallback> _listeners = [];
@override
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
_listeners.remove(listener);
}
}
Summary
Listenable provides the foundation for reactive programming in Flutter. It enables custom observable objects with listener-based change notification. Use Listenable for custom reactive solutions, advanced patterns, and when you need full control over notification behavior.
Next Steps
Did You Know?
- Listenable is the base class for ChangeNotifier
- AnimatedBuilder works with any Listenable
- Listenable supports multiple listeners
- Listeners can be added and removed
- Listenable enables custom observable objects
- Listenable is lightweight
- Listenable can be combined
- Listenable supports efficient UI updates