diff --git a/lib/editors/disposable_change_notifier.dart b/lib/editors/disposable_change_notifier.dart index ba8e8f5f..7e1f922e 100644 --- a/lib/editors/disposable_change_notifier.dart +++ b/lib/editors/disposable_change_notifier.dart @@ -1,19 +1,27 @@ -// @dart=2.9 +import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// A duplicate of the ChangeNotifier class which has the dispose method -/// renamed to disposeListeners. This is done so that it can be used +/// renamed to disposeListenables. This is done so that it can be used /// as a mixin with a State class which also has a dispose method + +class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { + _ListenerEntry(this.listener); + final VoidCallback listener; +} + class DisposableChangeNotifier implements Listenable { - ObserverList _listeners = ObserverList(); + LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>(); bool _debugAssertNotDisposed() { assert(() { if (_listeners == null) { - throw FlutterError('A $runtimeType was used after being disposed.\n' - 'Once you have called dispose() on a $runtimeType, it can no longer be used.'); + throw FlutterError( + 'A $runtimeType was used after being disposed.\n' + 'Once you have called dispose() on a $runtimeType, it can no longer be used.', + ); } return true; }()); @@ -38,16 +46,39 @@ class DisposableChangeNotifier implements Listenable { @protected bool get hasListeners { assert(_debugAssertNotDisposed()); - return _listeners.isNotEmpty; + return _listeners!.isNotEmpty; } /// Register a closure to be called when the object changes. /// + /// If the given closure is already registered, an additional instance is + /// added, and must be removed the same number of times it is added before it + /// will stop being called. + /// /// This method must not be called after [dispose] has been called. + /// + /// {@template flutter.foundation.ChangeNotifier.addListener} + /// If a listener is added twice, and is removed once during an iteration + /// (e.g. 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, therefore it will conservatively still + /// call 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. + /// {@endtemplate} + /// + /// See also: + /// + /// * [removeListener], which removes a previously registered closure from + /// the list of closures that are notified when the object changes. @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); - _listeners.add(listener); + _listeners!.add(_ListenerEntry(listener)); } /// Remove a previously registered closure from the list of closures that are @@ -57,22 +88,21 @@ class DisposableChangeNotifier implements Listenable { /// /// 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. + /// {@macro flutter.foundation.ChangeNotifier.addListener} /// - /// 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. + /// See also: + /// + /// * [addListener], which registers a closure to be called when the object + /// changes. @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); - _listeners.remove(listener); + for (final _ListenerEntry entry in _listeners!) { + if (entry.listener == listener) { + entry.unlink(); + return; + } + } } /// Discards any resources used by the object. After this is called, the @@ -90,46 +120,45 @@ class DisposableChangeNotifier implements Listenable { /// 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. + /// object may have changed. 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. + /// Surprising behavior can result when reentrantly removing a listener (e.g. /// in response to a notification) that has been registered multiple times. /// See the discussion at [removeListener]. @protected @visibleForTesting void notifyListeners() { assert(_debugAssertNotDisposed()); - if (_listeners != null) { - final List localListeners = - List.from(_listeners); - for (final 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, - ); - }, - )); - } + if (_listeners!.isEmpty) return; + + final List<_ListenerEntry> localListeners = + List<_ListenerEntry>.from(_listeners!); + + for (final _ListenerEntry entry in localListeners) { + try { + if (entry.list != null) entry.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, + ); + }, + )); } } } diff --git a/lib/widgets/folder_tree_view.dart b/lib/widgets/folder_tree_view.dart index a8f6997e..b3c29326 100644 --- a/lib/widgets/folder_tree_view.dart +++ b/lib/widgets/folder_tree_view.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - import 'package:flutter/material.dart'; import 'package:gitjournal/core/notes_folder_fs.dart'; @@ -14,9 +12,9 @@ class FolderTreeView extends StatefulWidget { final FolderSelectedCallback onFolderEntered; FolderTreeView({ - Key key, - @required this.rootFolder, - @required this.onFolderEntered, + Key? key, + required this.rootFolder, + required this.onFolderEntered, this.onFolderSelected = _doNothing, this.onFolderUnselected = _doNothing, }) : super(key: key); @@ -29,7 +27,7 @@ class FolderTreeView extends StatefulWidget { class FolderTreeViewState extends State { bool inSelectionMode = false; - NotesFolderFS selectedFolder; + NotesFolderFS? selectedFolder; @override Widget build(BuildContext context) { @@ -73,13 +71,13 @@ class FolderTile extends StatefulWidget { final NotesFolderFS folder; final FolderSelectedCallback onTap; final FolderSelectedCallback onLongPress; - final NotesFolderFS selectedFolder; + final NotesFolderFS? selectedFolder; FolderTile({ - @required this.folder, - @required this.onTap, - @required this.onLongPress, - @required this.selectedFolder, + required this.folder, + required this.onTap, + required this.onLongPress, + required this.selectedFolder, }); @override @@ -127,7 +125,7 @@ class FolderTileState extends State { var publicName = folder.publicName; if (folder.parent != null) { - publicName = publicName.substring(folder.parent.pathSpec().length); + publicName = publicName.substring(folder.parent!.pathSpec().length); if (publicName.startsWith('/')) { publicName = publicName.substring(1); } @@ -167,7 +165,7 @@ class FolderTileState extends State { var children = []; widget.folder.subFolders.forEach((folder) { children.add(FolderTile( - folder: folder, + folder: folder as NotesFolderFS, onTap: widget.onTap, onLongPress: widget.onLongPress, selectedFolder: widget.selectedFolder, diff --git a/lib/widgets/note_editor_selector.dart b/lib/widgets/note_editor_selector.dart index 330fbfa1..63350365 100644 --- a/lib/widgets/note_editor_selector.dart +++ b/lib/widgets/note_editor_selector.dart @@ -1,5 +1,3 @@ -// @dart=2.9 - /* Copyright 2020-2021 Vishesh Handa @@ -85,7 +83,7 @@ class NoteEditorSelector extends StatelessWidget { var selected = et == currentEditor; var theme = Theme.of(context); var listTileTheme = ListTileTheme.of(context); - var textStyle = theme.textTheme.bodyText2.copyWith( + var textStyle = theme.textTheme.bodyText2!.copyWith( color: selected ? theme.primaryColor : listTileTheme.textColor, );