import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'note.dart'; typedef NoteModificationCallback = void Function(Note note); typedef NoteRenameCallback = void Function(Note note, String oldPath); class NotesNotifier implements ChangeNotifier { ObserverList? _modListeners = ObserverList(); ObserverList? _renameListeners = ObserverList(); void addModifiedListener(NoteModificationCallback listener) { _modListeners?.add(listener); } void removeModifiedListener(NoteModificationCallback listener) { assert(_modListeners!.contains(listener)); _modListeners?.remove(listener); } void addRenameListener(NoteRenameCallback listener) { _renameListeners?.add(listener); } void removeRenameListener(NoteRenameCallback listener) { assert(_renameListeners!.contains(listener)); _renameListeners?.remove(listener); } @mustCallSuper @override void dispose() { assert(_debugAssertNotDisposed()); _modListeners = null; _renameListeners = null; _listeners = null; } // // ChangeNotifier implementation - How to not duplicate this? // ObserverList? _listeners = ObserverList(); bool _debugAssertNotDisposed() { assert(() { if (_listeners == null) { throw FlutterError.fromParts([ ErrorSummary('A $runtimeType was used after being disposed.'), ErrorDescription( 'Once you have called dispose() on a $runtimeType, it can no longer be used.') ]); } return true; }()); return true; } /// Whether any listeners are currently registered. /// /// Clients should not depend on this value for their behavior, because having /// one listener's logic change when another listener happens to start or stop /// listening will lead to extremely hard-to-track bugs. Subclasses might use /// this information to determine whether to do any work when there are no /// listeners, however; for example, resuming a [Stream] when a listener is /// added and pausing it when a listener is removed. /// /// Typically this is used by overriding [addListener], checking if /// [hasListeners] is false before calling `super.addListener()`, and if so, /// starting whatever work is needed to determine when to call /// [notifyListeners]; and similarly, by overriding [removeListener], checking /// if [hasListeners] is false after calling `super.removeListener()`, and if /// so, stopping that same work. @protected @override bool get hasListeners { assert(_debugAssertNotDisposed()); return _listeners!.isNotEmpty; } /// Register a closure to be called when the object changes. /// /// This method must not be called after [dispose] has been called. @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); _listeners!.add(listener); } /// Remove a previously registered closure from the list of closures that are /// notified when the object changes. /// /// If the given listener is not registered, the call is ignored. /// /// This method must not be called after [dispose] has been called. /// /// If a listener had been added twice, and is removed once during an /// iteration (i.e. in response to a notification), it will still be called /// again. If, on the other hand, it is removed as many times as it was /// registered, then it will no longer be called. This odd behavior is the /// result of the [ChangeNotifier] not being able to determine which listener /// is being removed, since they are identical, and therefore conservatively /// still calling all the listeners when it knows that any are still /// registered. /// /// This surprising behavior can be unexpectedly observed when registering a /// listener on two separate objects which are both forwarding all /// registrations to a common upstream object. @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); _listeners!.remove(listener); } /// Call all the registered listeners. /// /// Call this method whenever the object changes, to notify any clients the /// object may have. Listeners that are added during this iteration will not /// be visited. Listeners that are removed during this iteration will not be /// visited after they are removed. /// /// Exceptions thrown by listeners will be caught and reported using /// [FlutterError.reportError]. /// /// This method must not be called after [dispose] has been called. /// /// Surprising behavior can result when reentrantly removing a listener (i.e. /// in response to a notification) that has been registered multiple times. /// See the discussion at [removeListener]. @protected @visibleForTesting @override void notifyListeners() { assert(_debugAssertNotDisposed()); if (_listeners != null) { final List localListeners = List.from(_listeners!); for (VoidCallback listener in localListeners) { try { if (_listeners!.contains(listener)) { listener(); } } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'foundation library', context: ErrorDescription( 'while dispatching notifications for $runtimeType'), informationCollector: () sync* { yield DiagnosticsProperty( 'The $runtimeType sending notification was', this, style: DiagnosticsTreeStyle.errorProperty, ); }, )); } } } } void notifyModifiedListeners(Note note) { assert(_debugAssertNotDisposed()); if (_modListeners != null) { final localListeners = List.from(_modListeners!); for (var listener in localListeners) { try { if (_modListeners!.contains(listener)) { listener(note); } } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'foundation library', context: ErrorDescription( 'while dispatching notifications for $runtimeType'), informationCollector: () sync* { yield DiagnosticsProperty( 'The $runtimeType sending notification was', this, style: DiagnosticsTreeStyle.errorProperty, ); }, )); } } } } void notifyRenameListeners(Note note, String oldPath) { assert(_debugAssertNotDisposed()); if (_renameListeners != null) { final localListeners = List.from(_renameListeners!); for (var listener in localListeners) { try { if (_renameListeners!.contains(listener)) { listener(note, oldPath); } } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'foundation library', context: ErrorDescription( 'while dispatching notifications for $runtimeType'), informationCollector: () sync* { yield DiagnosticsProperty( 'The $runtimeType sending notification was', this, style: DiagnosticsTreeStyle.errorProperty, ); }, )); } } } } }