Synchronizing state with observers in Dart and Flutter
Flutter's Listenable
interface and ChangeNotifier
mixin provide a foundational approach to state management, allowing objects to notify listeners when their internal state changes. While powerful, this basic pattern can lead to verbose and hard-to-maintain code in more complex applications. This article introduces an enhanced version of the observer pattern that addresses these limitations.
The Traditional Approach
Let's start with a typical implementation using ChangeNotifier
. Consider a simple app settings manager:
class SimpleAppSettings with ChangeNotifier {
bool _notificationsEnabled = false;
String _someAuthenticationToken = '';
bool get notificationsEnabled => _notificationsEnabled;
String get someAuthenticationToken => _someAuthenticationToken;
void setNotificationsEnabled(bool enabled) {
_notificationsEnabled = enabled;
notifyListeners();
}
void setSomeAuthenticationToken(String token) {
_someAuthenticationToken = token;
notifyListeners();
}
}
To react to changes in these settings, we typically create a widget that listens to the settings object and manually compares previous values:
class SettingsListener extends StatefulWidget {
const SettingsListener({
required this.child,
super.key,
});
final Widget child;
@override
State<SettingsListener> createState() => _SettingsListenerState();
}
class _SettingsListenerState extends State<SettingsListener> {
var _token = '';
var _notificationsEnabled = false;
@override
void initState() {
super.initState();
settings.addListener(_onSettingsChanged);
}
@override
void dispose() {
settings.removeListener(_onSettingsChanged);
super.dispose();
}
void _onSettingsChanged() {
// if token changed show snackbar
if (_token != settings.someAuthenticationToken) {
_token = settings.someAuthenticationToken;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Token changed to ${settings.someAuthenticationToken}'),
),
);
}
if (_notificationsEnabled != settings.notificationsEnabled) {
_notificationsEnabled = settings.notificationsEnabled;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Notifications enabled: ${settings.notificationsEnabled}'),
),
);
}
}
@override
Widget build(BuildContext context) => widget.child;
}
This approach has several drawbacks:
- Need to manually track previous values
- Difficult to maintain as the number of fields grows
- Easy to introduce bugs in the comparison logic
Extended Observer Pattern
Let's introduce a more elegant solution using dedicated observers for specific changes: