Synchronizing state with observers in Dart and Flutter

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.

  1. The Traditional Approach
  2. Extended Observer Pattern
    1. Internet Connectivity Observer
    2. Log Observer
  3. Conclusion

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:

  1. Need to manually track previous values
  2. Difficult to maintain as the number of fields grows
  3. Easy to introduce bugs in the comparison logic

Extended Observer Pattern

Let's introduce a more elegant solution using dedicated observers for specific changes:

Read more