From 2711f1c4c98d019b0c99938630fd43d8ea4821f0 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Sat, 1 Feb 2020 13:14:24 +0100 Subject: [PATCH] Add a NotesFolderNotifier This also implements the ChangeNotifier, but additional provides more fine grained notifications. We neeed this in order to animate the list of notes / folders when they change. --- lib/core/notes_folder.dart | 4 +- lib/core/notes_folder_notifier.dart | 268 ++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 lib/core/notes_folder_notifier.dart diff --git a/lib/core/notes_folder.dart b/lib/core/notes_folder.dart index 873d7c88..8e375632 100644 --- a/lib/core/notes_folder.dart +++ b/lib/core/notes_folder.dart @@ -1,13 +1,13 @@ import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import 'package:path/path.dart'; import 'note.dart'; import 'note_fs_entity.dart'; +import 'notes_folder_notifier.dart'; -class NotesFolder with ChangeNotifier implements Comparable { +class NotesFolder with NotesFolderNotifier implements Comparable { final NotesFolder parent; String folderPath; diff --git a/lib/core/notes_folder_notifier.dart b/lib/core/notes_folder_notifier.dart new file mode 100644 index 00000000..936a0f18 --- /dev/null +++ b/lib/core/notes_folder_notifier.dart @@ -0,0 +1,268 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +import 'note.dart'; +import 'notes_folder.dart'; + +typedef FolderNotificationCallback = void Function( + int index, NotesFolder folder); +typedef NoteNotificationCallback = void Function(int index, Note note); + +class NotesFolderNotifier implements ChangeNotifier { + var _folderAddedListeners = ObserverList(); + var _folderRemovedListeners = ObserverList(); + + var _noteAddedListeners = ObserverList(); + var _noteRemovedListeners = ObserverList(); + + void addFolderRemovedListener(FolderNotificationCallback listener) { + _folderRemovedListeners.add(listener); + } + + void removeFolderRemovedListener(FolderNotificationCallback listener) { + _folderRemovedListeners.remove(listener); + } + + void addFolderAddedListener(FolderNotificationCallback listener) { + _folderAddedListeners.add(listener); + } + + void removeFolderAddedListener(FolderNotificationCallback listener) { + _folderAddedListeners.remove(listener); + } + + void addNoteAddedListener(NoteNotificationCallback listener) { + _noteAddedListeners.add(listener); + } + + void removeNoteAddedListener(NoteNotificationCallback listener) { + _noteAddedListeners.remove(listener); + } + + void addNoteRemovedListener(NoteNotificationCallback listener) { + _noteRemovedListeners.add(listener); + } + + void removeNoteRemovedListener(NoteNotificationCallback listener) { + _noteRemovedListeners.remove(listener); + } + + @mustCallSuper + @override + void dispose() { + _folderAddedListeners = null; + _folderRemovedListeners = null; + _noteAddedListeners = null; + _noteRemovedListeners = null; + + assert(_debugAssertNotDisposed()); + _listeners = null; + } + + void _notifyFolderCallback( + ObserverList _listeners, + int index, + NotesFolder folder, + ) { + if (_listeners == null) { + return; + } + final localListeners = List.from(_listeners); + for (var listener in localListeners) { + try { + if (_listeners.contains(listener)) { + listener(index, folder); + } + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'GitJournal', + context: ErrorDescription( + 'while dispatching notifications for $runtimeType'), + informationCollector: () sync* { + yield DiagnosticsProperty( + 'The $runtimeType sending notification was', + this, + style: DiagnosticsTreeStyle.errorProperty, + ); + }, + )); + } + } + notifyListeners(); + } + + void notifyFolderAdded(int index, NotesFolder folder) { + _notifyFolderCallback(_folderAddedListeners, index, folder); + } + + void notifyFolderRemoved(int index, NotesFolder folder) { + _notifyFolderCallback(_folderRemovedListeners, index, folder); + } + + void _notifyNoteCallback( + ObserverList _listeners, + int index, + Note note, + ) { + if (_listeners == null) { + return; + } + final localListeners = List.from(_listeners); + for (var listener in localListeners) { + try { + if (_listeners.contains(listener)) { + listener(index, note); + } + } catch (exception, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'GitJournal', + context: ErrorDescription( + 'while dispatching notifications for $runtimeType'), + informationCollector: () sync* { + yield DiagnosticsProperty( + 'The $runtimeType sending notification was', + this, + style: DiagnosticsTreeStyle.errorProperty, + ); + }, + )); + } + } + notifyListeners(); + } + + void notifyNoteAdded(int index, Note note) { + _notifyNoteCallback(_noteAddedListeners, index, note); + } + + void notifyNoteRemoved(int index, Note note) { + _notifyNoteCallback(_noteRemovedListeners, index, note); + } + + // + // 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, + ); + }, + )); + } + } + } + } +}