From 753f2b54b8142cebc2aeb7a4e36611c9e0a64689 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 1 Jun 2016 16:13:40 -0700 Subject: [PATCH 001/109] PaginatedDataTable (part 1) (#4306) This introduces the key parts of a paginated data table, not including the built-in pagination features. * Provide more data for the data table demo, so there's data to page. * Introduce a ChangeNotifier class which abstracts out addListener/removeListener/notifyListeners. We might be able to use this to simplify existing classes as well, though this patch doesn't do that. * Introduce DataTableSource, a delegate for getting data for data tables. This will also be used by ScrollingDataTable in due course. * Introduce PaginatedDataTable, a widget that wraps DataTable and only shows N rows at a time, fed by a DataTableSource. --- packages/listen/lib/src/listen.dart | 65 +++++++++++++++ packages/listen/test/listen_test.dart | 116 ++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 packages/listen/lib/src/listen.dart create mode 100644 packages/listen/test/listen_test.dart diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart new file mode 100644 index 00000000..7c784850 --- /dev/null +++ b/packages/listen/lib/src/listen.dart @@ -0,0 +1,65 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'assertions.dart'; +import 'basic_types.dart'; + +/// Abstract class that can be extended or mixed in that provides +/// a change notification API using [VoidCallback] for notifications. +abstract class ChangeNotifier { + List _listeners; + + /// Register a closure to be called when the object changes. + void addListener(VoidCallback listener) { + _listeners ??= []; + _listeners.add(listener); + } + + /// Remove a previously registered closure from the list of closures that are + /// notified when the object changes. + void removeListener(VoidCallback listener) { + _listeners?.remove(listener); + } + + /// Discards any resources used by the object. After this is called, the object + /// is not in a usable state and should be discarded. + /// + /// This method should only be called by the object's owner. + @mustCallSuper + void dispose() { + _listeners = null; + } + + /// Call all the registered listeners. + /// + /// Call this method whenever the object changes, to notify any clients the + /// object may have. + /// + /// Exceptions thrown by listeners will be caught and reported using + /// [FlutterError.reportError]. + @protected + void notifyListeners() { + if (_listeners != null) { + List listeners = new List.from(_listeners); + for (VoidCallback listener in listeners) { + try { + listener(); + } catch (exception, stack) { + FlutterError.reportError(new FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'foundation library', + context: 'while dispatching notifications for $runtimeType', + informationCollector: (StringBuffer information) { + information.writeln('The $runtimeType sending notification was:'); + information.write(' $this'); + } + )); + } + } + } + } +} diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart new file mode 100644 index 00000000..e1b7ae6f --- /dev/null +++ b/packages/listen/test/listen_test.dart @@ -0,0 +1,116 @@ + // Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class TestNotifier extends ChangeNotifier { + void notify() { + notifyListeners(); + } +} + +void main() { + testWidgets('ChangeNotifier', (WidgetTester tester) async { + final List log = []; + final VoidCallback listener = () { log.add('listener'); }; + final VoidCallback listener1 = () { log.add('listener1'); }; + final VoidCallback listener2 = () { log.add('listener2'); }; + final VoidCallback badListener = () { log.add('badListener'); throw null; }; + + final TestNotifier test = new TestNotifier(); + + test.addListener(listener); + test.addListener(listener); + test.notify(); + expect(log, equals(['listener', 'listener'])); + log.clear(); + + test.removeListener(listener); + test.notify(); + expect(log, equals(['listener'])); + log.clear(); + + test.removeListener(listener); + test.notify(); + expect(log, equals([])); + log.clear(); + + test.removeListener(listener); + test.notify(); + expect(log, equals([])); + log.clear(); + + test.addListener(listener); + test.notify(); + expect(log, equals(['listener'])); + log.clear(); + + test.addListener(listener1); + test.notify(); + expect(log, equals(['listener', 'listener1'])); + log.clear(); + + test.addListener(listener2); + test.notify(); + expect(log, equals(['listener', 'listener1', 'listener2'])); + log.clear(); + + test.removeListener(listener1); + test.notify(); + expect(log, equals(['listener', 'listener2'])); + log.clear(); + + test.addListener(listener1); + test.notify(); + expect(log, equals(['listener', 'listener2', 'listener1'])); + log.clear(); + + test.addListener(badListener); + test.notify(); + expect(log, equals(['listener', 'listener2', 'listener1', 'badListener'])); + expect(tester.takeException(), isNullThrownError); + log.clear(); + + test.addListener(listener1); + test.removeListener(listener); + test.removeListener(listener1); + test.removeListener(listener2); + test.addListener(listener2); + test.notify(); + expect(log, equals(['badListener', 'listener1', 'listener2'])); + expect(tester.takeException(), isNullThrownError); + log.clear(); + }); + + testWidgets('ChangeNotifier with mutating listener', (WidgetTester tester) async { + final TestNotifier test = new TestNotifier(); + final List log = []; + + final VoidCallback listener1 = () { log.add('listener1'); }; + final VoidCallback listener3 = () { log.add('listener3'); }; + final VoidCallback listener4 = () { log.add('listener4'); }; + final VoidCallback listener2 = () { + log.add('listener2'); + test.removeListener(listener1); + test.removeListener(listener3); + test.addListener(listener4); + }; + + test.addListener(listener1); + test.addListener(listener2); + test.addListener(listener3); + test.notify(); + expect(log, equals(['listener1', 'listener2', 'listener3'])); + log.clear(); + + test.notify(); + expect(log, equals(['listener2', 'listener4'])); + log.clear(); + + test.notify(); + expect(log, equals(['listener2', 'listener4', 'listener4'])); + log.clear(); + }); +} From ddb441d572e69ddac55bc1a7f068f3f25a0045ca Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 24 Jun 2016 15:53:48 -0700 Subject: [PATCH 002/109] Improve change notifier (#4747) This patch improves some subtle behaviors about the change notifier. --- packages/listen/lib/src/listen.dart | 13 ++++++++----- packages/listen/test/listen_test.dart | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7c784850..e20df8eb 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -30,23 +30,26 @@ abstract class ChangeNotifier { /// This method should only be called by the object's owner. @mustCallSuper void dispose() { - _listeners = null; + _listeners = const []; } /// Call all the registered listeners. /// /// Call this method whenever the object changes, to notify any clients the - /// object may have. + /// 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]. @protected void notifyListeners() { if (_listeners != null) { - List listeners = new List.from(_listeners); - for (VoidCallback listener in listeners) { + List localListeners = new List.from(_listeners); + for (VoidCallback listener in localListeners) { try { - listener(); + if (_listeners.contains(listener)) + listener(); } catch (exception, stack) { FlutterError.reportError(new FlutterErrorDetails( exception: exception, diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index e1b7ae6f..1d40c856 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -102,7 +102,7 @@ void main() { test.addListener(listener2); test.addListener(listener3); test.notify(); - expect(log, equals(['listener1', 'listener2', 'listener3'])); + expect(log, equals(['listener1', 'listener2'])); log.clear(); test.notify(); From 70d66bcfca5a8271693650a375feefc449606dee Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 15 Sep 2016 13:13:51 -0700 Subject: [PATCH 003/109] Extract a Listenable base class from Animation and ChangeNotifier (#5889) Having this base class lets classes like CustomPainter and DataTableSource be more agnostic as to what's generating the repaints. --- packages/listen/lib/src/listen.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index e20df8eb..5ce4d372 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -6,13 +6,15 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; +import 'listenable.dart'; -/// Abstract class that can be extended or mixed in that provides -/// a change notification API using [VoidCallback] for notifications. -abstract class ChangeNotifier { +/// A class that can be extended or mixed in that provides a change notification +/// API using [VoidCallback] for notifications. +class ChangeNotifier extends Listenable { List _listeners; /// Register a closure to be called when the object changes. + @override void addListener(VoidCallback listener) { _listeners ??= []; _listeners.add(listener); @@ -20,6 +22,7 @@ abstract class ChangeNotifier { /// Remove a previously registered closure from the list of closures that are /// notified when the object changes. + @override void removeListener(VoidCallback listener) { _listeners?.remove(listener); } From b9282817466dd9d5e3b8efde8215f91c0b38a779 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 15 Dec 2016 13:54:53 -0800 Subject: [PATCH 004/109] Listenable.merge (#7256) Sometimes you have several listenables, but you want to hand them to an API (e.g. CustomPainter) that only expects one. --- packages/listen/lib/src/listen.dart | 38 +++++++++++++++++++- packages/listen/test/listen_test.dart | 52 +++++++++++++++++++-------- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 5ce4d372..eb0e87c5 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -6,7 +6,27 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; -import 'listenable.dart'; + +/// An object that maintains a list of listeners. +abstract class Listenable { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const Listenable(); + + /// Return a [Listenable] that triggers when any of the given [Listenable]s + /// themselves trigger. + /// + /// The list must not be changed after this method has been called. Doing so + /// will lead to memory leaks or exceptions. + factory Listenable.merge(List listenables) = _MergingListenable; + + /// Register a closure to be called when the object notifies its listeners. + void addListener(VoidCallback listener); + + /// Remove a previously registered closure from the list of closures that the + /// object notifies. + void removeListener(VoidCallback listener); +} /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. @@ -69,3 +89,19 @@ class ChangeNotifier extends Listenable { } } } + +class _MergingListenable extends ChangeNotifier { + _MergingListenable(this._children) { + for (Listenable child in _children) + child.addListener(notifyListeners); + } + + final List _children; + + @override + void dispose() { + for (Listenable child in _children) + child.removeListener(notifyListeners); + super.dispose(); + } +} diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 1d40c856..e252f286 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -24,52 +24,52 @@ void main() { test.addListener(listener); test.addListener(listener); test.notify(); - expect(log, equals(['listener', 'listener'])); + expect(log, ['listener', 'listener']); log.clear(); test.removeListener(listener); test.notify(); - expect(log, equals(['listener'])); + expect(log, ['listener']); log.clear(); test.removeListener(listener); test.notify(); - expect(log, equals([])); + expect(log, []); log.clear(); test.removeListener(listener); test.notify(); - expect(log, equals([])); + expect(log, []); log.clear(); test.addListener(listener); test.notify(); - expect(log, equals(['listener'])); + expect(log, ['listener']); log.clear(); test.addListener(listener1); test.notify(); - expect(log, equals(['listener', 'listener1'])); + expect(log, ['listener', 'listener1']); log.clear(); test.addListener(listener2); test.notify(); - expect(log, equals(['listener', 'listener1', 'listener2'])); + expect(log, ['listener', 'listener1', 'listener2']); log.clear(); test.removeListener(listener1); test.notify(); - expect(log, equals(['listener', 'listener2'])); + expect(log, ['listener', 'listener2']); log.clear(); test.addListener(listener1); test.notify(); - expect(log, equals(['listener', 'listener2', 'listener1'])); + expect(log, ['listener', 'listener2', 'listener1']); log.clear(); test.addListener(badListener); test.notify(); - expect(log, equals(['listener', 'listener2', 'listener1', 'badListener'])); + expect(log, ['listener', 'listener2', 'listener1', 'badListener']); expect(tester.takeException(), isNullThrownError); log.clear(); @@ -79,7 +79,7 @@ void main() { test.removeListener(listener2); test.addListener(listener2); test.notify(); - expect(log, equals(['badListener', 'listener1', 'listener2'])); + expect(log, ['badListener', 'listener1', 'listener2']); expect(tester.takeException(), isNullThrownError); log.clear(); }); @@ -102,15 +102,39 @@ void main() { test.addListener(listener2); test.addListener(listener3); test.notify(); - expect(log, equals(['listener1', 'listener2'])); + expect(log, ['listener1', 'listener2']); log.clear(); test.notify(); - expect(log, equals(['listener2', 'listener4'])); + expect(log, ['listener2', 'listener4']); log.clear(); test.notify(); - expect(log, equals(['listener2', 'listener4', 'listener4'])); + expect(log, ['listener2', 'listener4', 'listener4']); + log.clear(); + }); + + testWidgets('Merging change notifiers', (WidgetTester tester) async { + final TestNotifier source1 = new TestNotifier(); + final TestNotifier source2 = new TestNotifier(); + final TestNotifier source3 = new TestNotifier(); + final List log = []; + + final Listenable merged = new Listenable.merge([source1, source2]); + final VoidCallback listener = () { log.add('listener'); }; + + merged.addListener(listener); + source1.notify(); + source2.notify(); + source3.notify(); + expect(log, ['listener', 'listener']); + log.clear(); + + merged.removeListener(listener); + source1.notify(); + source2.notify(); + source3.notify(); + expect(log, isEmpty); log.clear(); }); } From 23dda284ca6defe15a808c4e83d8b2217dda207a Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 5 Jan 2017 13:10:30 -0800 Subject: [PATCH 005/109] Allow nulls in `Listenable.merge` (#7355) This lets you use `Listenable.merge` without having to sanitize your incoming list of change notifiers, in case your semantics are that they are optional. --- packages/listen/lib/src/listen.dart | 6 +++-- packages/listen/test/listen_test.dart | 32 +++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index eb0e87c5..1814b8fa 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -18,6 +18,8 @@ abstract class Listenable { /// /// The list must not be changed after this method has been called. Doing so /// will lead to memory leaks or exceptions. + /// + /// The list may contain `null`s; they are ignored. factory Listenable.merge(List listenables) = _MergingListenable; /// Register a closure to be called when the object notifies its listeners. @@ -93,7 +95,7 @@ class ChangeNotifier extends Listenable { class _MergingListenable extends ChangeNotifier { _MergingListenable(this._children) { for (Listenable child in _children) - child.addListener(notifyListeners); + child?.addListener(notifyListeners); } final List _children; @@ -101,7 +103,7 @@ class _MergingListenable extends ChangeNotifier { @override void dispose() { for (Listenable child in _children) - child.removeListener(notifyListeners); + child?.removeListener(notifyListeners); super.dispose(); } } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index e252f286..9d0e2f63 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -121,20 +121,44 @@ void main() { final List log = []; final Listenable merged = new Listenable.merge([source1, source2]); - final VoidCallback listener = () { log.add('listener'); }; + final VoidCallback listener1 = () { log.add('listener1'); }; + final VoidCallback listener2 = () { log.add('listener2'); }; - merged.addListener(listener); + merged.addListener(listener1); source1.notify(); source2.notify(); source3.notify(); - expect(log, ['listener', 'listener']); + expect(log, ['listener1', 'listener1']); log.clear(); - merged.removeListener(listener); + merged.removeListener(listener1); source1.notify(); source2.notify(); source3.notify(); expect(log, isEmpty); log.clear(); + + merged.addListener(listener1); + merged.addListener(listener2); + source1.notify(); + source2.notify(); + source3.notify(); + expect(log, ['listener1', 'listener2', 'listener1', 'listener2']); + log.clear(); + }); + + testWidgets('Merging change notifiers ignores null', (WidgetTester tester) async { + final TestNotifier source1 = new TestNotifier(); + final TestNotifier source2 = new TestNotifier(); + final List log = []; + + final Listenable merged = new Listenable.merge([null, source1, null, source2, null]); + final VoidCallback listener = () { log.add('listener'); }; + + merged.addListener(listener); + source1.notify(); + source2.notify(); + expect(log, ['listener', 'listener']); + log.clear(); }); } From 5a09f6c4bc06c483ea201cf974ca5b950282f732 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Tue, 17 Jan 2017 11:00:10 -0800 Subject: [PATCH 006/109] Fix several minor bugs and add many tests (#7506) * MultiTapGestureRecognizer previously would assert if there was no competition. * GestureArenaTeam would always select the first recongizer as the winner even if a later recognizer actually accepted the pointer sequence. * debugPrintStack would fail a type check if maxFrames was non-null. * FractionalOffset.lerp would throw a null-pointer exception if its second argument was null. Also, add a number of tests for previously untested lines of code. --- packages/listen/test/listen_test.dart | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 9d0e2f63..09adeadb 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -84,7 +84,7 @@ void main() { log.clear(); }); - testWidgets('ChangeNotifier with mutating listener', (WidgetTester tester) async { + test('ChangeNotifier with mutating listener', () { final TestNotifier test = new TestNotifier(); final List log = []; @@ -114,7 +114,7 @@ void main() { log.clear(); }); - testWidgets('Merging change notifiers', (WidgetTester tester) async { + test('Merging change notifiers', () { final TestNotifier source1 = new TestNotifier(); final TestNotifier source2 = new TestNotifier(); final TestNotifier source3 = new TestNotifier(); @@ -147,7 +147,7 @@ void main() { log.clear(); }); - testWidgets('Merging change notifiers ignores null', (WidgetTester tester) async { + test('Merging change notifiers ignores null', () { final TestNotifier source1 = new TestNotifier(); final TestNotifier source2 = new TestNotifier(); final List log = []; @@ -161,4 +161,24 @@ void main() { expect(log, ['listener', 'listener']); log.clear(); }); + + test('Can dispose merged notifier', () { + final TestNotifier source1 = new TestNotifier(); + final TestNotifier source2 = new TestNotifier(); + final List log = []; + + final ChangeNotifier merged = new Listenable.merge([source1, source2]); + final VoidCallback listener = () { log.add('listener'); }; + + merged.addListener(listener); + source1.notify(); + source2.notify(); + expect(log, ['listener', 'listener']); + log.clear(); + merged.dispose(); + + source1.notify(); + source2.notify(); + expect(log, isEmpty); + }); } From ed19257122df54a41235b3a3659b665168a2fff5 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 17 Jan 2017 14:17:00 -0800 Subject: [PATCH 007/109] Add more asserts and docs to ChangeNotifier. (#7513) It took me a while to figure out what was going on (I was removing a listener after disposal). These asserts helped. --- packages/listen/lib/src/listen.dart | 35 +++++++++++++++++++++++++-- packages/listen/test/listen_test.dart | 9 +++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 1814b8fa..f09dd2d0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -32,29 +32,57 @@ abstract class Listenable { /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. +/// +/// [ChangeNotifier] is optimised for small numbers (one or two) of listeners. +/// It is O(N) for adding and removing listeners and O(N²) for dispatching +/// notifications (where N is the number of listeners). class ChangeNotifier extends Listenable { List _listeners; + bool _debugAssertNotDisposed() { + assert(() { + if (_listeners == const []) { + throw new 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; + }); + return true; + } + /// 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 ??= []; _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. @override void removeListener(VoidCallback listener) { + assert(_debugAssertNotDisposed); _listeners?.remove(listener); } - /// Discards any resources used by the object. After this is called, the object - /// is not in a usable state and should be discarded. + /// Discards any resources used by the object. After this is called, the + /// object is not in a usable state and should be discarded (calls to + /// [addListener] and [removeListener] will throw after the object is + /// disposed). /// /// This method should only be called by the object's owner. @mustCallSuper void dispose() { + assert(_debugAssertNotDisposed); _listeners = const []; } @@ -67,8 +95,11 @@ class ChangeNotifier extends Listenable { /// /// Exceptions thrown by listeners will be caught and reported using /// [FlutterError.reportError]. + /// + /// This method must not be called after [dispose] has been called. @protected void notifyListeners() { + assert(_debugAssertNotDisposed); if (_listeners != null) { List localListeners = new List.from(_listeners); for (VoidCallback listener in localListeners) { diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 09adeadb..24cb71bf 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -181,4 +181,13 @@ void main() { source2.notify(); expect(log, isEmpty); }); + + test('Cannot use a disposed ChangeNotifier', () { + final TestNotifier source = new TestNotifier(); + source.dispose(); + expect(() { source.addListener(null); }, throwsFlutterError); + expect(() { source.removeListener(null); }, throwsFlutterError); + expect(() { source.dispose(); }, throwsFlutterError); + expect(() { source.notify(); }, throwsFlutterError); + }); } From 8659b4b46ec192deaa5df3ff53bec9b6dd69171e Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Wed, 18 Jan 2017 22:17:13 -0800 Subject: [PATCH 008/109] Strengthen animation listener iteration patterns (#7537) This patch aligns the iteration patterns used by animations and ChangeNotifier. They now both respect re-entrant removal of listeners and coalesce duplication registrations. (Also, ChangeNotifier notification is no longer N^2). Fixes #7533 --- packages/listen/lib/src/listen.dart | 13 +++++++------ packages/listen/test/listen_test.dart | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index f09dd2d0..08761f3c 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:collection'; + import 'package:meta/meta.dart'; import 'assertions.dart'; @@ -37,11 +39,11 @@ abstract class Listenable { /// It is O(N) for adding and removing listeners and O(N²) for dispatching /// notifications (where N is the number of listeners). class ChangeNotifier extends Listenable { - List _listeners; + Set _listeners = new LinkedHashSet(); bool _debugAssertNotDisposed() { assert(() { - if (_listeners == const []) { + if (_listeners == null) { throw new FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.' @@ -58,7 +60,6 @@ class ChangeNotifier extends Listenable { @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed); - _listeners ??= []; _listeners.add(listener); } @@ -71,7 +72,7 @@ class ChangeNotifier extends Listenable { @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed); - _listeners?.remove(listener); + _listeners.remove(listener); } /// Discards any resources used by the object. After this is called, the @@ -83,7 +84,7 @@ class ChangeNotifier extends Listenable { @mustCallSuper void dispose() { assert(_debugAssertNotDisposed); - _listeners = const []; + _listeners = null; } /// Call all the registered listeners. @@ -101,7 +102,7 @@ class ChangeNotifier extends Listenable { void notifyListeners() { assert(_debugAssertNotDisposed); if (_listeners != null) { - List localListeners = new List.from(_listeners); + final List localListeners = new List.from(_listeners); for (VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 24cb71bf..d5ec5196 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -24,12 +24,12 @@ void main() { test.addListener(listener); test.addListener(listener); test.notify(); - expect(log, ['listener', 'listener']); + expect(log, ['listener']); log.clear(); test.removeListener(listener); test.notify(); - expect(log, ['listener']); + expect(log, []); log.clear(); test.removeListener(listener); @@ -79,7 +79,7 @@ void main() { test.removeListener(listener2); test.addListener(listener2); test.notify(); - expect(log, ['badListener', 'listener1', 'listener2']); + expect(log, ['badListener', 'listener2']); expect(tester.takeException(), isNullThrownError); log.clear(); }); @@ -110,7 +110,7 @@ void main() { log.clear(); test.notify(); - expect(log, ['listener2', 'listener4', 'listener4']); + expect(log, ['listener2', 'listener4']); log.clear(); }); From 9be184a8e3041eadaa131aa13c62cdde28611670 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 19 Jan 2017 14:36:25 -0800 Subject: [PATCH 009/109] Revert "Strengthen animation listener iteration patterns" (#7552) --- packages/listen/lib/src/listen.dart | 13 ++++++------- packages/listen/test/listen_test.dart | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 08761f3c..f09dd2d0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:collection'; - import 'package:meta/meta.dart'; import 'assertions.dart'; @@ -39,11 +37,11 @@ abstract class Listenable { /// It is O(N) for adding and removing listeners and O(N²) for dispatching /// notifications (where N is the number of listeners). class ChangeNotifier extends Listenable { - Set _listeners = new LinkedHashSet(); + List _listeners; bool _debugAssertNotDisposed() { assert(() { - if (_listeners == null) { + if (_listeners == const []) { throw new FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.' @@ -60,6 +58,7 @@ class ChangeNotifier extends Listenable { @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed); + _listeners ??= []; _listeners.add(listener); } @@ -72,7 +71,7 @@ class ChangeNotifier extends Listenable { @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed); - _listeners.remove(listener); + _listeners?.remove(listener); } /// Discards any resources used by the object. After this is called, the @@ -84,7 +83,7 @@ class ChangeNotifier extends Listenable { @mustCallSuper void dispose() { assert(_debugAssertNotDisposed); - _listeners = null; + _listeners = const []; } /// Call all the registered listeners. @@ -102,7 +101,7 @@ class ChangeNotifier extends Listenable { void notifyListeners() { assert(_debugAssertNotDisposed); if (_listeners != null) { - final List localListeners = new List.from(_listeners); + List localListeners = new List.from(_listeners); for (VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index d5ec5196..24cb71bf 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -24,12 +24,12 @@ void main() { test.addListener(listener); test.addListener(listener); test.notify(); - expect(log, ['listener']); + expect(log, ['listener', 'listener']); log.clear(); test.removeListener(listener); test.notify(); - expect(log, []); + expect(log, ['listener']); log.clear(); test.removeListener(listener); @@ -79,7 +79,7 @@ void main() { test.removeListener(listener2); test.addListener(listener2); test.notify(); - expect(log, ['badListener', 'listener2']); + expect(log, ['badListener', 'listener1', 'listener2']); expect(tester.takeException(), isNullThrownError); log.clear(); }); @@ -110,7 +110,7 @@ void main() { log.clear(); test.notify(); - expect(log, ['listener2', 'listener4']); + expect(log, ['listener2', 'listener4', 'listener4']); log.clear(); }); From b3b3ff8c806b6279824772fb953064ad4e277172 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 20 Jan 2017 15:38:53 -0800 Subject: [PATCH 010/109] Strengthen animation listener iteration patterns (#7566) This patch aligns the iteration patterns used by animations and ChangeNotifier. They now both respect re-entrant removal of listeners and coalesce duplication registrations. (Also, ChangeNotifier notification is no longer N^2). This patch introduces ObserverList to avoid the performance regression that the previous version of this patch caused. Fixes #7533 --- packages/listen/lib/src/listen.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index f09dd2d0..6baf67f9 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; +import 'observer_list.dart'; /// An object that maintains a list of listeners. abstract class Listenable { @@ -37,11 +38,11 @@ abstract class Listenable { /// It is O(N) for adding and removing listeners and O(N²) for dispatching /// notifications (where N is the number of listeners). class ChangeNotifier extends Listenable { - List _listeners; + ObserverList _listeners = new ObserverList(); bool _debugAssertNotDisposed() { assert(() { - if (_listeners == const []) { + if (_listeners == null) { throw new FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.' @@ -58,7 +59,6 @@ class ChangeNotifier extends Listenable { @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed); - _listeners ??= []; _listeners.add(listener); } @@ -71,7 +71,7 @@ class ChangeNotifier extends Listenable { @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed); - _listeners?.remove(listener); + _listeners.remove(listener); } /// Discards any resources used by the object. After this is called, the @@ -83,7 +83,7 @@ class ChangeNotifier extends Listenable { @mustCallSuper void dispose() { assert(_debugAssertNotDisposed); - _listeners = const []; + _listeners = null; } /// Call all the registered listeners. @@ -101,7 +101,7 @@ class ChangeNotifier extends Listenable { void notifyListeners() { assert(_debugAssertNotDisposed); if (_listeners != null) { - List localListeners = new List.from(_listeners); + final List localListeners = new List.from(_listeners); for (VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) From f758e9328c2c8e73e135fe2635e033aa050cd561 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Sun, 29 Jan 2017 22:39:15 -0800 Subject: [PATCH 011/109] Various documentation fixes. (#7726) Fixes for: https://github.com/flutter/flutter/issues/7570 https://github.com/flutter/flutter/issues/7231 https://github.com/flutter/flutter/issues/2841 and others --- packages/listen/lib/src/listen.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 6baf67f9..363335f2 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -68,6 +68,19 @@ class ChangeNotifier extends Listenable { /// 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); @@ -97,6 +110,10 @@ class ChangeNotifier extends Listenable { /// [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 void notifyListeners() { assert(_debugAssertNotDisposed); From 4adb1f3e1571c06de1e54fc6b1a0e37771de929d Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Tue, 28 Feb 2017 11:25:23 -0800 Subject: [PATCH 012/109] Add ValueNotifier (#8463) It's common to have a ChangeNotifier that wraps a single value. This class makes that easy by providing a generic implementation. --- packages/listen/lib/src/listen.dart | 24 ++++++++++++++++++++++++ packages/listen/test/listen_test.dart | 16 ++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 363335f2..5e9822a6 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -37,6 +37,10 @@ abstract class Listenable { /// [ChangeNotifier] is optimised for small numbers (one or two) of listeners. /// It is O(N) for adding and removing listeners and O(N²) for dispatching /// notifications (where N is the number of listeners). +/// +/// See also: +/// +/// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. class ChangeNotifier extends Listenable { ObserverList _listeners = new ObserverList(); @@ -155,3 +159,23 @@ class _MergingListenable extends ChangeNotifier { super.dispose(); } } + +/// A [ChangeNotifier] that holds a single value. +/// +/// When [value] is replaced, this class notifies its listeners. +class ValueNotifier extends ChangeNotifier { + /// Creates a [ChangeNotifier] that wraps this value. + ValueNotifier(this._value); + + /// The current value stored in this notifier. + /// + /// When the value is replaced, this class notifies its listeners. + T get value => _value; + T _value; + set value(T newValue) { + if (_value == newValue) + return; + _value = newValue; + notifyListeners(); + } +} diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 24cb71bf..73e036e8 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -190,4 +190,20 @@ void main() { expect(() { source.dispose(); }, throwsFlutterError); expect(() { source.notify(); }, throwsFlutterError); }); + + test('Value notifier', () { + final ValueNotifier notifier = new ValueNotifier(2.0); + + final List log = []; + final VoidCallback listener = () { log.add(notifier.value); }; + + notifier.addListener(listener); + notifier.value = 3.0; + + expect(log, equals([ 3.0 ])); + log.clear(); + + notifier.value = 3.0; + expect(log, isEmpty); + }); } From 3a4717c39ae820f353765b20dd53a2ffdb0f9a0f Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 6 Mar 2017 11:06:05 -0800 Subject: [PATCH 013/109] Animation Demo performance tweaks (#8586) --- packages/listen/lib/src/listen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 5e9822a6..608deb06 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -178,4 +178,7 @@ class ValueNotifier extends ChangeNotifier { _value = newValue; notifyListeners(); } + + @override + String toString() => '<$runtimeType>(value: $value)'; } From cea97acebcf1d36f75dab883bed32ab11db86f21 Mon Sep 17 00:00:00 2001 From: xster Date: Wed, 29 Mar 2017 10:59:18 -0700 Subject: [PATCH 014/109] Remove imports of meta (#9081) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 608deb06..14378016 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'assertions.dart'; import 'basic_types.dart'; From aa8474ec24538b55e7070032ad978cfbbf45f4e4 Mon Sep 17 00:00:00 2001 From: xster Date: Thu, 30 Mar 2017 18:19:00 -0700 Subject: [PATCH 015/109] Change foundation references in foundation to meta (#9107) * Change foundation references to meta * Remove specified shows --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 14378016..608deb06 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; From bd82d728e960cf973d761bd6b6140090be94535f Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Sat, 1 Apr 2017 17:30:21 -0700 Subject: [PATCH 016/109] Rationalize text input widgets (#9119) After this patch, there are three major text input widgets: * EditableText. This widget is a low-level editing control that interacts with the IME and displays a blinking cursor. * TextField. This widget is a Material Design text field, with all the bells and whistles. It is highly configurable and can be reduced down to a fairly simple control by setting its `decoration` property to null. * TextFormField. This widget is a FormField that wraps a TextField. This patch also replaces the InputValue data model for these widgets with a Listenable TextEditingController, which is much more flexible. Fixes #7031 --- packages/listen/lib/src/listen.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 608deb06..f1839d85 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -158,6 +158,19 @@ class _MergingListenable extends ChangeNotifier { child?.removeListener(notifyListeners); super.dispose(); } + + @override + String toString() { + final StringBuffer buffer = new StringBuffer(); + buffer.write('_MergingListenable(['); + for (int i = 0; i < _children.length; ++i) { + buffer.write(_children[i].toString()); + if (i < _children.length - 1) + buffer.write(', '); + } + buffer.write('])'); + return buffer.toString(); + } } /// A [ChangeNotifier] that holds a single value. From 5b6a59595ec83fa0323283c261c7b5090112943f Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 5 Apr 2017 22:46:36 -0700 Subject: [PATCH 017/109] Better toStrings for Listenable subclasses (#9244) --- packages/listen/lib/src/listen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index f1839d85..b1eb40cc 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -162,7 +162,7 @@ class _MergingListenable extends ChangeNotifier { @override String toString() { final StringBuffer buffer = new StringBuffer(); - buffer.write('_MergingListenable(['); + buffer.write('Listenable.merge(['); for (int i = 0; i < _children.length; ++i) { buffer.write(_children[i].toString()); if (i < _children.length - 1) @@ -193,5 +193,5 @@ class ValueNotifier extends ChangeNotifier { } @override - String toString() => '<$runtimeType>(value: $value)'; + String toString() => '$runtimeType(value: $value)'; } From 75fd88738a80e9cb42062a0e3fc73f3512e230be Mon Sep 17 00:00:00 2001 From: xster Date: Wed, 12 Apr 2017 23:09:40 -0700 Subject: [PATCH 018/109] Simplify change notifier toString and handle nulls (#9368) --- packages/listen/lib/src/listen.dart | 10 +-------- packages/listen/test/listen_test.dart | 29 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index b1eb40cc..2fa6d499 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -161,15 +161,7 @@ class _MergingListenable extends ChangeNotifier { @override String toString() { - final StringBuffer buffer = new StringBuffer(); - buffer.write('Listenable.merge(['); - for (int i = 0; i < _children.length; ++i) { - buffer.write(_children[i].toString()); - if (i < _children.length - 1) - buffer.write(', '); - } - buffer.write('])'); - return buffer.toString(); + return 'Listenable.merge([${_children.join(", ")}])'; } } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 73e036e8..759516a1 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -206,4 +206,33 @@ void main() { notifier.value = 3.0; expect(log, isEmpty); }); + + test('Listenable.merge toString', () { + final TestNotifier source1 = new TestNotifier(); + final TestNotifier source2 = new TestNotifier(); + + ChangeNotifier listenableUnderTest = new Listenable.merge([]); + expect(listenableUnderTest.toString(), 'Listenable.merge([])'); + + listenableUnderTest = new Listenable.merge([null]); + expect(listenableUnderTest.toString(), 'Listenable.merge([null])'); + + listenableUnderTest = new Listenable.merge([source1]); + expect( + listenableUnderTest.toString(), + "Listenable.merge([Instance of 'TestNotifier'])", + ); + + listenableUnderTest = new Listenable.merge([source1, source2]); + expect( + listenableUnderTest.toString(), + "Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])", + ); + + listenableUnderTest = new Listenable.merge([null, source2]); + expect( + listenableUnderTest.toString(), + "Listenable.merge([null, Instance of 'TestNotifier'])", + ); + }); } From 2521f2e630b589752387881bd31eac7297b8d108 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 23 May 2017 19:19:00 -0700 Subject: [PATCH 019/109] More docs. (#10214) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 2fa6d499..953296c1 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -185,5 +185,5 @@ class ValueNotifier extends ChangeNotifier { } @override - String toString() => '$runtimeType(value: $value)'; + String toString() => '$runtimeType#$hashCode($value)'; } From 1d927b5d8b6a66fd743efda1e47d7c03ed765e48 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 8 Jun 2017 17:13:03 -0700 Subject: [PATCH 020/109] More documentation (#10589) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 953296c1..06dd9ea9 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -20,7 +20,7 @@ abstract class Listenable { /// The list must not be changed after this method has been called. Doing so /// will lead to memory leaks or exceptions. /// - /// The list may contain `null`s; they are ignored. + /// The list may contain nulls; they are ignored. factory Listenable.merge(List listenables) = _MergingListenable; /// Register a closure to be called when the object notifies its listeners. From fbc785fd1edd3c0991decf21690b3a9b8a13e297 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Tue, 20 Jun 2017 18:13:28 -0700 Subject: [PATCH 021/109] Change all ocurrences of '$runtimeType#$hashCode' to use the idAndType method. (#10871) * Change all instances of '$runtimeType#$hashCode' to use the describeIdentity method. The describeIdentity method generates a shorter description with a consistent length consisting of the runtime type and the a 5 hex character long truncated version of the hash code. --- packages/listen/lib/src/listen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 06dd9ea9..f871fb19 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -7,6 +7,7 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; import 'observer_list.dart'; +import 'print.dart'; /// An object that maintains a list of listeners. abstract class Listenable { @@ -185,5 +186,5 @@ class ValueNotifier extends ChangeNotifier { } @override - String toString() => '$runtimeType#$hashCode($value)'; + String toString() => '${idAndType(this)}($value)'; } From f3fe60ab5f92c64607829c3ac8aa1738fdbb3ae3 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Tue, 20 Jun 2017 22:14:59 -0700 Subject: [PATCH 022/109] Revert "Change all ocurrences of '$runtimeType#$hashCode' to use the idAndType method. (#10871)" (#10880) This reverts commit d6afe099750ea905c43e001ef775912a409cb498. --- packages/listen/lib/src/listen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index f871fb19..06dd9ea9 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -7,7 +7,6 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; import 'observer_list.dart'; -import 'print.dart'; /// An object that maintains a list of listeners. abstract class Listenable { @@ -186,5 +185,5 @@ class ValueNotifier extends ChangeNotifier { } @override - String toString() => '${idAndType(this)}($value)'; + String toString() => '$runtimeType#$hashCode($value)'; } From 0370c2cde5a196c43beb58506d459a103cc0fd87 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Wed, 21 Jun 2017 10:47:06 -0700 Subject: [PATCH 023/109] Change all occurrences of '$runtimeType#$hashCode' to use the describeIdentity (#10888) * Revert "Revert "Change all ocurrences of '$runtimeType#$hashCode' to use the idAndType method. (#10871)" (#10880)" This reverts commit 7ccc66f772fd95506fcbac7a2c21d96f18d96487. --- packages/listen/lib/src/listen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 06dd9ea9..46407cf3 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -7,6 +7,7 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; import 'observer_list.dart'; +import 'tree_diagnostics_mixin.dart'; /// An object that maintains a list of listeners. abstract class Listenable { @@ -185,5 +186,5 @@ class ValueNotifier extends ChangeNotifier { } @override - String toString() => '$runtimeType#$hashCode($value)'; + String toString() => '${describeIdentity(this)}($value)'; } From bd4b999ac96d87c787d090795608492485643910 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Thu, 3 Aug 2017 09:49:44 -0700 Subject: [PATCH 024/109] Add Diagnosticable base class and add documentation. (#11458) Add Diagnosticable base class and documentation --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 46407cf3..7e207ed2 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -6,8 +6,8 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; +import 'diagnostics.dart'; import 'observer_list.dart'; -import 'tree_diagnostics_mixin.dart'; /// An object that maintains a list of listeners. abstract class Listenable { From 02e2d32a6d04cd62cd00e8a27bcb26016648864e Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Thu, 21 Sep 2017 09:33:01 +0200 Subject: [PATCH 025/109] use bool in assert (#12170) --- packages/listen/lib/src/listen.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7e207ed2..9c775417 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -54,7 +54,7 @@ class ChangeNotifier extends Listenable { ); } return true; - }); + }()); return true; } @@ -63,7 +63,7 @@ class ChangeNotifier extends Listenable { /// This method must not be called after [dispose] has been called. @override void addListener(VoidCallback listener) { - assert(_debugAssertNotDisposed); + assert(_debugAssertNotDisposed()); _listeners.add(listener); } @@ -88,7 +88,7 @@ class ChangeNotifier extends Listenable { /// registrations to a common upstream object. @override void removeListener(VoidCallback listener) { - assert(_debugAssertNotDisposed); + assert(_debugAssertNotDisposed()); _listeners.remove(listener); } @@ -100,7 +100,7 @@ class ChangeNotifier extends Listenable { /// This method should only be called by the object's owner. @mustCallSuper void dispose() { - assert(_debugAssertNotDisposed); + assert(_debugAssertNotDisposed()); _listeners = null; } @@ -121,7 +121,7 @@ class ChangeNotifier extends Listenable { /// See the discussion at [removeListener]. @protected void notifyListeners() { - assert(_debugAssertNotDisposed); + assert(_debugAssertNotDisposed()); if (_listeners != null) { final List localListeners = new List.from(_listeners); for (VoidCallback listener in localListeners) { From 19a9b1bac5804aa768745a4bc34a5805de76942f Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 17 Nov 2017 10:05:21 -0800 Subject: [PATCH 026/109] Fix spelling errors in all the dartdocs. (#13061) I got tired of drive-by spelling fixes, so I figured I'd just take care of them all at once. This only corrects errors in the dartdocs, not regular comments, and I skipped any sample code in the dartdocs. It doesn't touch any identifiers in the dartdocs either. No code changes, just comments. --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 9c775417..0eb9c525 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -35,7 +35,7 @@ abstract class Listenable { /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. /// -/// [ChangeNotifier] is optimised for small numbers (one or two) of listeners. +/// [ChangeNotifier] is optimized for small numbers (one or two) of listeners. /// It is O(N) for adding and removing listeners and O(N²) for dispatching /// notifications (where N is the number of listeners). /// From 8aa7aca73355f8b9d95faf03fde4d0ebccaa475c Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 18 Jan 2018 20:21:15 -0800 Subject: [PATCH 027/109] Fix the confusing-zero case with NestedScrollView. (#14133) * Fix the confusing-zero case with NestedScrollView. * Update mock_canvas.dart * Update tabs_demo.dart * more tweaks --- packages/listen/lib/src/listen.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 0eb9c525..2ea4d5d7 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -32,6 +32,16 @@ abstract class Listenable { void removeListener(VoidCallback listener); } +/// An interface for subclasses of [Listenable] that expose a [value]. +/// +/// This interface is implemented by [ValueNotifier] and [Animation], and +/// allows other APIs to accept either of those implementations interchangeably. +abstract class ValueListenable extends Listenable { + /// The current value of the object. When the value changes, the callbacks + /// registered with [addListener] will be invoked. + T get value; +} + /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. /// @@ -169,13 +179,14 @@ class _MergingListenable extends ChangeNotifier { /// A [ChangeNotifier] that holds a single value. /// /// When [value] is replaced, this class notifies its listeners. -class ValueNotifier extends ChangeNotifier { +class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value); /// The current value stored in this notifier. /// /// When the value is replaced, this class notifies its listeners. + @override T get value => _value; T _value; set value(T newValue) { From f9d12c600b4d6e76528216462389c4fac2738274 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Fri, 2 Feb 2018 23:27:29 +0100 Subject: [PATCH 028/109] some whitespace cleanup (#14443) --- packages/listen/test/listen_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 759516a1..af5186db 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -219,19 +219,19 @@ void main() { listenableUnderTest = new Listenable.merge([source1]); expect( - listenableUnderTest.toString(), + listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier'])", ); listenableUnderTest = new Listenable.merge([source1, source2]); expect( - listenableUnderTest.toString(), + listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])", ); listenableUnderTest = new Listenable.merge([null, source2]); expect( - listenableUnderTest.toString(), + listenableUnderTest.toString(), "Listenable.merge([null, Instance of 'TestNotifier'])", ); }); From c8ae2f879b7bb5426e190256e4d618070f8539c7 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 28 Feb 2018 16:37:36 -0800 Subject: [PATCH 029/109] Add a hasListeners to ChangeNotifier (#14946) I found that some ValueListeners want to know when they should start doing work (e.g. if the value comes from polling a network resource). --- packages/listen/lib/src/listen.dart | 21 ++++++++++++++++++++ packages/listen/test/listen_test.dart | 28 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 2ea4d5d7..88f9abca 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -68,6 +68,27 @@ class ChangeNotifier extends Listenable { 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 + 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. diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index af5186db..87fda33d 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -235,4 +235,32 @@ void main() { "Listenable.merge([null, Instance of 'TestNotifier'])", ); }); + + test('hasListeners', () { + final HasListenersTester notifier = new HasListenersTester(true); + expect(notifier.testHasListeners, isFalse); + void test1() { } + void test2() { } + notifier.addListener(test1); + expect(notifier.testHasListeners, isTrue); + notifier.addListener(test1); + expect(notifier.testHasListeners, isTrue); + notifier.removeListener(test1); + expect(notifier.testHasListeners, isTrue); + notifier.removeListener(test1); + expect(notifier.testHasListeners, isFalse); + notifier.addListener(test1); + expect(notifier.testHasListeners, isTrue); + notifier.addListener(test2); + expect(notifier.testHasListeners, isTrue); + notifier.removeListener(test1); + expect(notifier.testHasListeners, isTrue); + notifier.removeListener(test2); + expect(notifier.testHasListeners, isFalse); + }); +} + +class HasListenersTester extends ValueNotifier { + HasListenersTester(T value) : super(value); + bool get testHasListeners => hasListeners; } From e0105b90f618ba07cdf55673d8c8b448b17ee8aa Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Wed, 12 Sep 2018 08:29:29 +0200 Subject: [PATCH 030/109] Unnecessary new (#20138) * enable lint unnecessary_new * fix tests * fix tests * fix tests --- packages/listen/lib/src/listen.dart | 8 ++--- packages/listen/test/listen_test.dart | 44 +++++++++++++-------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 88f9abca..707612f0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -53,12 +53,12 @@ abstract class ValueListenable extends Listenable { /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. class ChangeNotifier extends Listenable { - ObserverList _listeners = new ObserverList(); + ObserverList _listeners = ObserverList(); bool _debugAssertNotDisposed() { assert(() { if (_listeners == null) { - throw new FlutterError( + throw FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.' ); @@ -154,13 +154,13 @@ class ChangeNotifier extends Listenable { void notifyListeners() { assert(_debugAssertNotDisposed()); if (_listeners != null) { - final List localListeners = new List.from(_listeners); + final List localListeners = List.from(_listeners); for (VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) listener(); } catch (exception, stack) { - FlutterError.reportError(new FlutterErrorDetails( + FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'foundation library', diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 87fda33d..85c2194e 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -19,7 +19,7 @@ void main() { final VoidCallback listener2 = () { log.add('listener2'); }; final VoidCallback badListener = () { log.add('badListener'); throw null; }; - final TestNotifier test = new TestNotifier(); + final TestNotifier test = TestNotifier(); test.addListener(listener); test.addListener(listener); @@ -85,7 +85,7 @@ void main() { }); test('ChangeNotifier with mutating listener', () { - final TestNotifier test = new TestNotifier(); + final TestNotifier test = TestNotifier(); final List log = []; final VoidCallback listener1 = () { log.add('listener1'); }; @@ -115,12 +115,12 @@ void main() { }); test('Merging change notifiers', () { - final TestNotifier source1 = new TestNotifier(); - final TestNotifier source2 = new TestNotifier(); - final TestNotifier source3 = new TestNotifier(); + final TestNotifier source1 = TestNotifier(); + final TestNotifier source2 = TestNotifier(); + final TestNotifier source3 = TestNotifier(); final List log = []; - final Listenable merged = new Listenable.merge([source1, source2]); + final Listenable merged = Listenable.merge([source1, source2]); final VoidCallback listener1 = () { log.add('listener1'); }; final VoidCallback listener2 = () { log.add('listener2'); }; @@ -148,11 +148,11 @@ void main() { }); test('Merging change notifiers ignores null', () { - final TestNotifier source1 = new TestNotifier(); - final TestNotifier source2 = new TestNotifier(); + final TestNotifier source1 = TestNotifier(); + final TestNotifier source2 = TestNotifier(); final List log = []; - final Listenable merged = new Listenable.merge([null, source1, null, source2, null]); + final Listenable merged = Listenable.merge([null, source1, null, source2, null]); final VoidCallback listener = () { log.add('listener'); }; merged.addListener(listener); @@ -163,11 +163,11 @@ void main() { }); test('Can dispose merged notifier', () { - final TestNotifier source1 = new TestNotifier(); - final TestNotifier source2 = new TestNotifier(); + final TestNotifier source1 = TestNotifier(); + final TestNotifier source2 = TestNotifier(); final List log = []; - final ChangeNotifier merged = new Listenable.merge([source1, source2]); + final ChangeNotifier merged = Listenable.merge([source1, source2]); final VoidCallback listener = () { log.add('listener'); }; merged.addListener(listener); @@ -183,7 +183,7 @@ void main() { }); test('Cannot use a disposed ChangeNotifier', () { - final TestNotifier source = new TestNotifier(); + final TestNotifier source = TestNotifier(); source.dispose(); expect(() { source.addListener(null); }, throwsFlutterError); expect(() { source.removeListener(null); }, throwsFlutterError); @@ -192,7 +192,7 @@ void main() { }); test('Value notifier', () { - final ValueNotifier notifier = new ValueNotifier(2.0); + final ValueNotifier notifier = ValueNotifier(2.0); final List log = []; final VoidCallback listener = () { log.add(notifier.value); }; @@ -208,28 +208,28 @@ void main() { }); test('Listenable.merge toString', () { - final TestNotifier source1 = new TestNotifier(); - final TestNotifier source2 = new TestNotifier(); + final TestNotifier source1 = TestNotifier(); + final TestNotifier source2 = TestNotifier(); - ChangeNotifier listenableUnderTest = new Listenable.merge([]); + ChangeNotifier listenableUnderTest = Listenable.merge([]); expect(listenableUnderTest.toString(), 'Listenable.merge([])'); - listenableUnderTest = new Listenable.merge([null]); + listenableUnderTest = Listenable.merge([null]); expect(listenableUnderTest.toString(), 'Listenable.merge([null])'); - listenableUnderTest = new Listenable.merge([source1]); + listenableUnderTest = Listenable.merge([source1]); expect( listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier'])", ); - listenableUnderTest = new Listenable.merge([source1, source2]); + listenableUnderTest = Listenable.merge([source1, source2]); expect( listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])", ); - listenableUnderTest = new Listenable.merge([null, source2]); + listenableUnderTest = Listenable.merge([null, source2]); expect( listenableUnderTest.toString(), "Listenable.merge([null, Instance of 'TestNotifier'])", @@ -237,7 +237,7 @@ void main() { }); test('hasListeners', () { - final HasListenersTester notifier = new HasListenersTester(true); + final HasListenersTester notifier = HasListenersTester(true); expect(notifier.testHasListeners, isFalse); void test1() { } void test2() { } From 84440f261a02710c7ccdc5939a3badff01c9d638 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Sat, 27 Oct 2018 16:51:39 -0700 Subject: [PATCH 031/109] [H] Created a variant of InheritedWidget specifically for Listenables (#23393) * Created a variant of InheritedWidget specifically for Listenables * Add more documentation per review comments --- packages/listen/lib/src/listen.dart | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 707612f0..566dada9 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -10,6 +10,49 @@ import 'diagnostics.dart'; import 'observer_list.dart'; /// An object that maintains a list of listeners. +/// +/// The listeners are typically used to notify clients that the object has been +/// updated. +/// +/// There are two variants of this interface: +/// +/// * [ValueListenable], an interface that augments the [Listenable] interface +/// with the concept of a _current value_. +/// +/// * [Animation], an interface that augments the [ValueListenable] interface +/// to add the concept of direction (forward or reverse). +/// +/// Many classes in the Flutter API use or implement these interfaces. The +/// following subclasses are especially relevant: +/// +/// * [ChangeNotifier], which can be subclassed or mixed in to create objects +/// that implement the [Listenable] interface. +/// +/// * [ValueNotifier], which implements the [ValueListenable] interface with +/// a mutable value that triggers the notifications when modified. +/// +/// The terms "notify clients", "send notifications", "trigger notifications", +/// and "fire notifications" are used interchangeably. +/// +/// See also: +/// +/// * [AnimatedBuilder], a widget that uses a builder callback to rebuild +/// whenever a given [Listenable] triggers its notifications. This widget is +/// commonly used with [Animation] subclasses, wherein its name. It is a +/// subclass of [AnimatedWidget], which can be used to create widgets that +/// are driven from a [Listenable]. +/// +/// * [ValueListenableBuilder], a widget that uses a builder callback to +/// rebuild whenever a [ValueListenable] object triggers its notifications, +/// providing the builder with the value of the object. +/// +/// * [InheritedNotifier], an abstract superclass for widgets that use a +/// [Listenable]'s notifications to trigger rebuilds in descendant widgets +/// that declare a dependency on them, using the [InheritedWidget] mechanism. +/// +/// * [new Listenable.merge], which creates a [Listenable] that triggers +/// notifications whenever any of a list of other [Listenable]s trigger their +/// notifications. abstract class Listenable { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. @@ -37,6 +80,10 @@ abstract class Listenable { /// This interface is implemented by [ValueNotifier] and [Animation], and /// allows other APIs to accept either of those implementations interchangeably. abstract class ValueListenable extends Listenable { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const ValueListenable(); + /// The current value of the object. When the value changes, the callbacks /// registered with [addListener] will be invoked. T get value; From 8b034f20f8cab49c0da1bd3255227c12b3b994b0 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 29 Oct 2018 19:44:36 -0700 Subject: [PATCH 032/109] Allow ChangeNotifier to be mixed in again (#23631) Luckily this class didn't actually need to extend its superclass, it only implements the interface. So we can change `extends` to `implements` and that's close enough, while allowing the class to be mixed in again. --- packages/listen/lib/src/listen.dart | 2 +- packages/listen/test/listen_test.dart | 35 ++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 566dada9..13a2e6d0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -99,7 +99,7 @@ abstract class ValueListenable extends Listenable { /// See also: /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. -class ChangeNotifier extends Listenable { +class ChangeNotifier implements Listenable { ObserverList _listeners = ObserverList(); bool _debugAssertNotDisposed() { diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 85c2194e..7897c41d 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -11,6 +11,24 @@ class TestNotifier extends ChangeNotifier { } } +class HasListenersTester extends ValueNotifier { + HasListenersTester(T value) : super(value); + bool get testHasListeners => hasListeners; +} + +class A { + bool result = false; + void test() { result = true; } +} + +class B extends A with ChangeNotifier { + @override + void test() { + notifyListeners(); + super.test(); + } +} + void main() { testWidgets('ChangeNotifier', (WidgetTester tester) async { final List log = []; @@ -258,9 +276,18 @@ void main() { notifier.removeListener(test2); expect(notifier.testHasListeners, isFalse); }); -} -class HasListenersTester extends ValueNotifier { - HasListenersTester(T value) : super(value); - bool get testHasListeners => hasListeners; + test('ChangeNotifier as a mixin', () { + // We document that this is a valid way to use this class. + final B b = B(); + int notifications = 0; + b.addListener(() { + notifications += 1; + }); + expect(b.result, isFalse); + expect(notifications, 0); + b.test(); + expect(b.result, isTrue); + expect(notifications, 1); + }); } From 3cd601c0006d9aa49f0eeae8e0ed9080caca6d52 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Tue, 18 Dec 2018 21:45:20 +0100 Subject: [PATCH 033/109] make see also sections uniform (#25513) --- packages/listen/lib/src/listen.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 13a2e6d0..2b215c10 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -41,15 +41,12 @@ import 'observer_list.dart'; /// commonly used with [Animation] subclasses, wherein its name. It is a /// subclass of [AnimatedWidget], which can be used to create widgets that /// are driven from a [Listenable]. -/// /// * [ValueListenableBuilder], a widget that uses a builder callback to /// rebuild whenever a [ValueListenable] object triggers its notifications, /// providing the builder with the value of the object. -/// /// * [InheritedNotifier], an abstract superclass for widgets that use a /// [Listenable]'s notifications to trigger rebuilds in descendant widgets /// that declare a dependency on them, using the [InheritedWidget] mechanism. -/// /// * [new Listenable.merge], which creates a [Listenable] that triggers /// notifications whenever any of a list of other [Listenable]s trigger their /// notifications. From 003ce83a88c8b4bc32841a4045abdd8f5e6179d6 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 10 Jan 2019 12:17:45 -0800 Subject: [PATCH 034/109] Fix Listenable.merge to not leak (#26313) --- packages/listen/lib/src/listen.dart | 22 ++++++++++++-------- packages/listen/test/listen_test.dart | 30 +++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 2b215c10..823b7389 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -220,19 +220,23 @@ class ChangeNotifier implements Listenable { } } -class _MergingListenable extends ChangeNotifier { - _MergingListenable(this._children) { - for (Listenable child in _children) - child?.addListener(notifyListeners); - } +class _MergingListenable extends Listenable { + _MergingListenable(this._children); final List _children; @override - void dispose() { - for (Listenable child in _children) - child?.removeListener(notifyListeners); - super.dispose(); + void addListener(VoidCallback listener) { + for (final Listenable child in _children) { + child?.addListener(listener); + } + } + + @override + void removeListener(VoidCallback listener) { + for (final Listenable child in _children) { + child?.removeListener(listener); + } } @override diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 7897c41d..6786fad5 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -9,6 +9,8 @@ class TestNotifier extends ChangeNotifier { void notify() { notifyListeners(); } + + bool get isListenedTo => hasListeners; } class HasListenersTester extends ValueNotifier { @@ -180,12 +182,12 @@ void main() { log.clear(); }); - test('Can dispose merged notifier', () { + test('Can remove from merged notifier', () { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); final List log = []; - final ChangeNotifier merged = Listenable.merge([source1, source2]); + final Listenable merged = Listenable.merge([source1, source2]); final VoidCallback listener = () { log.add('listener'); }; merged.addListener(listener); @@ -193,8 +195,8 @@ void main() { source2.notify(); expect(log, ['listener', 'listener']); log.clear(); - merged.dispose(); + merged.removeListener(listener); source1.notify(); source2.notify(); expect(log, isEmpty); @@ -229,7 +231,7 @@ void main() { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); - ChangeNotifier listenableUnderTest = Listenable.merge([]); + Listenable listenableUnderTest = Listenable.merge([]); expect(listenableUnderTest.toString(), 'Listenable.merge([])'); listenableUnderTest = Listenable.merge([null]); @@ -254,6 +256,26 @@ void main() { ); }); + test('Listenable.merge does not leak', () { + // Regression test for https://github.com/flutter/flutter/issues/25163. + + final TestNotifier source1 = TestNotifier(); + final TestNotifier source2 = TestNotifier(); + final VoidCallback fakeListener = () {}; + + final Listenable listenableUnderTest = Listenable.merge([source1, source2]); + expect(source1.isListenedTo, isFalse); + expect(source2.isListenedTo, isFalse); + listenableUnderTest.addListener(fakeListener); + expect(source1.isListenedTo, isTrue); + expect(source2.isListenedTo, isTrue); + + listenableUnderTest.removeListener(fakeListener); + expect(source1.isListenedTo, isFalse); + expect(source2.isListenedTo, isFalse); + }); + + test('hasListeners', () { final HasListenersTester notifier = HasListenersTester(true); expect(notifier.testHasListeners, isFalse); From 2dca3d3cf9ecc485e279d9af7f01048e5ceb9a8a Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 30 Jan 2019 08:56:12 -0800 Subject: [PATCH 035/109] Remove all obsolete "// ignore:" (#27271) --- packages/listen/lib/src/listen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 823b7389..7a53b0e0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -195,6 +195,7 @@ class ChangeNotifier implements Listenable { /// 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) { From e3335e88a63bf0399df9b28c5c7bcc720f2e819f Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 31 Jan 2019 14:28:01 -0800 Subject: [PATCH 036/109] [H] Add ImageStreamCompleter.hasListeners (and cleanup) (#25865) * Remove stray extra space * Add ImageStreamCompleter.hasListeners (and cleanup) This is mostly just some cleanup of stuff I ran into, but it makes `hasListeners` protected on `ImageStreamCompleter`, because otherwise there's no way to track if listeners are registered or not. * Address review comments --- packages/listen/test/listen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 6786fad5..0cbc0ffd 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -1,4 +1,4 @@ - // Copyright 2016 The Chromium Authors. All rights reserved. +// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From e75fc4e6225b879214ceca3d8d230b69d8c64d3a Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Fri, 1 Mar 2019 08:17:55 +0100 Subject: [PATCH 037/109] Add missing trailing commas (#28673) * add trailing commas on list/map/parameters * add trailing commas on Invocation with nb of arg>1 * add commas for widget containing widgets * add trailing commas if instantiation contains trailing comma * revert bad change --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7a53b0e0..8a27022a 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -213,7 +213,7 @@ class ChangeNotifier implements Listenable { informationCollector: (StringBuffer information) { information.writeln('The $runtimeType sending notification was:'); information.write(' $this'); - } + }, )); } } From 407052ada1e516713cc573a802cf21d11378eb3a Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Sat, 9 Mar 2019 09:03:11 +0100 Subject: [PATCH 038/109] fix block formatting (#29051) --- packages/listen/test/listen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 0cbc0ffd..3c09faaa 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -261,7 +261,7 @@ void main() { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); - final VoidCallback fakeListener = () {}; + final VoidCallback fakeListener = () { }; final Listenable listenableUnderTest = Listenable.merge([source1, source2]); expect(source1.isListenedTo, isFalse); From 592b64be3e3fd6d8760c6b0f00a808fdd9a4e7a8 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Wed, 20 Mar 2019 23:23:31 +0100 Subject: [PATCH 039/109] some spaces formatting (#29452) * some space formattings * always use blocks in if-else if a block is used * format spaces in for and while * allow multiline if conditions * fix missing space --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 8a27022a..4c906f1f 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -228,7 +228,7 @@ class _MergingListenable extends Listenable { @override void addListener(VoidCallback listener) { - for (final Listenable child in _children) { + for (final Listenable child in _children) { child?.addListener(listener); } } From bd2c3ffde11a55caf1f8820379bcc1cdd4d238a1 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 4 Apr 2019 00:13:07 -0700 Subject: [PATCH 040/109] Be more explicit when ValueNotifier notifies (#30461) --- packages/listen/lib/src/listen.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 4c906f1f..47de62c0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -248,14 +248,18 @@ class _MergingListenable extends Listenable { /// A [ChangeNotifier] that holds a single value. /// -/// When [value] is replaced, this class notifies its listeners. +/// When [value] is replaced with something that is not equal to the old +/// value as evaluated by the equality operator ==, this class notifies its +/// listeners. class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value); /// The current value stored in this notifier. /// - /// When the value is replaced, this class notifies its listeners. + /// When the value is replaced with something that is not equal to the old + /// value as evaluated by the equality operator ==, this class notifies its + /// listeners. @override T get value => _value; T _value; From 1064ef9327dfab9857d4280260275126aabcb43f Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Wed, 1 May 2019 11:20:12 -0700 Subject: [PATCH 041/109] Refactor core uses of FlutterError. (#30983) Make FlutterError objects more structured so they can be displayed better in debugging tools such as Dart DevTools. --- packages/listen/lib/src/listen.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 47de62c0..1e51dd15 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -209,10 +209,13 @@ class ChangeNotifier implements Listenable { exception: exception, stack: stack, library: 'foundation library', - context: 'while dispatching notifications for $runtimeType', - informationCollector: (StringBuffer information) { - information.writeln('The $runtimeType sending notification was:'); - information.write(' $this'); + context: ErrorDescription('while dispatching notifications for $runtimeType'), + informationCollector: () sync* { + yield DiagnosticsProperty( + 'The $runtimeType sending notification was', + this, + style: DiagnosticsTreeStyle.errorProperty, + ); }, )); } From 599a3598c98ba6ef73ee1b4330ad252b5e639b9a Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Sun, 9 Jun 2019 11:03:46 -0700 Subject: [PATCH 042/109] Enable web foundation tests (#34032) --- packages/listen/test/listen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 3c09faaa..020ecdc8 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -102,7 +102,7 @@ void main() { expect(log, ['badListener', 'listener1', 'listener2']); expect(tester.takeException(), isNullThrownError); log.clear(); - }); + }, skip: isBrowser); test('ChangeNotifier with mutating listener', () { final TestNotifier test = TestNotifier(); From a1dc2dd67e0fb14028ed8359fa99c54be58f882b Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Tue, 24 Sep 2019 21:03:37 +0200 Subject: [PATCH 043/109] fix some bad indentations (#41172) --- packages/listen/test/listen_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 020ecdc8..d0ef1046 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -37,7 +37,10 @@ void main() { final VoidCallback listener = () { log.add('listener'); }; final VoidCallback listener1 = () { log.add('listener1'); }; final VoidCallback listener2 = () { log.add('listener2'); }; - final VoidCallback badListener = () { log.add('badListener'); throw null; }; + final VoidCallback badListener = () { + log.add('badListener'); + throw null; + }; final TestNotifier test = TestNotifier(); From 5f22f5250ffd4b2de661fa001f63a66520c993a1 Mon Sep 17 00:00:00 2001 From: Albertus Angga Raharja Date: Mon, 28 Oct 2019 11:00:49 -0700 Subject: [PATCH 044/109] Add more structure to errors (continuation of #34684) (#42640) * Add structured errors in Animations, TabView, ChangeNotifier * Add structured error on MaterialPageRoute, BoxBorder, DecorationImagePainter, TextSpan * Add structured errors in Debug * Fix test errors * Add structured errors in Scaffold and Stepper * Add structured errors in part of Rendering Layer * Fix failing test due to FloatingPoint precision * Fix failing tests due to precision error and not using final * Fix failing test due to floating precision error with RegEx instead * Add structured error in CustomLayout and increase test coverage * Add structured error & its test in ListBody * Add structured error in ProxyBox and increase test coverage * Add structured error message in Viewport * Fix styles and add more assertions on ErrorHint and DiagnosticProperty * Add structured error in scheduler/binding and scheduler/ticker Signed-off-by: Albertus Angga Raharja * Add structured error in AssetBundle and TextInput Signed-off-by: Albertus Angga Raharja * Add structured errors in several widgets #1 Signed-off-by: Albertus Angga Raharja * Remove unused import Signed-off-by: Albertus Angga Raharja * Add assertions on hint messages Signed-off-by: Albertus Angga Raharja * Fix catch spacing Signed-off-by: Albertus Angga Raharja * Add structured error in several widgets part 2 and increase code coverage Signed-off-by: Albertus Angga Raharja * Add structured error in flutter_test/widget_tester * Fix floating precision accuracy by using RegExp Signed-off-by: Albertus Angga Raharja * Remove todo to add tests in Scaffold showBottomSheet Signed-off-by: Albertus Angga Raharja * Fix reviews by indenting lines and fixing the assertion orders Signed-off-by: Albertus Angga Raharja * Fix failing tests due to renaming class Signed-off-by: Albertus Angga Raharja * Try skipping the NetworkBundleTest Signed-off-by: Albertus Angga Raharja * Remove leading space in material/debug error hint Signed-off-by: Albertus Angga Raharja --- packages/listen/lib/src/listen.dart | 8 ++++---- packages/listen/test/listen_test.dart | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 1e51dd15..51732dca 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -102,10 +102,10 @@ class ChangeNotifier implements Listenable { 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.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; }()); diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index d0ef1046..f9d53870 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -315,4 +315,24 @@ void main() { expect(b.result, isTrue); expect(notifications, 1); }); + + test('Throws FlutterError when disposed and called', () { + final TestNotifier testNotifier = TestNotifier(); + testNotifier.dispose(); + FlutterError error; + try { + testNotifier.dispose(); + } on FlutterError catch (e) { + error = e; + } + expect(error, isNotNull); + expect(error, isFlutterError); + expect(error.toStringDeep(), equalsIgnoringHashCodes( + 'FlutterError\n' + ' A TestNotifier was used after being disposed.\n' + ' Once you have called dispose() on a TestNotifier, it can no\n' + ' longer be used.\n' + )); + }); + } From 075b144a68a2c17d3a93976a5655f57c9ef08896 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 27 Nov 2019 15:04:02 -0800 Subject: [PATCH 045/109] License update (#45373) * Update project.pbxproj files to say Flutter rather than Chromium Also, the templates now have an empty organization so that we don't cause people to give their apps a Flutter copyright. * Update the copyright notice checker to require a standard notice on all files * Update copyrights on Dart files. (This was a mechanical commit.) * Fix weird license headers on Dart files that deviate from our conventions; relicense Shrine. Some were already marked "The Flutter Authors", not clear why. Their dates have been normalized. Some were missing the blank line after the license. Some were randomly different in trivial ways for no apparent reason (e.g. missing the trailing period). * Clean up the copyrights in non-Dart files. (Manual edits.) Also, make sure templates don't have copyrights. * Fix some more ORGANIZATIONNAMEs --- packages/listen/lib/src/listen.dart | 2 +- packages/listen/test/listen_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 51732dca..4174f0b0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -1,4 +1,4 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. +// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index f9d53870..7b69f973 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -1,4 +1,4 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. +// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. From 46c3bbebec63d97384dce4c7ef7db8a0fee0260f Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Tue, 7 Jan 2020 16:32:04 +0100 Subject: [PATCH 046/109] enable lint prefer_final_in_for_each (#47724) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 4174f0b0..3f7cac30 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -200,7 +200,7 @@ class ChangeNotifier implements Listenable { assert(_debugAssertNotDisposed()); if (_listeners != null) { final List localListeners = List.from(_listeners); - for (VoidCallback listener in localListeners) { + for (final VoidCallback listener in localListeners) { try { if (_listeners.contains(listener)) listener(); From 120772a3546c10f8ab03ca3673feb94388d25ccc Mon Sep 17 00:00:00 2001 From: Albertus Angga Raharja Date: Thu, 20 Feb 2020 22:51:53 +0700 Subject: [PATCH 047/109] Avoid using FlutterError.fromParts when possible (#43696) This PR is a follow up of https://github.com/flutter/flutter/pull/42640 Some changes of that PR includes redundant changes using FlutterError.fromParts constructor even though it's not necessary. Some minor changes are: - Remove one unnecessary todo - Fix indent consistencies --- packages/listen/lib/src/listen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 3f7cac30..c45aa2af 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -102,10 +102,10 @@ class ChangeNotifier implements Listenable { 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.') - ]); + 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; }()); From 2c68313d69d16b14491477f2f3938143eadb3670 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 10 Mar 2020 15:23:45 -0700 Subject: [PATCH 048/109] =?UTF-8?q?Add=20sample=20for=20InheritedNotifier,?= =?UTF-8?q?=20convert=20two=20others=20to=20DartPa=E2=80=A6=20(#52349)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a sample for InheritedNotifier, and converts a couple of other samples to be DartPad samples. I also added a new sample template stateful_widget_material_ticker, which adds a TickerProviderStateMixin to the state object so that animation controllers can be created there easily. --- packages/listen/lib/src/listen.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index c45aa2af..7f3ba4bd 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -38,9 +38,10 @@ import 'observer_list.dart'; /// /// * [AnimatedBuilder], a widget that uses a builder callback to rebuild /// whenever a given [Listenable] triggers its notifications. This widget is -/// commonly used with [Animation] subclasses, wherein its name. It is a -/// subclass of [AnimatedWidget], which can be used to create widgets that -/// are driven from a [Listenable]. +/// commonly used with [Animation] subclasses, hence its name, but is by no +/// means limited to animations, as it can be used with any [Listenable]. It +/// is a subclass of [AnimatedWidget], which can be used to create widgets +/// that are driven from a [Listenable]. /// * [ValueListenableBuilder], a widget that uses a builder callback to /// rebuild whenever a [ValueListenable] object triggers its notifications, /// providing the builder with the value of the object. From 4ee3d6222f39a6b88e642c0d3aa87c6d6cbab362 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 7 Apr 2020 16:49:39 -0700 Subject: [PATCH 049/109] Revise Action API (#42940) This updates the Action API in accordance with the design doc for the changes: flutter.dev/go/actions-and-shortcuts-design-revision Fixes #53276 --- packages/listen/lib/src/listen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7f3ba4bd..0e92bce9 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -183,9 +183,9 @@ class ChangeNotifier 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]. From 18139a9feb33d90d533c8a4a4adac46beb17887c Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 8 Apr 2020 12:37:03 -0700 Subject: [PATCH 050/109] Skip Audits (2) (#53837) --- packages/listen/test/listen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 7b69f973..86f0e9aa 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -105,7 +105,7 @@ void main() { expect(log, ['badListener', 'listener1', 'listener2']); expect(tester.takeException(), isNullThrownError); log.clear(); - }, skip: isBrowser); + }); test('ChangeNotifier with mutating listener', () { final TestNotifier test = TestNotifier(); From 4ef2c3b1883cd3c9d306a610a4a3d746b1371bfb Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Thu, 11 Jun 2020 14:11:30 +0200 Subject: [PATCH 051/109] Opt out nnbd in packages/flutter (#59186) * add language version 2.8 in packages/flutter * enable non-nullable analyzer flag --- packages/listen/lib/src/listen.dart | 2 ++ packages/listen/test/listen_test.dart | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 0e92bce9..ecdf8fcc 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.8 + import 'package:meta/meta.dart'; import 'assertions.dart'; diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 86f0e9aa..61c6cd07 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.8 + import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; From 21806215a6e695ffea5b7eee05506356744cfbe1 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Wed, 15 Jul 2020 18:55:27 +0200 Subject: [PATCH 052/109] migrate foundation to nullsafety (#61188) * migrate foundation to nullsafety * address review comments --- packages/listen/lib/src/listen.dart | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index ecdf8fcc..26c39a3f 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - import 'package:meta/meta.dart'; import 'assertions.dart'; @@ -65,7 +63,7 @@ abstract class Listenable { /// will lead to memory leaks or exceptions. /// /// The list may contain nulls; they are ignored. - factory Listenable.merge(List listenables) = _MergingListenable; + factory Listenable.merge(List listenables) = _MergingListenable; /// Register a closure to be called when the object notifies its listeners. void addListener(VoidCallback listener); @@ -100,7 +98,7 @@ abstract class ValueListenable extends Listenable { /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. class ChangeNotifier implements Listenable { - ObserverList _listeners = ObserverList(); + ObserverList? _listeners = ObserverList(); bool _debugAssertNotDisposed() { assert(() { @@ -133,7 +131,7 @@ class ChangeNotifier implements Listenable { @protected bool get hasListeners { assert(_debugAssertNotDisposed()); - return _listeners.isNotEmpty; + return _listeners!.isNotEmpty; } /// Register a closure to be called when the object changes. @@ -142,7 +140,7 @@ class ChangeNotifier implements Listenable { @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); - _listeners.add(listener); + _listeners!.add(listener); } /// Remove a previously registered closure from the list of closures that are @@ -167,7 +165,7 @@ class ChangeNotifier implements Listenable { @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); - _listeners.remove(listener); + _listeners!.remove(listener); } /// Discards any resources used by the object. After this is called, the @@ -202,10 +200,10 @@ class ChangeNotifier implements Listenable { void notifyListeners() { assert(_debugAssertNotDisposed()); if (_listeners != null) { - final List localListeners = List.from(_listeners); + final List localListeners = List.from(_listeners!); for (final VoidCallback listener in localListeners) { try { - if (_listeners.contains(listener)) + if (_listeners!.contains(listener)) listener(); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( @@ -230,18 +228,18 @@ class ChangeNotifier implements Listenable { class _MergingListenable extends Listenable { _MergingListenable(this._children); - final List _children; + final List _children; @override void addListener(VoidCallback listener) { - for (final Listenable child in _children) { + for (final Listenable? child in _children) { child?.addListener(listener); } } @override void removeListener(VoidCallback listener) { - for (final Listenable child in _children) { + for (final Listenable? child in _children) { child?.removeListener(listener); } } From d9976c46cce883eabcf45be803d25e0f537a7fb7 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Wed, 22 Jul 2020 18:28:08 -0700 Subject: [PATCH 053/109] Minor doc updates (#62008) --- packages/listen/lib/src/listen.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 26c39a3f..ec52884b 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -77,6 +77,12 @@ abstract class Listenable { /// /// This interface is implemented by [ValueNotifier] and [Animation], and /// allows other APIs to accept either of those implementations interchangeably. +/// +/// See also: +/// +/// * [ValueListenableBuilder], a widget that uses a builder callback to +/// rebuild whenever a [ValueListenable] object triggers its notifications, +/// providing the builder with the value of the object. abstract class ValueListenable extends Listenable { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. From 2538ba669cd95e72a8be8cf263489a0da6130f88 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Wed, 12 Aug 2020 13:41:09 -0700 Subject: [PATCH 054/109] [null-safety] update to several framework test cases/APIs for null assertions (#62946) --- packages/listen/test/listen_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 61c6cd07..29e5ec13 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -210,8 +210,8 @@ void main() { test('Cannot use a disposed ChangeNotifier', () { final TestNotifier source = TestNotifier(); source.dispose(); - expect(() { source.addListener(null); }, throwsFlutterError); - expect(() { source.removeListener(null); }, throwsFlutterError); + expect(() { source.addListener(() { }); }, throwsFlutterError); + expect(() { source.removeListener(() { }); }, throwsFlutterError); expect(() { source.dispose(); }, throwsFlutterError); expect(() { source.notify(); }, throwsFlutterError); }); From 9e3eaf5e734adc45047ac04e4af8f9ba87e62258 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Wed, 26 Aug 2020 19:34:03 +0100 Subject: [PATCH 055/109] Use a LinkedList to improve the performance of ChangeNotifier (#62330) * Use a LinkedList in ChangeNotifier implementation to take O(N^2) notification time to O(N) --- packages/listen/lib/src/listen.dart | 66 ++++++++++++++++----------- packages/listen/test/listen_test.dart | 61 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 27 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index ec52884b..0293cf48 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:collection'; + import 'package:meta/meta.dart'; import 'assertions.dart'; import 'basic_types.dart'; import 'diagnostics.dart'; -import 'observer_list.dart'; /// An object that maintains a list of listeners. /// @@ -93,18 +94,22 @@ abstract class ValueListenable extends Listenable { T get value; } +class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { + _ListenerEntry(this.listener); + final VoidCallback listener; +} + /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. /// -/// [ChangeNotifier] is optimized for small numbers (one or two) of listeners. -/// It is O(N) for adding and removing listeners and O(N²) for dispatching +/// It is O(1) for adding listeners and O(N) for removing listeners and dispatching /// notifications (where N is the number of listeners). /// /// See also: /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. class ChangeNotifier implements Listenable { - ObserverList? _listeners = ObserverList(); + LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>(); bool _debugAssertNotDisposed() { assert(() { @@ -146,7 +151,7 @@ class ChangeNotifier implements Listenable { @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 @@ -171,7 +176,12 @@ class ChangeNotifier implements Listenable { @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 @@ -205,27 +215,29 @@ class ChangeNotifier implements Listenable { @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/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 29e5ec13..cb3f6a8a 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -139,6 +139,67 @@ void main() { log.clear(); }); + test('During notifyListeners, a listener was added and removed immediately', () { + final TestNotifier source = TestNotifier(); + final List log = []; + + final VoidCallback listener3 = () { log.add('listener3'); }; + final VoidCallback listener2 = () { log.add('listener2'); }; + void listener1() { + log.add('listener1'); + source.addListener(listener2); + source.removeListener(listener2); + source.addListener(listener3); + } + + source.addListener(listener1); + + source.notify(); + + expect(log, ['listener1']); + }); + + test( + 'If a listener in the middle of the list of listeners removes itself, ' + 'notifyListeners still notifies all listeners', () { + final TestNotifier source = TestNotifier(); + final List log = []; + + void selfRemovingListener() { + log.add('selfRemovingListener'); + source.removeListener(selfRemovingListener); + } + final VoidCallback listener1 = () { log.add('listener1'); }; + + source.addListener(listener1); + source.addListener(selfRemovingListener); + source.addListener(listener1); + + source.notify(); + + expect(log, ['listener1', 'selfRemovingListener', 'listener1']); + }); + + test('If the first listener removes itself, notifyListeners still notify all listeners', () { + final TestNotifier source = TestNotifier(); + final List log = []; + + void selfRemovingListener() { + log.add('selfRemovingListener'); + source.removeListener(selfRemovingListener); + } + void listener1() { + log.add('listener1'); + } + + source.addListener(selfRemovingListener); + source.addListener(listener1); + + source.notifyListeners(); + + expect(log, ['selfRemovingListener', 'listener1']); + }); + test('Merging change notifiers', () { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); From caaf8886bc45b4b19c3581155f62964ff12644fc Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 5 Oct 2020 00:42:50 -0700 Subject: [PATCH 056/109] Migrate foundation test to nullsafety (#62616) * Migrate --- packages/listen/test/listen_test.dart | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index cb3f6a8a..1ec3c64e 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -41,7 +39,7 @@ void main() { final VoidCallback listener2 = () { log.add('listener2'); }; final VoidCallback badListener = () { log.add('badListener'); - throw null; + throw ArgumentError(); }; final TestNotifier test = TestNotifier(); @@ -95,7 +93,7 @@ void main() { test.addListener(badListener); test.notify(); expect(log, ['listener', 'listener2', 'listener1', 'badListener']); - expect(tester.takeException(), isNullThrownError); + expect(tester.takeException(), isArgumentError); log.clear(); test.addListener(listener1); @@ -105,7 +103,7 @@ void main() { test.addListener(listener2); test.notify(); expect(log, ['badListener', 'listener1', 'listener2']); - expect(tester.takeException(), isNullThrownError); + expect(tester.takeException(), isArgumentError); log.clear(); }); @@ -238,7 +236,7 @@ void main() { final TestNotifier source2 = TestNotifier(); final List log = []; - final Listenable merged = Listenable.merge([null, source1, null, source2, null]); + final Listenable merged = Listenable.merge([null, source1, null, source2, null]); final VoidCallback listener = () { log.add('listener'); }; merged.addListener(listener); @@ -300,7 +298,7 @@ void main() { Listenable listenableUnderTest = Listenable.merge([]); expect(listenableUnderTest.toString(), 'Listenable.merge([])'); - listenableUnderTest = Listenable.merge([null]); + listenableUnderTest = Listenable.merge([null]); expect(listenableUnderTest.toString(), 'Listenable.merge([null])'); listenableUnderTest = Listenable.merge([source1]); @@ -315,7 +313,7 @@ void main() { "Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])", ); - listenableUnderTest = Listenable.merge([null, source2]); + listenableUnderTest = Listenable.merge([null, source2]); expect( listenableUnderTest.toString(), "Listenable.merge([null, Instance of 'TestNotifier'])", @@ -382,14 +380,14 @@ void main() { test('Throws FlutterError when disposed and called', () { final TestNotifier testNotifier = TestNotifier(); testNotifier.dispose(); - FlutterError error; + FlutterError? error; try { testNotifier.dispose(); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); - expect(error, isFlutterError); + expect(error!, isFlutterError); expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' A TestNotifier was used after being disposed.\n' From 049ecbd477c5093c5dadb85168740e511a801a04 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 19 Oct 2020 11:26:50 -0700 Subject: [PATCH 057/109] Mark keys that match a shortcut, but have no action defined as "not handled". (#67359) - - When I added notification of key events before processing them as text, it made it so that shortcut key bindings like the spacebar would prevent spaces from being inserted into text fields, which is obviously not desirable (and so that change was reverted). At the same time, we do want to make it possible to override key events so that they can do things like intercept a tab key or arrow keys that change the focus. This PR changes the behavior of the Shortcuts widget so that if it has a shortcut defined, but no action is bound to the intent, then instead of responding that the key is "handled", it responds as if nothing handled it. This allows the engine to continue to process the key as text entry. This PR includes: - Modification of the callback type for key handlers to return a KeyEventResult instead of a bool, so that we can return more information (i.e. the extra state of "stop propagation"). - Modification of the ActionDispatcher.invokeAction contract to require that Action.isEnabled return true before calling it. It will now assert if the action isn't enabled when invokeAction is called. This is to allow optimization of the number of calls to isEnabled, since the shortcuts widget now wants to know if the action was enabled before deciding to either handle the key or to return ignored. - Modification to ShortcutManager.handleKeypress to return KeyEventResult.ignored for keys which don't have an enabled action associated with them. - Adds an attribute to DoNothingAction that allows it to mark a key as not handled, even though it does have an action associated with it. This will allow disabling of a shortcut for a subtree. --- packages/listen/lib/src/listen.dart | 41 ++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 0293cf48..f44cacb6 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -147,7 +147,30 @@ class ChangeNotifier implements Listenable { /// 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()); @@ -161,18 +184,12 @@ class ChangeNotifier 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()); @@ -208,7 +225,7 @@ class ChangeNotifier implements Listenable { /// /// 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 From af721570074ba0e1e616f261341b79dd68d7e48a Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Fri, 5 Mar 2021 18:29:04 -0800 Subject: [PATCH 058/109] enable prefer_function_declarations_over_variables lint (#77398) --- packages/listen/test/listen_test.dart | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 1ec3c64e..eb19e4f7 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -34,13 +34,13 @@ class B extends A with ChangeNotifier { void main() { testWidgets('ChangeNotifier', (WidgetTester tester) async { final List log = []; - final VoidCallback listener = () { log.add('listener'); }; - final VoidCallback listener1 = () { log.add('listener1'); }; - final VoidCallback listener2 = () { log.add('listener2'); }; - final VoidCallback badListener = () { + void listener() { log.add('listener'); } + void listener1() { log.add('listener1'); } + void listener2() { log.add('listener2'); } + void badListener() { log.add('badListener'); throw ArgumentError(); - }; + } final TestNotifier test = TestNotifier(); @@ -111,15 +111,15 @@ void main() { final TestNotifier test = TestNotifier(); final List log = []; - final VoidCallback listener1 = () { log.add('listener1'); }; - final VoidCallback listener3 = () { log.add('listener3'); }; - final VoidCallback listener4 = () { log.add('listener4'); }; - final VoidCallback listener2 = () { + void listener1() { log.add('listener1'); } + void listener3() { log.add('listener3'); } + void listener4() { log.add('listener4'); } + void listener2() { log.add('listener2'); test.removeListener(listener1); test.removeListener(listener3); test.addListener(listener4); - }; + } test.addListener(listener1); test.addListener(listener2); @@ -141,8 +141,8 @@ void main() { final TestNotifier source = TestNotifier(); final List log = []; - final VoidCallback listener3 = () { log.add('listener3'); }; - final VoidCallback listener2 = () { log.add('listener2'); }; + void listener3() { log.add('listener3'); } + void listener2() { log.add('listener2'); } void listener1() { log.add('listener1'); source.addListener(listener2); @@ -167,7 +167,7 @@ void main() { log.add('selfRemovingListener'); source.removeListener(selfRemovingListener); } - final VoidCallback listener1 = () { log.add('listener1'); }; + void listener1() { log.add('listener1'); } source.addListener(listener1); source.addListener(selfRemovingListener); @@ -205,8 +205,8 @@ void main() { final List log = []; final Listenable merged = Listenable.merge([source1, source2]); - final VoidCallback listener1 = () { log.add('listener1'); }; - final VoidCallback listener2 = () { log.add('listener2'); }; + void listener1() { log.add('listener1'); } + void listener2() { log.add('listener2'); } merged.addListener(listener1); source1.notify(); @@ -237,7 +237,7 @@ void main() { final List log = []; final Listenable merged = Listenable.merge([null, source1, null, source2, null]); - final VoidCallback listener = () { log.add('listener'); }; + void listener() { log.add('listener'); } merged.addListener(listener); source1.notify(); @@ -252,7 +252,7 @@ void main() { final List log = []; final Listenable merged = Listenable.merge([source1, source2]); - final VoidCallback listener = () { log.add('listener'); }; + void listener() { log.add('listener'); } merged.addListener(listener); source1.notify(); @@ -279,7 +279,7 @@ void main() { final ValueNotifier notifier = ValueNotifier(2.0); final List log = []; - final VoidCallback listener = () { log.add(notifier.value); }; + void listener() { log.add(notifier.value); } notifier.addListener(listener); notifier.value = 3.0; @@ -325,7 +325,7 @@ void main() { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); - final VoidCallback fakeListener = () { }; + void fakeListener() { } final Listenable listenableUnderTest = Listenable.merge([source1, source2]); expect(source1.isListenedTo, isFalse); From 70a1d53898cd1265ad00fa1b590440f6eacd2f29 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Tue, 30 Mar 2021 06:29:02 +0200 Subject: [PATCH 059/109] add missing trailing commas (#79299) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index f44cacb6..ce7b7166 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -116,7 +116,7 @@ class ChangeNotifier implements Listenable { 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.' + 'Once you have called dispose() on a $runtimeType, it can no longer be used.', ); } return true; From bcfd49931a37b4acea5cc77badf19174517382e2 Mon Sep 17 00:00:00 2001 From: Romain Rastel Date: Thu, 15 Apr 2021 01:49:03 +0200 Subject: [PATCH 060/109] Improve the performances of ChangeNotifier (#71947) --- packages/listen/lib/src/listen.dart | 144 ++++++++++++++--- packages/listen/test/listen_test.dart | 218 ++++++++++++++++++++++---- 2 files changed, 307 insertions(+), 55 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index ce7b7166..4dd56c4f 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:collection'; - import 'package:meta/meta.dart'; import 'assertions.dart'; @@ -94,11 +92,6 @@ abstract class ValueListenable extends Listenable { T get value; } -class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { - _ListenerEntry(this.listener); - final VoidCallback listener; -} - /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. /// @@ -109,11 +102,15 @@ class _ListenerEntry extends LinkedListEntry<_ListenerEntry> { /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. class ChangeNotifier implements Listenable { - LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>(); + int _count = 0; + List _listeners = List.filled(0, null); + int _notificationCallStackDepth = 0; + int _reentrantlyRemovedListeners = 0; + bool _debugDisposed = false; bool _debugAssertNotDisposed() { assert(() { - if (_listeners == null) { + if (_debugDisposed) { throw FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.', @@ -142,7 +139,7 @@ class ChangeNotifier implements Listenable { @protected bool get hasListeners { assert(_debugAssertNotDisposed()); - return _listeners!.isNotEmpty; + return _count > 0; } /// Register a closure to be called when the object changes. @@ -174,7 +171,48 @@ class ChangeNotifier implements Listenable { @override void addListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); - _listeners!.add(_ListenerEntry(listener)); + if (_count == _listeners.length) { + if (_count == 0) { + _listeners = List.filled(1, null); + } else { + final List newListeners = + List.filled(_listeners.length * 2, null); + for (int i = 0; i < _count; i++) { + newListeners[i] = _listeners[i]; + } + _listeners = newListeners; + } + } + _listeners[_count++] = listener; + } + + void _removeAt(int index) { + // The list holding the listeners is not growable for performances reasons. + // We still want to shrink this list if a lot of listeners have been added + // and then removed outside a notifyListeners iteration. + // We do this only when the real number of listeners is half the length + // of our list. + _count -= 1; + if (_count * 2 <= _listeners.length) { + final List newListeners = List.filled(_count, null); + + // Listeners before the index are at the same place. + for (int i = 0; i < index; i++) + newListeners[i] = _listeners[i]; + + // Listeners after the index move towards the start of the list. + for (int i = index; i < _count; i++) + newListeners[i] = _listeners[i + 1]; + + _listeners = newListeners; + } else { + // When there are more listeners than half the length of the list, we only + // shift our listeners, so that we avoid to reallocate memory for the + // whole list. + for (int i = index; i < _count; i++) + _listeners[i] = _listeners[i + 1]; + _listeners[_count] = null; + } } /// Remove a previously registered closure from the list of closures that are @@ -193,10 +231,22 @@ class ChangeNotifier implements Listenable { @override void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); - for (final _ListenerEntry entry in _listeners!) { - if (entry.listener == listener) { - entry.unlink(); - return; + for (int i = 0; i < _count; i++) { + final VoidCallback? _listener = _listeners[i]; + if (_listener == listener) { + if (_notificationCallStackDepth > 0) { + // We don't resize the list during notifyListeners iterations + // but we set to null, the listeners we want to remove. We will + // effectively resize the list at the end of all notifyListeners + // iterations. + _listeners[i] = null; + _reentrantlyRemovedListeners++; + } else { + // When we are outside the notifyListeners iterations we can + // effectively shrink the list. + _removeAt(i); + } + break; } } } @@ -210,7 +260,10 @@ class ChangeNotifier implements Listenable { @mustCallSuper void dispose() { assert(_debugAssertNotDisposed()); - _listeners = null; + assert(() { + _debugDisposed = true; + return true; + }()); } /// Call all the registered listeners. @@ -232,15 +285,26 @@ class ChangeNotifier implements Listenable { @visibleForTesting void notifyListeners() { assert(_debugAssertNotDisposed()); - if (_listeners!.isEmpty) + if (_count == 0) return; - final List<_ListenerEntry> localListeners = List<_ListenerEntry>.from(_listeners!); + // To make sure that listeners removed during this iteration are not called, + // we set them to null, but we don't shrink the list right away. + // By doing this, we can continue to iterate on our list until it reaches + // the last listener added before the call to this method. + + // To allow potential listeners to recursively call notifyListener, we track + // the number of times this method is called in _notificationCallStackDepth. + // Once every recursive iteration is finished (i.e. when _notificationCallStackDepth == 0), + // we can safely shrink our list so that it will only contain not null + // listeners. - for (final _ListenerEntry entry in localListeners) { + _notificationCallStackDepth++; + + final int end = _count; + for (int i = 0; i < end; i++) { try { - if (entry.list != null) - entry.listener(); + _listeners[i]?.call(); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, @@ -257,6 +321,44 @@ class ChangeNotifier implements Listenable { )); } } + + _notificationCallStackDepth--; + + if (_notificationCallStackDepth == 0 && _reentrantlyRemovedListeners > 0) { + // We really remove the listeners when all notifications are done. + final int newLength = _count - _reentrantlyRemovedListeners; + if (newLength * 2 <= _listeners.length) { + // As in _removeAt, we only shrink the list when the real number of + // listeners is half the length of our list. + final List newListeners = List.filled(newLength, null); + + int newIndex = 0; + for (int i = 0; i < _count; i++) { + final VoidCallback? listener = _listeners[i]; + if (listener != null) { + newListeners[newIndex++] = listener; + } + } + + _listeners = newListeners; + } else { + // Otherwise we put all the null references at the end. + for (int i = 0; i < newLength; i += 1) { + if (_listeners[i] == null) { + // We swap this item with the next not null item. + int swapIndex = i + 1; + while(_listeners[swapIndex] == null){ + swapIndex += 1; + } + _listeners[i] = _listeners[swapIndex]; + _listeners[swapIndex] = null; + } + } + } + + _reentrantlyRemovedListeners = 0; + _count = newLength; + } } } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index eb19e4f7..05b406c4 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -20,7 +20,9 @@ class HasListenersTester extends ValueNotifier { class A { bool result = false; - void test() { result = true; } + void test() { + result = true; + } } class B extends A with ChangeNotifier { @@ -31,12 +33,36 @@ class B extends A with ChangeNotifier { } } +class Counter with ChangeNotifier { + int get value => _value; + int _value = 0; + set value(int value) { + if (_value != value) { + _value = value; + notifyListeners(); + } + } + + void notify() { + notifyListeners(); + } +} + void main() { testWidgets('ChangeNotifier', (WidgetTester tester) async { final List log = []; - void listener() { log.add('listener'); } - void listener1() { log.add('listener1'); } - void listener2() { log.add('listener2'); } + void listener() { + log.add('listener'); + } + + void listener1() { + log.add('listener1'); + } + + void listener2() { + log.add('listener2'); + } + void badListener() { log.add('badListener'); throw ArgumentError(); @@ -111,9 +137,18 @@ void main() { final TestNotifier test = TestNotifier(); final List log = []; - void listener1() { log.add('listener1'); } - void listener3() { log.add('listener3'); } - void listener4() { log.add('listener4'); } + void listener1() { + log.add('listener1'); + } + + void listener3() { + log.add('listener3'); + } + + void listener4() { + log.add('listener4'); + } + void listener2() { log.add('listener2'); test.removeListener(listener1); @@ -137,12 +172,19 @@ void main() { log.clear(); }); - test('During notifyListeners, a listener was added and removed immediately', () { + test('During notifyListeners, a listener was added and removed immediately', + () { final TestNotifier source = TestNotifier(); final List log = []; - void listener3() { log.add('listener3'); } - void listener2() { log.add('listener2'); } + void listener3() { + log.add('listener3'); + } + + void listener2() { + log.add('listener2'); + } + void listener1() { log.add('listener1'); source.addListener(listener2); @@ -167,7 +209,10 @@ void main() { log.add('selfRemovingListener'); source.removeListener(selfRemovingListener); } - void listener1() { log.add('listener1'); } + + void listener1() { + log.add('listener1'); + } source.addListener(listener1); source.addListener(selfRemovingListener); @@ -178,7 +223,9 @@ void main() { expect(log, ['listener1', 'selfRemovingListener', 'listener1']); }); - test('If the first listener removes itself, notifyListeners still notify all listeners', () { + test( + 'If the first listener removes itself, notifyListeners still notify all listeners', + () { final TestNotifier source = TestNotifier(); final List log = []; @@ -186,6 +233,7 @@ void main() { log.add('selfRemovingListener'); source.removeListener(selfRemovingListener); } + void listener1() { log.add('listener1'); } @@ -205,8 +253,13 @@ void main() { final List log = []; final Listenable merged = Listenable.merge([source1, source2]); - void listener1() { log.add('listener1'); } - void listener2() { log.add('listener2'); } + void listener1() { + log.add('listener1'); + } + + void listener2() { + log.add('listener2'); + } merged.addListener(listener1); source1.notify(); @@ -236,8 +289,11 @@ void main() { final TestNotifier source2 = TestNotifier(); final List log = []; - final Listenable merged = Listenable.merge([null, source1, null, source2, null]); - void listener() { log.add('listener'); } + final Listenable merged = + Listenable.merge([null, source1, null, source2, null]); + void listener() { + log.add('listener'); + } merged.addListener(listener); source1.notify(); @@ -252,7 +308,9 @@ void main() { final List log = []; final Listenable merged = Listenable.merge([source1, source2]); - void listener() { log.add('listener'); } + void listener() { + log.add('listener'); + } merged.addListener(listener); source1.notify(); @@ -269,22 +327,32 @@ void main() { test('Cannot use a disposed ChangeNotifier', () { final TestNotifier source = TestNotifier(); source.dispose(); - expect(() { source.addListener(() { }); }, throwsFlutterError); - expect(() { source.removeListener(() { }); }, throwsFlutterError); - expect(() { source.dispose(); }, throwsFlutterError); - expect(() { source.notify(); }, throwsFlutterError); + expect(() { + source.addListener(() {}); + }, throwsFlutterError); + expect(() { + source.removeListener(() {}); + }, throwsFlutterError); + expect(() { + source.dispose(); + }, throwsFlutterError); + expect(() { + source.notify(); + }, throwsFlutterError); }); test('Value notifier', () { final ValueNotifier notifier = ValueNotifier(2.0); final List log = []; - void listener() { log.add(notifier.value); } + void listener() { + log.add(notifier.value); + } notifier.addListener(listener); notifier.value = 3.0; - expect(log, equals([ 3.0 ])); + expect(log, equals([3.0])); log.clear(); notifier.value = 3.0; @@ -325,9 +393,10 @@ void main() { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); - void fakeListener() { } + void fakeListener() {} - final Listenable listenableUnderTest = Listenable.merge([source1, source2]); + final Listenable listenableUnderTest = + Listenable.merge([source1, source2]); expect(source1.isListenedTo, isFalse); expect(source2.isListenedTo, isFalse); listenableUnderTest.addListener(fakeListener); @@ -339,12 +408,11 @@ void main() { expect(source2.isListenedTo, isFalse); }); - test('hasListeners', () { final HasListenersTester notifier = HasListenersTester(true); expect(notifier.testHasListeners, isFalse); - void test1() { } - void test2() { } + void test1() {} + void test2() {} notifier.addListener(test1); expect(notifier.testHasListeners, isTrue); notifier.addListener(test1); @@ -388,12 +456,94 @@ void main() { } expect(error, isNotNull); expect(error!, isFlutterError); - expect(error.toStringDeep(), equalsIgnoringHashCodes( - 'FlutterError\n' - ' A TestNotifier was used after being disposed.\n' - ' Once you have called dispose() on a TestNotifier, it can no\n' - ' longer be used.\n' - )); + expect( + error.toStringDeep(), + equalsIgnoringHashCodes('FlutterError\n' + ' A TestNotifier was used after being disposed.\n' + ' Once you have called dispose() on a TestNotifier, it can no\n' + ' longer be used.\n')); + }); + + test('notifyListener can be called recursively', () { + final Counter counter = Counter(); + final List log = []; + + void listener1() { + log.add('listener1'); + if (counter.value < 0) { + counter.value = 0; + } + } + + counter.addListener(listener1); + counter.notify(); + expect(log, ['listener1']); + log.clear(); + + counter.value = 3; + expect(log, ['listener1']); + log.clear(); + + counter.value = -2; + expect(log, ['listener1', 'listener1']); + log.clear(); }); + test('Remove Listeners while notifying on a list which will not resize', () { + final TestNotifier test = TestNotifier(); + final List log = []; + final List listeners = []; + + void autoRemove() { + // We remove 4 listeners. + // We will end up with (13-4 = 9) listeners. + test.removeListener(listeners[1]); + test.removeListener(listeners[3]); + test.removeListener(listeners[4]); + test.removeListener(autoRemove); + } + + test.addListener(autoRemove); + + // We add 12 more listeners. + for (int i = 0; i < 12; i++) { + void listener() { + log.add('listener$i'); + } + + listeners.add(listener); + test.addListener(listener); + } + + final List remainingListenerIndexes = [ + 0, + 2, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ]; + final List expectedLog = + remainingListenerIndexes.map((int i) => 'listener$i').toList(); + + test.notify(); + expect(log, expectedLog); + + log.clear(); + // We expect to have the same result after the removal of previous listeners. + test.notify(); + expect(log, expectedLog); + + // We remove all other listeners. + for (int i = 0; i < remainingListenerIndexes.length; i++) { + test.removeListener(listeners[remainingListenerIndexes[i]]); + } + + log.clear(); + test.notify(); + expect(log, []); + }); } From 77d3dc253e40ee7dabf33dead73a1c0efa6ba93a Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 15 Apr 2021 10:49:02 -0700 Subject: [PATCH 061/109] Treat some exceptions as unhandled when a debugger is attached (#78649) --- packages/listen/lib/src/listen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 4dd56c4f..a895823e 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -283,6 +283,7 @@ class ChangeNotifier implements Listenable { /// See the discussion at [removeListener]. @protected @visibleForTesting + @pragma('vm:notify-debugger-on-exception') void notifyListeners() { assert(_debugAssertNotDisposed()); if (_count == 0) From f476d220b089d6f58db3ecc19a4fda0b2658b622 Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Thu, 22 Apr 2021 22:33:59 +0200 Subject: [PATCH 062/109] add trailing commas in flutter/test/{foundation,gestures} (#80926) --- packages/listen/test/listen_test.dart | 60 ++++++++++++++------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 05b406c4..bca63e32 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -172,8 +172,7 @@ void main() { log.clear(); }); - test('During notifyListeners, a listener was added and removed immediately', - () { + test('During notifyListeners, a listener was added and removed immediately', () { final TestNotifier source = TestNotifier(); final List log = []; @@ -200,32 +199,32 @@ void main() { }); test( - 'If a listener in the middle of the list of listeners removes itself, ' - 'notifyListeners still notifies all listeners', () { - final TestNotifier source = TestNotifier(); - final List log = []; - - void selfRemovingListener() { - log.add('selfRemovingListener'); - source.removeListener(selfRemovingListener); - } + 'If a listener in the middle of the list of listeners removes itself, ' + 'notifyListeners still notifies all listeners', + () { + final TestNotifier source = TestNotifier(); + final List log = []; + + void selfRemovingListener() { + log.add('selfRemovingListener'); + source.removeListener(selfRemovingListener); + } - void listener1() { - log.add('listener1'); - } + void listener1() { + log.add('listener1'); + } - source.addListener(listener1); - source.addListener(selfRemovingListener); - source.addListener(listener1); + source.addListener(listener1); + source.addListener(selfRemovingListener); + source.addListener(listener1); - source.notify(); + source.notify(); - expect(log, ['listener1', 'selfRemovingListener', 'listener1']); - }); + expect(log, ['listener1', 'selfRemovingListener', 'listener1']); + }, + ); - test( - 'If the first listener removes itself, notifyListeners still notify all listeners', - () { + test('If the first listener removes itself, notifyListeners still notify all listeners', () { final TestNotifier source = TestNotifier(); final List log = []; @@ -457,11 +456,14 @@ void main() { expect(error, isNotNull); expect(error!, isFlutterError); expect( - error.toStringDeep(), - equalsIgnoringHashCodes('FlutterError\n' - ' A TestNotifier was used after being disposed.\n' - ' Once you have called dispose() on a TestNotifier, it can no\n' - ' longer be used.\n')); + error.toStringDeep(), + equalsIgnoringHashCodes( + 'FlutterError\n' + ' A TestNotifier was used after being disposed.\n' + ' Once you have called dispose() on a TestNotifier, it can no\n' + ' longer be used.\n', + ), + ); }); test('notifyListener can be called recursively', () { @@ -524,7 +526,7 @@ void main() { 8, 9, 10, - 11 + 11, ]; final List expectedLog = remainingListenerIndexes.map((int i) => 'listener$i').toList(); From ae484e4caa08c542046bce6a387db31acefaff3a Mon Sep 17 00:00:00 2001 From: Abhishek Ghaskata Date: Fri, 14 May 2021 23:14:03 +0530 Subject: [PATCH 063/109] Enable unnecessary_null_checks lint (#82084) --- packages/listen/test/listen_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index bca63e32..5cb15091 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -454,9 +454,9 @@ void main() { error = e; } expect(error, isNotNull); - expect(error!, isFlutterError); + expect(error, isFlutterError); expect( - error.toStringDeep(), + error!.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' A TestNotifier was used after being disposed.\n' From 8920924b60136584f242e742a3df2fdd295d16dc Mon Sep 17 00:00:00 2001 From: Ahmed Ashour Date: Thu, 1 Jul 2021 22:51:05 +0200 Subject: [PATCH 064/109] Add space before curly parentheses. (#85306) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index a895823e..095054ba 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -348,7 +348,7 @@ class ChangeNotifier implements Listenable { if (_listeners[i] == null) { // We swap this item with the next not null item. int swapIndex = i + 1; - while(_listeners[swapIndex] == null){ + while(_listeners[swapIndex] == null) { swapIndex += 1; } _listeners[i] = _listeners[swapIndex]; From 55352a4e2d060ccf23a52673161623f7e9e6cb8a Mon Sep 17 00:00:00 2001 From: Dan Field Date: Sun, 12 Dec 2021 13:05:03 -0800 Subject: [PATCH 065/109] Ban sync*/async* from user facing code (#95050) --- packages/listen/lib/src/listen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 095054ba..a910f03e 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -312,13 +312,13 @@ class ChangeNotifier implements Listenable { stack: stack, library: 'foundation library', context: ErrorDescription('while dispatching notifications for $runtimeType'), - informationCollector: () sync* { - yield DiagnosticsProperty( + informationCollector: () => [ + DiagnosticsProperty( 'The $runtimeType sending notification was', this, style: DiagnosticsTreeStyle.errorProperty, - ); - }, + ), + ], )); } } From 0867a08a1b8cf8d730f836768aef8709967cbdc9 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Fri, 21 Jan 2022 14:43:59 -0800 Subject: [PATCH 066/109] Enable no_leading_underscores_for_local_identifiers (#96422) --- packages/listen/lib/src/listen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index a910f03e..2775511b 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -232,8 +232,8 @@ class ChangeNotifier implements Listenable { void removeListener(VoidCallback listener) { assert(_debugAssertNotDisposed()); for (int i = 0; i < _count; i++) { - final VoidCallback? _listener = _listeners[i]; - if (_listener == listener) { + final VoidCallback? listenerAtIndex = _listeners[i]; + if (listenerAtIndex == listener) { if (_notificationCallStackDepth > 0) { // We don't resize the list during notifyListeners iterations // but we set to null, the listeners we want to remove. We will From cf527c8ab492d98847f97abb5043713c4c0d3bfb Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:10:13 -0800 Subject: [PATCH 067/109] Allow remove listener on disposed change notifier (#97988) --- packages/listen/lib/src/listen.dart | 20 +++++++++++++++----- packages/listen/test/listen_test.dart | 17 +++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 2775511b..5f45282d 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -103,7 +103,12 @@ abstract class ValueListenable extends Listenable { /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. class ChangeNotifier implements Listenable { int _count = 0; - List _listeners = List.filled(0, null); + // The _listeners is intentionally set to a fixed-length _GrowableList instead + // of const [] for performance reasons. + // See https://github.com/flutter/flutter/pull/71947/files#r545722476 for + // more details. + static final List _emptyListeners = List.filled(0, null); + List _listeners = _emptyListeners; int _notificationCallStackDepth = 0; int _reentrantlyRemovedListeners = 0; bool _debugDisposed = false; @@ -220,7 +225,7 @@ class ChangeNotifier implements Listenable { /// /// If the given listener is not registered, the call is ignored. /// - /// This method must not be called after [dispose] has been called. + /// This method returns immediately if [dispose] has been called. /// /// {@macro flutter.foundation.ChangeNotifier.addListener} /// @@ -230,7 +235,11 @@ class ChangeNotifier implements Listenable { /// changes. @override void removeListener(VoidCallback listener) { - assert(_debugAssertNotDisposed()); + // This method is allowed to be called on disposed instances for usability + // reasons. Due to how our frame scheduling logic between render objects and + // overlays, it is common that the owner of this instance would be disposed a + // frame earlier than the listeners. Allowing calls to this method after it + // is disposed makes it easier for listeners to properly clean up. for (int i = 0; i < _count; i++) { final VoidCallback? listenerAtIndex = _listeners[i]; if (listenerAtIndex == listener) { @@ -253,8 +262,7 @@ class ChangeNotifier implements Listenable { /// Discards any resources used by the object. After this is called, the /// object is not in a usable state and should be discarded (calls to - /// [addListener] and [removeListener] will throw after the object is - /// disposed). + /// [addListener] will throw after the object is disposed). /// /// This method should only be called by the object's owner. @mustCallSuper @@ -264,6 +272,8 @@ class ChangeNotifier implements Listenable { _debugDisposed = true; return true; }()); + _listeners = _emptyListeners; + _count = 0; } /// Call all the registered listeners. diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 5cb15091..d17d33ff 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -323,15 +323,12 @@ void main() { expect(log, isEmpty); }); - test('Cannot use a disposed ChangeNotifier', () { + test('Cannot use a disposed ChangeNotifier except for remove listener', () { final TestNotifier source = TestNotifier(); source.dispose(); expect(() { source.addListener(() {}); }, throwsFlutterError); - expect(() { - source.removeListener(() {}); - }, throwsFlutterError); expect(() { source.dispose(); }, throwsFlutterError); @@ -340,6 +337,18 @@ void main() { }, throwsFlutterError); }); + test('Can remove listener on a disposed ChangeNotifier', () { + final TestNotifier source = TestNotifier(); + FlutterError? error; + try { + source.dispose(); + source.removeListener(() {}); + } on FlutterError catch (e) { + error = e; + } + expect(error, isNull); + }); + test('Value notifier', () { final ValueNotifier notifier = ValueNotifier(2.0); From 00c6aee963a2eea7815e8fef331caa95f439e487 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Tue, 15 Feb 2022 18:15:18 -0800 Subject: [PATCH 068/109] Add explanation to ChangeNotifier (#98295) --- packages/listen/lib/src/listen.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 5f45282d..d3b521f7 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -104,9 +104,13 @@ abstract class ValueListenable extends Listenable { class ChangeNotifier implements Listenable { int _count = 0; // The _listeners is intentionally set to a fixed-length _GrowableList instead - // of const [] for performance reasons. - // See https://github.com/flutter/flutter/pull/71947/files#r545722476 for - // more details. + // of const []. + // + // The const [] creates an instance of _ImmutableList which would be + // different from fixed-length _GrowableList used elsewhere in this class. + // keeping runtime type the same during the lifetime of this class lets the + // compiler to infer concrete type for this property, and thus improves + // performance. static final List _emptyListeners = List.filled(0, null); List _listeners = _emptyListeners; int _notificationCallStackDepth = 0; From ad97e2450e1f53485ff8a86086018230815025e7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 28 Feb 2022 21:41:20 +0100 Subject: [PATCH 069/109] feat: Added docstring examples to AnimatedBuilder and ChangeNotifier (#98628) --- packages/listen/lib/src/listen.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index d3b521f7..a9e20eda 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -98,6 +98,8 @@ abstract class ValueListenable extends Listenable { /// It is O(1) for adding listeners and O(N) for removing listeners and dispatching /// notifications (where N is the number of listeners). /// +/// {@macro flutter.flutter.animatedbuilder_changenotifier.rebuild} +/// /// See also: /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. From f41901aa01c89dd179d75bf0a2926fb0968d71ec Mon Sep 17 00:00:00 2001 From: Pierre-Louis <6655696+guidezpl@users.noreply.github.com> Date: Mon, 21 Mar 2022 20:18:40 +0100 Subject: [PATCH 070/109] Fix `deprecated_new_in_comment_reference` for `material` library (#100289) * fix deprecated_new_in_comment_reference for `material` library in a future version of the SDK, these will be flagged, fix them now * Update pubspec.yaml --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index a9e20eda..48b3b2a5 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -47,7 +47,7 @@ import 'diagnostics.dart'; /// * [InheritedNotifier], an abstract superclass for widgets that use a /// [Listenable]'s notifications to trigger rebuilds in descendant widgets /// that declare a dependency on them, using the [InheritedWidget] mechanism. -/// * [new Listenable.merge], which creates a [Listenable] that triggers +/// * [Listenable.merge], which creates a [Listenable] that triggers /// notifications whenever any of a list of other [Listenable]s trigger their /// notifications. abstract class Listenable { From 752591f97ce07dffff695537557f437a7105aaf3 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 13 Apr 2022 22:38:40 -0700 Subject: [PATCH 071/109] super parameters for framework (#100905) --- packages/listen/test/listen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index d17d33ff..7b348e11 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -14,7 +14,7 @@ class TestNotifier extends ChangeNotifier { } class HasListenersTester extends ValueNotifier { - HasListenersTester(T value) : super(value); + HasListenersTester(super.value); bool get testHasListeners => hasListeners; } From c95bf1320f5de01796f29313d8e0fa956fc1d091 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 19 May 2022 13:17:42 -0700 Subject: [PATCH 072/109] Add ShortcutsRegistry (#103456) This adds a ShortcutsRegistry for ShortcutActivator to Intent mappings that can be modified from its descendants. This is so that descendants can make shortcuts dynamically available to a larger portion of the app than just their descendants. This is a precursor needed by the new MenuBar, for instance, so that the menu bar itself can be placed where it likes, but the shortcuts it defines can be in effect for most, if not all, of the UI surface in the app. For example, the "Ctrl-Q" quit binding would need to work even if the focused widget wasn't a child of the MenuBar. This just provides the shortcut to intent mapping, the actions activated by the intent are described in the context where they make sense. For example, defining a "Ctrl-C" shortcut mapped to a "CopyIntent" should perform different functions if it happens while a TextField has focus vs when a drawing has focus, so those different areas would need to define different actions mapped to "CopyIntent". A hypothetical "QuitIntent" would probably be active for the entire app, so would be mapped in an Actions widget near the top of the hierarchy. --- packages/listen/lib/src/listen.dart | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 48b3b2a5..a2b5790d 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -119,7 +119,24 @@ class ChangeNotifier implements Listenable { int _reentrantlyRemovedListeners = 0; bool _debugDisposed = false; - bool _debugAssertNotDisposed() { + /// Used by subclasses to assert that the [ChangeNotifier] has not yet been + /// disposed. + /// + /// {@tool snippet} + /// The `debugAssertNotDisposed` function should only be called inside of an + /// assert, as in this example. + /// + /// ```dart + /// class MyNotifier with ChangeNotifier { + /// void doUpdate() { + /// assert(debugAssertNotDisposed()); + /// // ... + /// } + /// } + /// ``` + /// {@end-tool} + @protected + bool debugAssertNotDisposed() { assert(() { if (_debugDisposed) { throw FlutterError( @@ -149,7 +166,7 @@ class ChangeNotifier implements Listenable { /// so, stopping that same work. @protected bool get hasListeners { - assert(_debugAssertNotDisposed()); + assert(debugAssertNotDisposed()); return _count > 0; } @@ -181,7 +198,7 @@ class ChangeNotifier implements Listenable { /// the list of closures that are notified when the object changes. @override void addListener(VoidCallback listener) { - assert(_debugAssertNotDisposed()); + assert(debugAssertNotDisposed()); if (_count == _listeners.length) { if (_count == 0) { _listeners = List.filled(1, null); @@ -273,7 +290,7 @@ class ChangeNotifier implements Listenable { /// This method should only be called by the object's owner. @mustCallSuper void dispose() { - assert(_debugAssertNotDisposed()); + assert(debugAssertNotDisposed()); assert(() { _debugDisposed = true; return true; @@ -301,7 +318,7 @@ class ChangeNotifier implements Listenable { @visibleForTesting @pragma('vm:notify-debugger-on-exception') void notifyListeners() { - assert(_debugAssertNotDisposed()); + assert(debugAssertNotDisposed()); if (_count == 0) return; From c0d40af66d57a49e28cb7318f64e42d40d43840a Mon Sep 17 00:00:00 2001 From: Pierre-Louis <6655696+guidezpl@users.noreply.github.com> Date: Wed, 25 May 2022 19:55:22 +0200 Subject: [PATCH 073/109] Use `curly_braces_in_flow_control_structures` for `foundation`, `gestures`, `painting`, `physics` (#104610) * Use `curly_braces_in_flow_control_structures` for `foundation` * Use `curly_braces_in_flow_control_structures` for `gestures` * Use `curly_braces_in_flow_control_structures` for `painting` * Use `curly_braces_in_flow_control_structures` for `physics` * fix comments * remove trailing space * fix TODO style --- packages/listen/lib/src/listen.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index a2b5790d..2a7daea7 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -225,20 +225,23 @@ class ChangeNotifier implements Listenable { final List newListeners = List.filled(_count, null); // Listeners before the index are at the same place. - for (int i = 0; i < index; i++) + for (int i = 0; i < index; i++) { newListeners[i] = _listeners[i]; + } // Listeners after the index move towards the start of the list. - for (int i = index; i < _count; i++) + for (int i = index; i < _count; i++) { newListeners[i] = _listeners[i + 1]; + } _listeners = newListeners; } else { // When there are more listeners than half the length of the list, we only // shift our listeners, so that we avoid to reallocate memory for the // whole list. - for (int i = index; i < _count; i++) + for (int i = index; i < _count; i++) { _listeners[i] = _listeners[i + 1]; + } _listeners[_count] = null; } } @@ -319,8 +322,9 @@ class ChangeNotifier implements Listenable { @pragma('vm:notify-debugger-on-exception') void notifyListeners() { assert(debugAssertNotDisposed()); - if (_count == 0) + if (_count == 0) { return; + } // To make sure that listeners removed during this iteration are not called, // we set them to null, but we don't shrink the list right away. @@ -439,8 +443,9 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { T get value => _value; T _value; set value(T newValue) { - if (_value == newValue) + if (_value == newValue) { return; + } _value = newValue; notifyListeners(); } From 7317aae9008427ce19269c3d707b3a07bd39fa50 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 31 May 2022 10:33:46 -0700 Subject: [PATCH 074/109] Switch debugAssertNotDisposed to be a static (#104772) This reverts part of the change made in #103456 to expose a debug check for subclasses of ChangeNotifier to avoid code duplication. Instead of making debugAssertNotDisposed a public instance function, it is now a public static function. It makes it harder to call, slightly, but it means that everyone who implemented ChangeNotifier instead of extending it doesn't get broken. --- packages/listen/lib/src/listen.dart | 23 +++++++++++++---------- packages/listen/test/listen_test.dart | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 2a7daea7..2b22a7fd 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -129,19 +129,22 @@ class ChangeNotifier implements Listenable { /// ```dart /// class MyNotifier with ChangeNotifier { /// void doUpdate() { - /// assert(debugAssertNotDisposed()); + /// assert(ChangeNotifier.debugAssertNotDisposed(this)); /// // ... /// } /// } /// ``` /// {@end-tool} - @protected - bool debugAssertNotDisposed() { + // This is static and not an instance method because too many people try to + // implement ChangeNotifier instead of extending it (and so it is too breaking + // to add a method, especially for debug). + static bool debugAssertNotDisposed(ChangeNotifier notifier) { assert(() { - if (_debugDisposed) { + if (notifier._debugDisposed) { throw FlutterError( - 'A $runtimeType was used after being disposed.\n' - 'Once you have called dispose() on a $runtimeType, it can no longer be used.', + 'A ${notifier.runtimeType} was used after being disposed.\n' + 'Once you have called dispose() on a ${notifier.runtimeType}, it ' + 'can no longer be used.', ); } return true; @@ -166,7 +169,7 @@ class ChangeNotifier implements Listenable { /// so, stopping that same work. @protected bool get hasListeners { - assert(debugAssertNotDisposed()); + assert(ChangeNotifier.debugAssertNotDisposed(this)); return _count > 0; } @@ -198,7 +201,7 @@ class ChangeNotifier implements Listenable { /// the list of closures that are notified when the object changes. @override void addListener(VoidCallback listener) { - assert(debugAssertNotDisposed()); + assert(ChangeNotifier.debugAssertNotDisposed(this)); if (_count == _listeners.length) { if (_count == 0) { _listeners = List.filled(1, null); @@ -293,7 +296,7 @@ class ChangeNotifier implements Listenable { /// This method should only be called by the object's owner. @mustCallSuper void dispose() { - assert(debugAssertNotDisposed()); + assert(ChangeNotifier.debugAssertNotDisposed(this)); assert(() { _debugDisposed = true; return true; @@ -321,7 +324,7 @@ class ChangeNotifier implements Listenable { @visibleForTesting @pragma('vm:notify-debugger-on-exception') void notifyListeners() { - assert(debugAssertNotDisposed()); + assert(ChangeNotifier.debugAssertNotDisposed(this)); if (_count == 0) { return; } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 7b348e11..cfdf4aa5 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -475,6 +475,29 @@ void main() { ); }); + test('Calling debugAssertNotDisposed works as intended', () { + final TestNotifier testNotifier = TestNotifier(); + expect(ChangeNotifier.debugAssertNotDisposed(testNotifier), isTrue); + testNotifier.dispose(); + FlutterError? error; + try { + ChangeNotifier.debugAssertNotDisposed(testNotifier); + } on FlutterError catch (e) { + error = e; + } + expect(error, isNotNull); + expect(error, isFlutterError); + expect( + error!.toStringDeep(), + equalsIgnoringHashCodes( + 'FlutterError\n' + ' A TestNotifier was used after being disposed.\n' + ' Once you have called dispose() on a TestNotifier, it can no\n' + ' longer be used.\n', + ), + ); + }); + test('notifyListener can be called recursively', () { final Counter counter = Counter(); final List log = []; From badb5ece0488bd184a094832961bb1e3d1174c5a Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Thu, 9 Jun 2022 20:53:11 +0200 Subject: [PATCH 075/109] Export public API types from foundation/*.dart library. (#105648) --- packages/listen/lib/src/listen.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 2b22a7fd..8721bc15 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -8,6 +8,8 @@ import 'assertions.dart'; import 'basic_types.dart'; import 'diagnostics.dart'; +export 'dart:ui' show VoidCallback; + /// An object that maintains a list of listeners. /// /// The listeners are typically used to notify clients that the object has been From 1351c0196523c443539c30e23789481d7554e2ff Mon Sep 17 00:00:00 2001 From: Alexandre Ardhuin Date: Fri, 24 Jun 2022 23:21:05 +0200 Subject: [PATCH 076/109] Export public API types from foundation/scheduler/gestures/semantics (#106409) --- packages/listen/lib/src/listen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 8721bc15..485be0fe 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,10 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show VoidCallback; + import 'package:meta/meta.dart'; import 'assertions.dart'; -import 'basic_types.dart'; import 'diagnostics.dart'; export 'dart:ui' show VoidCallback; From 1e6f91ee6302e42985254960ee1ca00a8c18b75f Mon Sep 17 00:00:00 2001 From: Dan Field Date: Tue, 26 Jul 2022 15:09:05 -0700 Subject: [PATCH 077/109] Update docs on ChangeNotifier.dispose and KeepAliveHandle.release (#108384) --- packages/listen/lib/src/listen.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 485be0fe..f79fb7b2 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -297,6 +297,10 @@ class ChangeNotifier implements Listenable { /// [addListener] will throw after the object is disposed). /// /// This method should only be called by the object's owner. + /// + /// This method does not notify listeners, and clears the listener list once + /// it is called. Consumers of this class must decide on whether to notify + /// listeners or not immediately before disposal. @mustCallSuper void dispose() { assert(ChangeNotifier.debugAssertNotDisposed(this)); From 11835bbd05d04b6da7313305336efcb0122ebf70 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Thu, 4 Aug 2022 10:21:06 -0700 Subject: [PATCH 078/109] Can call ChangeNotifier.hasListeners after disposed (#108931) --- packages/listen/lib/src/listen.dart | 7 +++---- packages/listen/test/listen_test.dart | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index f79fb7b2..71c7fc23 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -170,11 +170,10 @@ class ChangeNotifier implements Listenable { /// [notifyListeners]; and similarly, by overriding [removeListener], checking /// if [hasListeners] is false after calling `super.removeListener()`, and if /// so, stopping that same work. + /// + /// This method returns false if [dispose] has been called. @protected - bool get hasListeners { - assert(ChangeNotifier.debugAssertNotDisposed(this)); - return _count > 0; - } + bool get hasListeners => _count > 0; /// Register a closure to be called when the object changes. /// diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index cfdf4aa5..8ff4e29a 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -349,6 +349,20 @@ void main() { expect(error, isNull); }); + test('Can check hasListener on a disposed ChangeNotifier', () { + final HasListenersTester source = HasListenersTester(0); + source.addListener(() { }); + expect(source.testHasListeners, isTrue); + FlutterError? error; + try { + source.dispose(); + expect(source.testHasListeners, isFalse); + } on FlutterError catch (e) { + error = e; + } + expect(error, isNull); + }); + test('Value notifier', () { final ValueNotifier notifier = ValueNotifier(2.0); From e114036469238826654e1fcb11d8f9deb47e5fba Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Fri, 9 Sep 2022 17:23:10 -0700 Subject: [PATCH 079/109] Create class MemoryAllocations. (#110230) --- packages/listen/lib/src/listen.dart | 36 ++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 71c7fc23..50689d82 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; import 'assertions.dart'; import 'diagnostics.dart'; +import 'memory_allocations.dart'; export 'dart:ui' show VoidCallback; @@ -95,6 +96,8 @@ abstract class ValueListenable extends Listenable { T get value; } +const String _flutterFoundationLibrary = 'package:flutter/foundation.dart'; + /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. /// @@ -122,6 +125,13 @@ class ChangeNotifier implements Listenable { int _reentrantlyRemovedListeners = 0; bool _debugDisposed = false; + /// If true, the event [ObjectCreated] for this instance was dispatched to + /// [MemoryAllocations]. + /// + /// As [ChangedNotifier] is used as mixin, it does not have constructor, + /// so we use [addListener] to dispatch the event. + bool _creationDispatched = false; + /// Used by subclasses to assert that the [ChangeNotifier] has not yet been /// disposed. /// @@ -204,6 +214,16 @@ class ChangeNotifier implements Listenable { @override void addListener(VoidCallback listener) { assert(ChangeNotifier.debugAssertNotDisposed(this)); + if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { + MemoryAllocations.instance.dispatchObjectEvent(() { + return ObjectCreated( + library: _flutterFoundationLibrary, + className: 'ChangeNotifier', + object: this, + ); + }); + _creationDispatched = true; + } if (_count == _listeners.length) { if (_count == 0) { _listeners = List.filled(1, null); @@ -307,6 +327,9 @@ class ChangeNotifier implements Listenable { _debugDisposed = true; return true; }()); + if (kFlutterMemoryAllocationsEnabled && _creationDispatched) { + MemoryAllocations.instance.dispatchObjectEvent(() => ObjectDisposed(object: this)); + } _listeners = _emptyListeners; _count = 0; } @@ -441,7 +464,18 @@ class _MergingListenable extends Listenable { /// listeners. class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. - ValueNotifier(this._value); + ValueNotifier(this._value) { + if (kFlutterMemoryAllocationsEnabled) { + MemoryAllocations.instance.dispatchObjectEvent(() { + return ObjectCreated( + library: _flutterFoundationLibrary, + className: 'ValueNotifier', + object: this, + ); + }); + } + _creationDispatched = true; + } /// The current value stored in this notifier. /// From 856914a9604315e4bc09042ca9c411f4b819f97f Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 12 Sep 2022 11:31:45 -0700 Subject: [PATCH 080/109] Fix references to symbols to use brackets instead of backticks (#111331) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 50689d82..671f7665 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -136,7 +136,7 @@ class ChangeNotifier implements Listenable { /// disposed. /// /// {@tool snippet} - /// The `debugAssertNotDisposed` function should only be called inside of an + /// The [debugAssertNotDisposed] function should only be called inside of an /// assert, as in this example. /// /// ```dart From 2de98b93b756435e2c1d0448386fd3d14402758f Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Wed, 14 Sep 2022 17:11:12 -0700 Subject: [PATCH 081/109] Fix performance regression. (#111615) --- packages/listen/lib/src/listen.dart | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 671f7665..d742bd8c 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -215,13 +215,11 @@ class ChangeNotifier implements Listenable { void addListener(VoidCallback listener) { assert(ChangeNotifier.debugAssertNotDisposed(this)); if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { - MemoryAllocations.instance.dispatchObjectEvent(() { - return ObjectCreated( - library: _flutterFoundationLibrary, - className: 'ChangeNotifier', - object: this, - ); - }); + MemoryAllocations.instance.dispatchObjectEvent(ObjectCreated( + library: _flutterFoundationLibrary, + className: 'ChangeNotifier', + object: this, + )); _creationDispatched = true; } if (_count == _listeners.length) { @@ -328,7 +326,7 @@ class ChangeNotifier implements Listenable { return true; }()); if (kFlutterMemoryAllocationsEnabled && _creationDispatched) { - MemoryAllocations.instance.dispatchObjectEvent(() => ObjectDisposed(object: this)); + MemoryAllocations.instance.dispatchObjectEvent(ObjectDisposed(object: this)); } _listeners = _emptyListeners; _count = 0; @@ -466,13 +464,11 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - MemoryAllocations.instance.dispatchObjectEvent(() { - return ObjectCreated( - library: _flutterFoundationLibrary, - className: 'ValueNotifier', - object: this, - ); - }); + MemoryAllocations.instance.dispatchObjectEvent(ObjectCreated( + library: _flutterFoundationLibrary, + className: 'ValueNotifier', + object: this, + )); } _creationDispatched = true; } From 1179330119c1f0903aae66ac518548983a135881 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 20 Sep 2022 16:39:46 -0700 Subject: [PATCH 082/109] Instrument State, Layer, RenderObject and Element. (#111328) --- packages/listen/lib/src/listen.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index d742bd8c..bfd5ce58 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -215,11 +215,11 @@ class ChangeNotifier implements Listenable { void addListener(VoidCallback listener) { assert(ChangeNotifier.debugAssertNotDisposed(this)); if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { - MemoryAllocations.instance.dispatchObjectEvent(ObjectCreated( + MemoryAllocations.instance.dispatchObjectCreated( library: _flutterFoundationLibrary, - className: 'ChangeNotifier', + className: '$ChangeNotifier', object: this, - )); + ); _creationDispatched = true; } if (_count == _listeners.length) { @@ -326,7 +326,7 @@ class ChangeNotifier implements Listenable { return true; }()); if (kFlutterMemoryAllocationsEnabled && _creationDispatched) { - MemoryAllocations.instance.dispatchObjectEvent(ObjectDisposed(object: this)); + MemoryAllocations.instance.dispatchObjectDisposed(object: this); } _listeners = _emptyListeners; _count = 0; @@ -464,11 +464,11 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - MemoryAllocations.instance.dispatchObjectEvent(ObjectCreated( + MemoryAllocations.instance.dispatchObjectCreated( library: _flutterFoundationLibrary, - className: 'ValueNotifier', + className: '$ValueNotifier', object: this, - )); + ); } _creationDispatched = true; } From ddf33c681e9c043a76e6a15c78c394f68931493c Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Fri, 18 Nov 2022 09:29:59 -0800 Subject: [PATCH 083/109] Disallow dispose during listener callback (#114530) * Disallow dispose during listener callback * addressing comment * add comments to code * Addressing comments * fix test --- packages/listen/lib/src/listen.dart | 6 ++++++ packages/listen/test/listen_test.dart | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index bfd5ce58..c09c025e 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -321,6 +321,12 @@ class ChangeNotifier implements Listenable { @mustCallSuper void dispose() { assert(ChangeNotifier.debugAssertNotDisposed(this)); + assert( + _notificationCallStackDepth == 0, + 'The "dispose()" method on $this was called during the call to ' + '"notifyListeners()". This is likely to cause errors since it modifies ' + 'the list of listeners while the list is being used.', + ); assert(() { _debugDisposed = true; return true; diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 8ff4e29a..9025eada 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -49,6 +49,22 @@ class Counter with ChangeNotifier { } void main() { + testWidgets('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { + final TestNotifier test = TestNotifier(); + bool callbackDidFinish = false; + void foo() { + test.dispose(); + callbackDidFinish = true; + } + + test.addListener(foo); + test.notify(); + final AssertionError error = tester.takeException() as AssertionError; + expect(error.toString().contains('dispose()'), isTrue); + // Make sure it crashes during dispose call. + expect(callbackDidFinish, isFalse); + }); + testWidgets('ChangeNotifier', (WidgetTester tester) async { final List log = []; void listener() { From a55eb7c4ca82a3a57ab5b17f11df93247f70d9c2 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 6 Dec 2022 17:15:22 -0800 Subject: [PATCH 084/109] Add ListenableBuilder with examples (#116543) * Add ListenableBuilder with examples * Add tests * Add tests * Fix Test * Change AnimatedBuilder to be a subclass of ListenableBuilder --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index c09c025e..01b9ac09 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -104,7 +104,7 @@ const String _flutterFoundationLibrary = 'package:flutter/foundation.dart'; /// It is O(1) for adding listeners and O(N) for removing listeners and dispatching /// notifications (where N is the number of listeners). /// -/// {@macro flutter.flutter.animatedbuilder_changenotifier.rebuild} +/// {@macro flutter.flutter.ListenableBuilder.ChangeNotifier.rebuild} /// /// See also: /// From 52c3962f137bbf89dd43b4877fcb96ec24c9432c Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 21 Mar 2023 13:21:58 -0700 Subject: [PATCH 085/109] Bump lower Dart SDK constraints to 3.0 & add class modifiers (#122546) Bump lower Dart SDK constraints to 3.0 & add class modifiers --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 01b9ac09..5fcb5d8a 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -109,7 +109,7 @@ const String _flutterFoundationLibrary = 'package:flutter/foundation.dart'; /// See also: /// /// * [ValueNotifier], which is a [ChangeNotifier] that wraps a single value. -class ChangeNotifier implements Listenable { +mixin class ChangeNotifier implements Listenable { int _count = 0; // The _listeners is intentionally set to a fixed-length _GrowableList instead // of const []. From 32cf9838a6a9803d2b4d8c205cf0c5bcb0a69680 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 22 Mar 2023 11:58:57 -0700 Subject: [PATCH 086/109] Documentation improvements (#122787) Documentation improvements --- packages/listen/lib/src/listen.dart | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 5fcb5d8a..7883dbb8 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -104,7 +104,29 @@ const String _flutterFoundationLibrary = 'package:flutter/foundation.dart'; /// It is O(1) for adding listeners and O(N) for removing listeners and dispatching /// notifications (where N is the number of listeners). /// -/// {@macro flutter.flutter.ListenableBuilder.ChangeNotifier.rebuild} +/// ## Using ChangeNotifier subclasses for data models +/// +/// A data structure can extend or mix in [ChangeNotifier] to implement the +/// [Listenable] interface and thus become usable with widgets that listen for +/// changes to [Listenable]s, such as [ListenableBuilder]. +/// +/// {@tool dartpad} +/// The following example implements a simple counter that utilizes a +/// [ListenableBuilder] to limit rebuilds to only the [Text] widget containing +/// the count. The current count is stored in a [ChangeNotifier] subclass, which +/// rebuilds the [ListenableBuilder]'s contents when its value is changed. +/// +/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.2.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// In this case, the [ChangeNotifier] subclass encapsulates a list, and notifies +/// the clients any time an item is added to the list. This example only supports +/// adding items; as an exercise, consider adding buttons to remove items from +/// the list as well. +/// +/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.3.dart ** +/// {@end-tool} /// /// See also: /// @@ -466,6 +488,19 @@ class _MergingListenable extends Listenable { /// When [value] is replaced with something that is not equal to the old /// value as evaluated by the equality operator ==, this class notifies its /// listeners. +/// +/// ## Limitations +/// +/// Because this class only notifies listeners when the [value]'s _identity_ +/// changes, listeners will not be notified when mutable state within the +/// value itself changes. +/// +/// For example, a `ValueNotifier>` will not notify its listeners +/// when the _contents_ of the list are changed. +/// +/// As a result, this class is best used with only immutable data types. +/// +/// For mutable data types, consider extending [ChangeNotifier] directly. class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { From 550efcb884a9cd6a299f2adf20025014e140fd99 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 4 May 2023 12:09:41 -0700 Subject: [PATCH 087/109] Define testWidgetsWithLeakTracking. (#125063) --- packages/listen/test/listen_test.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 9025eada..661431e6 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'leak_tracking.dart'; + class TestNotifier extends ChangeNotifier { void notify() { notifyListeners(); @@ -49,23 +51,25 @@ class Counter with ChangeNotifier { } void main() { - testWidgets('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { final TestNotifier test = TestNotifier(); bool callbackDidFinish = false; void foo() { test.dispose(); callbackDidFinish = true; } - test.addListener(foo); + test.notify(); + final AssertionError error = tester.takeException() as AssertionError; expect(error.toString().contains('dispose()'), isTrue); // Make sure it crashes during dispose call. expect(callbackDidFinish, isFalse); + test.dispose(); }); - testWidgets('ChangeNotifier', (WidgetTester tester) async { + testWidgetsWithLeakTracking('ChangeNotifier', (WidgetTester tester) async { final List log = []; void listener() { log.add('listener'); @@ -147,6 +151,7 @@ void main() { expect(log, ['badListener', 'listener1', 'listener2']); expect(tester.takeException(), isArgumentError); log.clear(); + test.dispose(); }); test('ChangeNotifier with mutating listener', () { From 0977b2a278623c5afebb3a9bee2587448cd9a8e6 Mon Sep 17 00:00:00 2001 From: Tomasz Gucio <72562119+tgucio@users.noreply.github.com> Date: Mon, 15 May 2023 11:07:30 +0200 Subject: [PATCH 088/109] Add spaces after flow control statements (#126320) --- packages/listen/lib/src/listen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7883dbb8..0b8c6f4a 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -443,7 +443,7 @@ mixin class ChangeNotifier implements Listenable { if (_listeners[i] == null) { // We swap this item with the next not null item. int swapIndex = i + 1; - while(_listeners[swapIndex] == null) { + while (_listeners[swapIndex] == null) { swapIndex += 1; } _listeners[i] = _listeners[swapIndex]; From f61d5710c15b69736ffb32bf35621afab6611b63 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 14 Aug 2023 10:05:20 -0700 Subject: [PATCH 089/109] Unpin leak_tracker and handle breaking changes in API. (#132352) --- packages/listen/test/listen_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 661431e6..a7adb3df 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -4,8 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'leak_tracking.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestNotifier extends ChangeNotifier { void notify() { From 795dc9707dc9fb4c2b21a4c930b55fab826a002c Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Wed, 23 Aug 2023 15:55:28 -0700 Subject: [PATCH 090/109] Enable ChangeNotifier clients to dispatch event of object creation in constructor. (#133060) --- packages/listen/lib/src/listen.dart | 51 +++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 0b8c6f4a..d8ab7d57 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -207,6 +207,39 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; + /// Dispatches event of object creation to [MemoryAllocations.instance]. + /// + /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] + /// is false, the method is noop. + /// + /// Tools like leak_tracker use the event of object creation to help + /// developers identify the owner of the object, for troubleshooting purposes, + /// by taking stack trace at the moment of the event. + /// + /// But, as [ChangeNotifier] is mixin, it does not have its own constructor. So, it + /// communicates object creation in first `addListener`, that results + /// in the stack trace pointing to `addListener`, not to constructor. + /// + /// To make debugging easier, invoke [ChangeNotifier.maybeDispatchObjectCreation] + /// in constructor of the class. It will help + /// to identify the owner. + /// + /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` + /// so that the method is tree-shaken away when the flag is false. + @protected + void maybeDispatchObjectCreation() { + // Tree shaker does not include this method and the class MemoryAllocations + // if kFlutterMemoryAllocationsEnabled is false. + if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { + MemoryAllocations.instance.dispatchObjectCreated( + library: _flutterFoundationLibrary, + className: '$ChangeNotifier', + object: this, + ); + _creationDispatched = true; + } + } + /// Register a closure to be called when the object changes. /// /// If the given closure is already registered, an additional instance is @@ -236,14 +269,11 @@ mixin class ChangeNotifier implements Listenable { @override void addListener(VoidCallback listener) { assert(ChangeNotifier.debugAssertNotDisposed(this)); - if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { - MemoryAllocations.instance.dispatchObjectCreated( - library: _flutterFoundationLibrary, - className: '$ChangeNotifier', - object: this, - ); - _creationDispatched = true; + + if (kFlutterMemoryAllocationsEnabled) { + maybeDispatchObjectCreation(); } + if (_count == _listeners.length) { if (_count == 0) { _listeners = List.filled(1, null); @@ -505,13 +535,8 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - MemoryAllocations.instance.dispatchObjectCreated( - library: _flutterFoundationLibrary, - className: '$ValueNotifier', - object: this, - ); + maybeDispatchObjectCreation(); } - _creationDispatched = true; } /// The current value stored in this notifier. From 6b04285539ff40819ed206d3e3a6ba4073e81ad6 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Thu, 24 Aug 2023 13:41:57 -0700 Subject: [PATCH 091/109] Users of ChangeNotifier should dispatch event of object creation in constructor. (#133210) --- packages/listen/test/listen_test.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index a7adb3df..d766c619 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -27,6 +27,12 @@ class A { } class B extends A with ChangeNotifier { + B() { + if (kFlutterMemoryAllocationsEnabled) { + maybeDispatchObjectCreation(); + } + } + @override void test() { notifyListeners(); @@ -35,6 +41,12 @@ class B extends A with ChangeNotifier { } class Counter with ChangeNotifier { + Counter() { + if (kFlutterMemoryAllocationsEnabled) { + maybeDispatchObjectCreation(); + } + } + int get value => _value; int _value = 0; set value(int value) { From a0b459b651a42470e2236f572fcd4cc74215508c Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 18 Sep 2023 15:33:06 -0700 Subject: [PATCH 092/109] Resolve breaking change of adding a method to ChangeNotifier. (#134953) --- packages/listen/lib/src/listen.dart | 14 +++++++------- packages/listen/test/listen_test.dart | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index d8ab7d57..7bfc2fb2 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -207,7 +207,7 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; - /// Dispatches event of object creation to [MemoryAllocations.instance]. + /// Dispatches event of the [object] creation to [MemoryAllocations.instance]. /// /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] /// is false, the method is noop. @@ -227,16 +227,16 @@ mixin class ChangeNotifier implements Listenable { /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` /// so that the method is tree-shaken away when the flag is false. @protected - void maybeDispatchObjectCreation() { + static void maybeDispatchObjectCreation(ChangeNotifier object) { // Tree shaker does not include this method and the class MemoryAllocations // if kFlutterMemoryAllocationsEnabled is false. - if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { + if (kFlutterMemoryAllocationsEnabled && !object._creationDispatched) { MemoryAllocations.instance.dispatchObjectCreated( library: _flutterFoundationLibrary, className: '$ChangeNotifier', - object: this, + object: object, ); - _creationDispatched = true; + object._creationDispatched = true; } } @@ -271,7 +271,7 @@ mixin class ChangeNotifier implements Listenable { assert(ChangeNotifier.debugAssertNotDisposed(this)); if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + maybeDispatchObjectCreation(this); } if (_count == _listeners.length) { @@ -535,7 +535,7 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + ChangeNotifier.maybeDispatchObjectCreation(this); } } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index d766c619..6bb678a9 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -29,7 +29,7 @@ class A { class B extends A with ChangeNotifier { B() { if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + ChangeNotifier.maybeDispatchObjectCreation(this); } } @@ -43,7 +43,7 @@ class B extends A with ChangeNotifier { class Counter with ChangeNotifier { Counter() { if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + ChangeNotifier.maybeDispatchObjectCreation(this); } } From bc766be8f0be208dd8446568970d52955c095bd9 Mon Sep 17 00:00:00 2001 From: Zachary Anderson Date: Mon, 18 Sep 2023 16:04:06 -0700 Subject: [PATCH 093/109] Revert "Resolve breaking change of adding a method to ChangeNotifier." (#134978) Reverts flutter/flutter#134953 Several failures on CI --- packages/listen/lib/src/listen.dart | 14 +++++++------- packages/listen/test/listen_test.dart | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7bfc2fb2..d8ab7d57 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -207,7 +207,7 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; - /// Dispatches event of the [object] creation to [MemoryAllocations.instance]. + /// Dispatches event of object creation to [MemoryAllocations.instance]. /// /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] /// is false, the method is noop. @@ -227,16 +227,16 @@ mixin class ChangeNotifier implements Listenable { /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` /// so that the method is tree-shaken away when the flag is false. @protected - static void maybeDispatchObjectCreation(ChangeNotifier object) { + void maybeDispatchObjectCreation() { // Tree shaker does not include this method and the class MemoryAllocations // if kFlutterMemoryAllocationsEnabled is false. - if (kFlutterMemoryAllocationsEnabled && !object._creationDispatched) { + if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { MemoryAllocations.instance.dispatchObjectCreated( library: _flutterFoundationLibrary, className: '$ChangeNotifier', - object: object, + object: this, ); - object._creationDispatched = true; + _creationDispatched = true; } } @@ -271,7 +271,7 @@ mixin class ChangeNotifier implements Listenable { assert(ChangeNotifier.debugAssertNotDisposed(this)); if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(this); + maybeDispatchObjectCreation(); } if (_count == _listeners.length) { @@ -535,7 +535,7 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - ChangeNotifier.maybeDispatchObjectCreation(this); + maybeDispatchObjectCreation(); } } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 6bb678a9..d766c619 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -29,7 +29,7 @@ class A { class B extends A with ChangeNotifier { B() { if (kFlutterMemoryAllocationsEnabled) { - ChangeNotifier.maybeDispatchObjectCreation(this); + maybeDispatchObjectCreation(); } } @@ -43,7 +43,7 @@ class B extends A with ChangeNotifier { class Counter with ChangeNotifier { Counter() { if (kFlutterMemoryAllocationsEnabled) { - ChangeNotifier.maybeDispatchObjectCreation(this); + maybeDispatchObjectCreation(); } } From f0a1d7570fd8f3fd0750eecca8b621833346ecde Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 18 Sep 2023 20:31:54 -0700 Subject: [PATCH 094/109] Reland Resolve breaking change of adding a method to ChangeNotifier. (#134983) --- packages/listen/lib/src/listen.dart | 14 +++++++------- packages/listen/test/listen_test.dart | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index d8ab7d57..7bfc2fb2 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -207,7 +207,7 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; - /// Dispatches event of object creation to [MemoryAllocations.instance]. + /// Dispatches event of the [object] creation to [MemoryAllocations.instance]. /// /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] /// is false, the method is noop. @@ -227,16 +227,16 @@ mixin class ChangeNotifier implements Listenable { /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` /// so that the method is tree-shaken away when the flag is false. @protected - void maybeDispatchObjectCreation() { + static void maybeDispatchObjectCreation(ChangeNotifier object) { // Tree shaker does not include this method and the class MemoryAllocations // if kFlutterMemoryAllocationsEnabled is false. - if (kFlutterMemoryAllocationsEnabled && !_creationDispatched) { + if (kFlutterMemoryAllocationsEnabled && !object._creationDispatched) { MemoryAllocations.instance.dispatchObjectCreated( library: _flutterFoundationLibrary, className: '$ChangeNotifier', - object: this, + object: object, ); - _creationDispatched = true; + object._creationDispatched = true; } } @@ -271,7 +271,7 @@ mixin class ChangeNotifier implements Listenable { assert(ChangeNotifier.debugAssertNotDisposed(this)); if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + maybeDispatchObjectCreation(this); } if (_count == _listeners.length) { @@ -535,7 +535,7 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + ChangeNotifier.maybeDispatchObjectCreation(this); } } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index d766c619..6bb678a9 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -29,7 +29,7 @@ class A { class B extends A with ChangeNotifier { B() { if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + ChangeNotifier.maybeDispatchObjectCreation(this); } } @@ -43,7 +43,7 @@ class B extends A with ChangeNotifier { class Counter with ChangeNotifier { Counter() { if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(); + ChangeNotifier.maybeDispatchObjectCreation(this); } } From 98b3ef02f469605fe2d8c4564a7b91fc0be36a7a Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Fri, 15 Dec 2023 14:13:31 -0800 Subject: [PATCH 095/109] Remove usage of testWidgetsWithLeakTracking. (#140239) --- packages/listen/test/listen_test.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 6bb678a9..aa41dc3b 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; class TestNotifier extends ChangeNotifier { void notify() { @@ -62,7 +61,7 @@ class Counter with ChangeNotifier { } void main() { - testWidgetsWithLeakTracking('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { + testWidgets('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { final TestNotifier test = TestNotifier(); bool callbackDidFinish = false; void foo() { @@ -80,7 +79,7 @@ void main() { test.dispose(); }); - testWidgetsWithLeakTracking('ChangeNotifier', (WidgetTester tester) async { + testWidgets('ChangeNotifier', (WidgetTester tester) async { final List log = []; void listener() { log.add('listener'); From c9a5ebe8fe3fc8e2d9227d00650b53c4eddc3e6f Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 2 Jan 2024 09:56:30 -0800 Subject: [PATCH 096/109] Rename MemoryAllocations to FlutterMemoryAllocations. (#140623) Contributes to https://github.com/flutter/flutter/issues/140622 --- packages/listen/lib/src/listen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7bfc2fb2..27ef987e 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -148,7 +148,7 @@ mixin class ChangeNotifier implements Listenable { bool _debugDisposed = false; /// If true, the event [ObjectCreated] for this instance was dispatched to - /// [MemoryAllocations]. + /// [FlutterMemoryAllocations]. /// /// As [ChangedNotifier] is used as mixin, it does not have constructor, /// so we use [addListener] to dispatch the event. @@ -207,7 +207,7 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; - /// Dispatches event of the [object] creation to [MemoryAllocations.instance]. + /// Dispatches event of the [object] creation to [FlutterMemoryAllocations.instance]. /// /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] /// is false, the method is noop. @@ -231,7 +231,7 @@ mixin class ChangeNotifier implements Listenable { // Tree shaker does not include this method and the class MemoryAllocations // if kFlutterMemoryAllocationsEnabled is false. if (kFlutterMemoryAllocationsEnabled && !object._creationDispatched) { - MemoryAllocations.instance.dispatchObjectCreated( + FlutterMemoryAllocations.instance.dispatchObjectCreated( library: _flutterFoundationLibrary, className: '$ChangeNotifier', object: object, @@ -384,7 +384,7 @@ mixin class ChangeNotifier implements Listenable { return true; }()); if (kFlutterMemoryAllocationsEnabled && _creationDispatched) { - MemoryAllocations.instance.dispatchObjectDisposed(object: this); + FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); } _listeners = _emptyListeners; _count = 0; From 419db80f62fc6793a9ccbd2c746f7030f5d2d329 Mon Sep 17 00:00:00 2001 From: Nate Date: Mon, 26 Feb 2024 16:52:23 -0700 Subject: [PATCH 097/109] Allow `Listenable.merge()` to use any iterable (#143675) This is a very small change that fixes #143664. --- packages/listen/lib/src/listen.dart | 10 +++++----- packages/listen/test/listen_test.dart | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 27ef987e..7cf4c00d 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -62,11 +62,11 @@ abstract class Listenable { /// Return a [Listenable] that triggers when any of the given [Listenable]s /// themselves trigger. /// - /// The list must not be changed after this method has been called. Doing so - /// will lead to memory leaks or exceptions. + /// Once the factory is called, items must not be added or removed from the iterable. + /// Doing so will lead to memory leaks or exceptions. /// - /// The list may contain nulls; they are ignored. - factory Listenable.merge(List listenables) = _MergingListenable; + /// The iterable may contain nulls; they are ignored. + factory Listenable.merge(Iterable listenables) = _MergingListenable; /// Register a closure to be called when the object notifies its listeners. void addListener(VoidCallback listener); @@ -491,7 +491,7 @@ mixin class ChangeNotifier implements Listenable { class _MergingListenable extends Listenable { _MergingListenable(this._children); - final List _children; + final Iterable _children; @override void addListener(VoidCallback listener) { diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index aa41dc3b..55ef811f 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -314,6 +314,21 @@ void main() { log.clear(); }); + test('Merging change notifiers supports any iterable', () { + final TestNotifier source1 = TestNotifier(); + final TestNotifier source2 = TestNotifier(); + final List log = []; + + final Listenable merged = Listenable.merge({source1, source2}); + void listener() => log.add('listener'); + + merged.addListener(listener); + source1.notify(); + source2.notify(); + expect(log, ['listener', 'listener']); + log.clear(); + }); + test('Merging change notifiers ignores null', () { final TestNotifier source1 = TestNotifier(); final TestNotifier source2 = TestNotifier(); From 99f64b9e063599afeb7676b58b65c69b967ddde0 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 2 Jul 2024 09:19:14 -0700 Subject: [PATCH 098/109] Docimports for foundation (#151119) Part of https://github.com/flutter/flutter/issues/150800 Things I couldn't get to work: --- packages/listen/lib/src/listen.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 7cf4c00d..0297902f 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/// @docImport 'package:flutter/animation.dart'; +/// @docImport 'package:flutter/widgets.dart'; +library; + import 'dart:ui' show VoidCallback; import 'package:meta/meta.dart'; @@ -150,7 +154,7 @@ mixin class ChangeNotifier implements Listenable { /// If true, the event [ObjectCreated] for this instance was dispatched to /// [FlutterMemoryAllocations]. /// - /// As [ChangedNotifier] is used as mixin, it does not have constructor, + /// As [ChangeNotifier] is used as mixin, it does not have constructor, /// so we use [addListener] to dispatch the event. bool _creationDispatched = false; From 95ac2da6293dfd6420e9a0a33abf12589302de8d Mon Sep 17 00:00:00 2001 From: Chad Jones Date: Fri, 19 Apr 2013 10:28:48 -0700 Subject: [PATCH 099/109] Initial empty repository From bae7241dfbe9efaf647af49d32fe6463955bc417 Mon Sep 17 00:00:00 2001 From: John McDole Date: Tue, 17 Dec 2024 14:30:30 -0800 Subject: [PATCH 100/109] Trigger Build (#160476) Hello Monorepo. From 7b46eeb951f534200f492d555596fc03e97e5328 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 19 Dec 2024 12:06:21 -0800 Subject: [PATCH 101/109] Auto-format Framework (#160545) This auto-formats all *.dart files in the repository outside of the `engine` subdirectory and enforces that these files stay formatted with a presubmit check. **Reviewers:** Please carefully review all the commits except for the one titled "formatted". The "formatted" commit was auto-generated by running `dev/tools/format.sh -a -f`. The other commits were hand-crafted to prepare the repo for the formatting change. I recommend reviewing the commits one-by-one via the "Commits" tab and avoiding Github's "Files changed" tab as it will likely slow down your browser because of the size of this PR. --------- Co-authored-by: Kate Lovett Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> --- packages/listen/lib/src/listen.dart | 35 +++++++------ packages/listen/test/listen_test.dart | 71 ++++++++++----------------- 2 files changed, 45 insertions(+), 61 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 0297902f..063a295c 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -282,8 +282,10 @@ mixin class ChangeNotifier implements Listenable { if (_count == 0) { _listeners = List.filled(1, null); } else { - final List newListeners = - List.filled(_listeners.length * 2, null); + final List newListeners = List.filled( + _listeners.length * 2, + null, + ); for (int i = 0; i < _count; i++) { newListeners[i] = _listeners[i]; } @@ -436,19 +438,22 @@ mixin class ChangeNotifier implements Listenable { try { _listeners[i]?.call(); } catch (exception, stack) { - FlutterError.reportError(FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'foundation library', - context: ErrorDescription('while dispatching notifications for $runtimeType'), - informationCollector: () => [ - DiagnosticsProperty( - 'The $runtimeType sending notification was', - this, - style: DiagnosticsTreeStyle.errorProperty, - ), - ], - )); + FlutterError.reportError( + FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'foundation library', + context: ErrorDescription('while dispatching notifications for $runtimeType'), + informationCollector: + () => [ + DiagnosticsProperty( + 'The $runtimeType sending notification was', + this, + style: DiagnosticsTreeStyle.errorProperty, + ), + ], + ), + ); } } diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 55ef811f..aaef73e4 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -68,6 +68,7 @@ void main() { test.dispose(); callbackDidFinish = true; } + test.addListener(foo); test.notify(); @@ -229,31 +230,28 @@ void main() { expect(log, ['listener1']); }); - test( - 'If a listener in the middle of the list of listeners removes itself, ' - 'notifyListeners still notifies all listeners', - () { - final TestNotifier source = TestNotifier(); - final List log = []; + test('If a listener in the middle of the list of listeners removes itself, ' + 'notifyListeners still notifies all listeners', () { + final TestNotifier source = TestNotifier(); + final List log = []; - void selfRemovingListener() { - log.add('selfRemovingListener'); - source.removeListener(selfRemovingListener); - } + void selfRemovingListener() { + log.add('selfRemovingListener'); + source.removeListener(selfRemovingListener); + } - void listener1() { - log.add('listener1'); - } + void listener1() { + log.add('listener1'); + } - source.addListener(listener1); - source.addListener(selfRemovingListener); - source.addListener(listener1); + source.addListener(listener1); + source.addListener(selfRemovingListener); + source.addListener(listener1); - source.notify(); + source.notify(); - expect(log, ['listener1', 'selfRemovingListener', 'listener1']); - }, - ); + expect(log, ['listener1', 'selfRemovingListener', 'listener1']); + }); test('If the first listener removes itself, notifyListeners still notify all listeners', () { final TestNotifier source = TestNotifier(); @@ -334,8 +332,7 @@ void main() { final TestNotifier source2 = TestNotifier(); final List log = []; - final Listenable merged = - Listenable.merge([null, source1, null, source2, null]); + final Listenable merged = Listenable.merge([null, source1, null, source2, null]); void listener() { log.add('listener'); } @@ -397,7 +394,7 @@ void main() { test('Can check hasListener on a disposed ChangeNotifier', () { final HasListenersTester source = HasListenersTester(0); - source.addListener(() { }); + source.addListener(() {}); expect(source.testHasListeners, isTrue); FlutterError? error; try { @@ -438,10 +435,7 @@ void main() { expect(listenableUnderTest.toString(), 'Listenable.merge([null])'); listenableUnderTest = Listenable.merge([source1]); - expect( - listenableUnderTest.toString(), - "Listenable.merge([Instance of 'TestNotifier'])", - ); + expect(listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier'])"); listenableUnderTest = Listenable.merge([source1, source2]); expect( @@ -450,10 +444,7 @@ void main() { ); listenableUnderTest = Listenable.merge([null, source2]); - expect( - listenableUnderTest.toString(), - "Listenable.merge([null, Instance of 'TestNotifier'])", - ); + expect(listenableUnderTest.toString(), "Listenable.merge([null, Instance of 'TestNotifier'])"); }); test('Listenable.merge does not leak', () { @@ -463,8 +454,7 @@ void main() { final TestNotifier source2 = TestNotifier(); void fakeListener() {} - final Listenable listenableUnderTest = - Listenable.merge([source1, source2]); + final Listenable listenableUnderTest = Listenable.merge([source1, source2]); expect(source1.isListenedTo, isFalse); expect(source2.isListenedTo, isFalse); listenableUnderTest.addListener(fakeListener); @@ -609,19 +599,8 @@ void main() { test.addListener(listener); } - final List remainingListenerIndexes = [ - 0, - 2, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - ]; - final List expectedLog = - remainingListenerIndexes.map((int i) => 'listener$i').toList(); + final List remainingListenerIndexes = [0, 2, 5, 6, 7, 8, 9, 10, 11]; + final List expectedLog = remainingListenerIndexes.map((int i) => 'listener$i').toList(); test.notify(); expect(log, expectedLog); From 927822bac8eef44df9c9d43af72a4e5a6821dc0e Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 25 Feb 2025 15:18:53 -0800 Subject: [PATCH 102/109] Clean up leak tracker instrumentation tech debt. (#164070) Fixes https://github.com/flutter/flutter/issues/137435 Tests are not needed because it is refactoring. Requested [exempt](https://discord.com/channels/608014603317936148/608018585025118217/1343801945982505001). @goderbauer , if looks good, can you merge it please, to avoid conflicts, as there are many files here? --- packages/listen/lib/src/listen.dart | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 063a295c..026c9af3 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -11,6 +11,7 @@ import 'dart:ui' show VoidCallback; import 'package:meta/meta.dart'; import 'assertions.dart'; +import 'debug.dart'; import 'diagnostics.dart'; import 'memory_allocations.dart'; @@ -100,8 +101,6 @@ abstract class ValueListenable extends Listenable { T get value; } -const String _flutterFoundationLibrary = 'package:flutter/foundation.dart'; - /// A class that can be extended or mixed in that provides a change notification /// API using [VoidCallback] for notifications. /// @@ -156,7 +155,7 @@ mixin class ChangeNotifier implements Listenable { /// /// As [ChangeNotifier] is used as mixin, it does not have constructor, /// so we use [addListener] to dispatch the event. - bool _creationDispatched = false; + bool _debugCreationDispatched = false; /// Used by subclasses to assert that the [ChangeNotifier] has not yet been /// disposed. @@ -232,16 +231,13 @@ mixin class ChangeNotifier implements Listenable { /// so that the method is tree-shaken away when the flag is false. @protected static void maybeDispatchObjectCreation(ChangeNotifier object) { - // Tree shaker does not include this method and the class MemoryAllocations - // if kFlutterMemoryAllocationsEnabled is false. - if (kFlutterMemoryAllocationsEnabled && !object._creationDispatched) { - FlutterMemoryAllocations.instance.dispatchObjectCreated( - library: _flutterFoundationLibrary, - className: '$ChangeNotifier', - object: object, - ); - object._creationDispatched = true; - } + assert(() { + if (!object._debugCreationDispatched) { + debugMaybeDispatchCreated('foundation', 'ChangeNotifier', object); + object._debugCreationDispatched = true; + } + return true; + }()); } /// Register a closure to be called when the object changes. @@ -387,11 +383,11 @@ mixin class ChangeNotifier implements Listenable { ); assert(() { _debugDisposed = true; + if (_debugCreationDispatched) { + assert(debugMaybeDispatchDisposed(this)); + } return true; }()); - if (kFlutterMemoryAllocationsEnabled && _creationDispatched) { - FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this); - } _listeners = _emptyListeners; _count = 0; } From 55ed68cd8b73d3eb09d9e413c813425f8a4c4a57 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 23 Jun 2025 19:10:55 -0500 Subject: [PATCH 103/109] Flutter test cleanup (#170891) Mostly related to error tracking. --- packages/listen/test/listen_test.dart | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index aaef73e4..dadad354 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -382,28 +382,16 @@ void main() { test('Can remove listener on a disposed ChangeNotifier', () { final TestNotifier source = TestNotifier(); - FlutterError? error; - try { - source.dispose(); - source.removeListener(() {}); - } on FlutterError catch (e) { - error = e; - } - expect(error, isNull); + source.dispose(); + source.removeListener(() {}); }); test('Can check hasListener on a disposed ChangeNotifier', () { final HasListenersTester source = HasListenersTester(0); source.addListener(() {}); expect(source.testHasListeners, isTrue); - FlutterError? error; - try { - source.dispose(); - expect(source.testHasListeners, isFalse); - } on FlutterError catch (e) { - error = e; - } - expect(error, isNull); + source.dispose(); + expect(source.testHasListeners, isFalse); }); test('Value notifier', () { From 0a91f20422d9a44377a4ed6163b8d14a78e86a32 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Mon, 7 Jul 2025 12:58:32 -0500 Subject: [PATCH 104/109] Bump Dart to 3.8 and reformat (#171703) Bumps the Dart version to 3.8 across the repo (excluding engine/src/flutter/third_party) and applies formatting updates from Dart 3.8. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/listen/lib/src/listen.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 026c9af3..9b0e663f 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -440,14 +440,13 @@ mixin class ChangeNotifier implements Listenable { stack: stack, library: 'foundation library', context: ErrorDescription('while dispatching notifications for $runtimeType'), - informationCollector: - () => [ - DiagnosticsProperty( - 'The $runtimeType sending notification was', - this, - style: DiagnosticsTreeStyle.errorProperty, - ), - ], + informationCollector: () => [ + DiagnosticsProperty( + 'The $runtimeType sending notification was', + this, + style: DiagnosticsTreeStyle.errorProperty, + ), + ], ), ); } From 8386a09f127fea0a8f1545a07e8e7e8b94145e11 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Tue, 25 Nov 2025 19:10:39 -0600 Subject: [PATCH 105/109] Modernize framework lints (#179089) WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/listen/lib/src/listen.dart | 27 +++---- packages/listen/test/listen_test.dart | 108 +++++++++++++------------- 2 files changed, 66 insertions(+), 69 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 9b0e663f..754010c5 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -278,11 +278,8 @@ mixin class ChangeNotifier implements Listenable { if (_count == 0) { _listeners = List.filled(1, null); } else { - final List newListeners = List.filled( - _listeners.length * 2, - null, - ); - for (int i = 0; i < _count; i++) { + final newListeners = List.filled(_listeners.length * 2, null); + for (var i = 0; i < _count; i++) { newListeners[i] = _listeners[i]; } _listeners = newListeners; @@ -299,15 +296,15 @@ mixin class ChangeNotifier implements Listenable { // of our list. _count -= 1; if (_count * 2 <= _listeners.length) { - final List newListeners = List.filled(_count, null); + final newListeners = List.filled(_count, null); // Listeners before the index are at the same place. - for (int i = 0; i < index; i++) { + for (var i = 0; i < index; i++) { newListeners[i] = _listeners[i]; } // Listeners after the index move towards the start of the list. - for (int i = index; i < _count; i++) { + for (var i = index; i < _count; i++) { newListeners[i] = _listeners[i + 1]; } @@ -316,7 +313,7 @@ mixin class ChangeNotifier implements Listenable { // When there are more listeners than half the length of the list, we only // shift our listeners, so that we avoid to reallocate memory for the // whole list. - for (int i = index; i < _count; i++) { + for (var i = index; i < _count; i++) { _listeners[i] = _listeners[i + 1]; } _listeners[_count] = null; @@ -343,7 +340,7 @@ mixin class ChangeNotifier implements Listenable { // overlays, it is common that the owner of this instance would be disposed a // frame earlier than the listeners. Allowing calls to this method after it // is disposed makes it easier for listeners to properly clean up. - for (int i = 0; i < _count; i++) { + for (var i = 0; i < _count; i++) { final VoidCallback? listenerAtIndex = _listeners[i]; if (listenerAtIndex == listener) { if (_notificationCallStackDepth > 0) { @@ -430,7 +427,7 @@ mixin class ChangeNotifier implements Listenable { _notificationCallStackDepth++; final int end = _count; - for (int i = 0; i < end; i++) { + for (var i = 0; i < end; i++) { try { _listeners[i]?.call(); } catch (exception, stack) { @@ -460,10 +457,10 @@ mixin class ChangeNotifier implements Listenable { if (newLength * 2 <= _listeners.length) { // As in _removeAt, we only shrink the list when the real number of // listeners is half the length of our list. - final List newListeners = List.filled(newLength, null); + final newListeners = List.filled(newLength, null); - int newIndex = 0; - for (int i = 0; i < _count; i++) { + var newIndex = 0; + for (var i = 0; i < _count; i++) { final VoidCallback? listener = _listeners[i]; if (listener != null) { newListeners[newIndex++] = listener; @@ -473,7 +470,7 @@ mixin class ChangeNotifier implements Listenable { _listeners = newListeners; } else { // Otherwise we put all the null references at the end. - for (int i = 0; i < newLength; i += 1) { + for (var i = 0; i < newLength; i += 1) { if (_listeners[i] == null) { // We swap this item with the next not null item. int swapIndex = i + 1; diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index dadad354..1b7bfc5d 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -62,8 +62,8 @@ class Counter with ChangeNotifier { void main() { testWidgets('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { - final TestNotifier test = TestNotifier(); - bool callbackDidFinish = false; + final test = TestNotifier(); + var callbackDidFinish = false; void foo() { test.dispose(); callbackDidFinish = true; @@ -73,7 +73,7 @@ void main() { test.notify(); - final AssertionError error = tester.takeException() as AssertionError; + final error = tester.takeException() as AssertionError; expect(error.toString().contains('dispose()'), isTrue); // Make sure it crashes during dispose call. expect(callbackDidFinish, isFalse); @@ -81,7 +81,7 @@ void main() { }); testWidgets('ChangeNotifier', (WidgetTester tester) async { - final List log = []; + final log = []; void listener() { log.add('listener'); } @@ -99,7 +99,7 @@ void main() { throw ArgumentError(); } - final TestNotifier test = TestNotifier(); + final test = TestNotifier(); test.addListener(listener); test.addListener(listener); @@ -166,8 +166,8 @@ void main() { }); test('ChangeNotifier with mutating listener', () { - final TestNotifier test = TestNotifier(); - final List log = []; + final test = TestNotifier(); + final log = []; void listener1() { log.add('listener1'); @@ -205,8 +205,8 @@ void main() { }); test('During notifyListeners, a listener was added and removed immediately', () { - final TestNotifier source = TestNotifier(); - final List log = []; + final source = TestNotifier(); + final log = []; void listener3() { log.add('listener3'); @@ -232,8 +232,8 @@ void main() { test('If a listener in the middle of the list of listeners removes itself, ' 'notifyListeners still notifies all listeners', () { - final TestNotifier source = TestNotifier(); - final List log = []; + final source = TestNotifier(); + final log = []; void selfRemovingListener() { log.add('selfRemovingListener'); @@ -254,8 +254,8 @@ void main() { }); test('If the first listener removes itself, notifyListeners still notify all listeners', () { - final TestNotifier source = TestNotifier(); - final List log = []; + final source = TestNotifier(); + final log = []; void selfRemovingListener() { log.add('selfRemovingListener'); @@ -275,12 +275,12 @@ void main() { }); test('Merging change notifiers', () { - final TestNotifier source1 = TestNotifier(); - final TestNotifier source2 = TestNotifier(); - final TestNotifier source3 = TestNotifier(); - final List log = []; + final source1 = TestNotifier(); + final source2 = TestNotifier(); + final source3 = TestNotifier(); + final log = []; - final Listenable merged = Listenable.merge([source1, source2]); + final merged = Listenable.merge([source1, source2]); void listener1() { log.add('listener1'); } @@ -313,11 +313,11 @@ void main() { }); test('Merging change notifiers supports any iterable', () { - final TestNotifier source1 = TestNotifier(); - final TestNotifier source2 = TestNotifier(); - final List log = []; + final source1 = TestNotifier(); + final source2 = TestNotifier(); + final log = []; - final Listenable merged = Listenable.merge({source1, source2}); + final merged = Listenable.merge({source1, source2}); void listener() => log.add('listener'); merged.addListener(listener); @@ -328,11 +328,11 @@ void main() { }); test('Merging change notifiers ignores null', () { - final TestNotifier source1 = TestNotifier(); - final TestNotifier source2 = TestNotifier(); - final List log = []; + final source1 = TestNotifier(); + final source2 = TestNotifier(); + final log = []; - final Listenable merged = Listenable.merge([null, source1, null, source2, null]); + final merged = Listenable.merge([null, source1, null, source2, null]); void listener() { log.add('listener'); } @@ -345,11 +345,11 @@ void main() { }); test('Can remove from merged notifier', () { - final TestNotifier source1 = TestNotifier(); - final TestNotifier source2 = TestNotifier(); - final List log = []; + final source1 = TestNotifier(); + final source2 = TestNotifier(); + final log = []; - final Listenable merged = Listenable.merge([source1, source2]); + final merged = Listenable.merge([source1, source2]); void listener() { log.add('listener'); } @@ -367,7 +367,7 @@ void main() { }); test('Cannot use a disposed ChangeNotifier except for remove listener', () { - final TestNotifier source = TestNotifier(); + final source = TestNotifier(); source.dispose(); expect(() { source.addListener(() {}); @@ -381,13 +381,13 @@ void main() { }); test('Can remove listener on a disposed ChangeNotifier', () { - final TestNotifier source = TestNotifier(); + final source = TestNotifier(); source.dispose(); source.removeListener(() {}); }); test('Can check hasListener on a disposed ChangeNotifier', () { - final HasListenersTester source = HasListenersTester(0); + final source = HasListenersTester(0); source.addListener(() {}); expect(source.testHasListeners, isTrue); source.dispose(); @@ -395,9 +395,9 @@ void main() { }); test('Value notifier', () { - final ValueNotifier notifier = ValueNotifier(2.0); + final notifier = ValueNotifier(2.0); - final List log = []; + final log = []; void listener() { log.add(notifier.value); } @@ -413,10 +413,10 @@ void main() { }); test('Listenable.merge toString', () { - final TestNotifier source1 = TestNotifier(); - final TestNotifier source2 = TestNotifier(); + final source1 = TestNotifier(); + final source2 = TestNotifier(); - Listenable listenableUnderTest = Listenable.merge([]); + var listenableUnderTest = Listenable.merge([]); expect(listenableUnderTest.toString(), 'Listenable.merge([])'); listenableUnderTest = Listenable.merge([null]); @@ -438,11 +438,11 @@ void main() { test('Listenable.merge does not leak', () { // Regression test for https://github.com/flutter/flutter/issues/25163. - final TestNotifier source1 = TestNotifier(); - final TestNotifier source2 = TestNotifier(); + final source1 = TestNotifier(); + final source2 = TestNotifier(); void fakeListener() {} - final Listenable listenableUnderTest = Listenable.merge([source1, source2]); + final listenableUnderTest = Listenable.merge([source1, source2]); expect(source1.isListenedTo, isFalse); expect(source2.isListenedTo, isFalse); listenableUnderTest.addListener(fakeListener); @@ -455,7 +455,7 @@ void main() { }); test('hasListeners', () { - final HasListenersTester notifier = HasListenersTester(true); + final notifier = HasListenersTester(true); expect(notifier.testHasListeners, isFalse); void test1() {} void test2() {} @@ -479,8 +479,8 @@ void main() { test('ChangeNotifier as a mixin', () { // We document that this is a valid way to use this class. - final B b = B(); - int notifications = 0; + final b = B(); + var notifications = 0; b.addListener(() { notifications += 1; }); @@ -492,7 +492,7 @@ void main() { }); test('Throws FlutterError when disposed and called', () { - final TestNotifier testNotifier = TestNotifier(); + final testNotifier = TestNotifier(); testNotifier.dispose(); FlutterError? error; try { @@ -514,7 +514,7 @@ void main() { }); test('Calling debugAssertNotDisposed works as intended', () { - final TestNotifier testNotifier = TestNotifier(); + final testNotifier = TestNotifier(); expect(ChangeNotifier.debugAssertNotDisposed(testNotifier), isTrue); testNotifier.dispose(); FlutterError? error; @@ -537,8 +537,8 @@ void main() { }); test('notifyListener can be called recursively', () { - final Counter counter = Counter(); - final List log = []; + final counter = Counter(); + final log = []; void listener1() { log.add('listener1'); @@ -562,9 +562,9 @@ void main() { }); test('Remove Listeners while notifying on a list which will not resize', () { - final TestNotifier test = TestNotifier(); - final List log = []; - final List listeners = []; + final test = TestNotifier(); + final log = []; + final listeners = []; void autoRemove() { // We remove 4 listeners. @@ -578,7 +578,7 @@ void main() { test.addListener(autoRemove); // We add 12 more listeners. - for (int i = 0; i < 12; i++) { + for (var i = 0; i < 12; i++) { void listener() { log.add('listener$i'); } @@ -587,7 +587,7 @@ void main() { test.addListener(listener); } - final List remainingListenerIndexes = [0, 2, 5, 6, 7, 8, 9, 10, 11]; + final remainingListenerIndexes = [0, 2, 5, 6, 7, 8, 9, 10, 11]; final List expectedLog = remainingListenerIndexes.map((int i) => 'listener$i').toList(); test.notify(); @@ -599,7 +599,7 @@ void main() { expect(log, expectedLog); // We remove all other listeners. - for (int i = 0; i < remainingListenerIndexes.length; i++) { + for (var i = 0; i < remainingListenerIndexes.length; i++) { test.removeListener(listeners[remainingListenerIndexes[i]]); } From 65e165d923d33ff7aa0076bb2df0ea488f584c01 Mon Sep 17 00:00:00 2001 From: Mohellebi abdessalem <116356835+AbdeMohlbi@users.noreply.github.com> Date: Tue, 6 Jan 2026 09:28:35 +0100 Subject: [PATCH 106/109] Improve documentation about ValueNotifier's behavior (#179870) fixes #142418 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- packages/listen/lib/src/listen.dart | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index 754010c5..f5ab99c0 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -516,22 +516,26 @@ class _MergingListenable extends Listenable { /// A [ChangeNotifier] that holds a single value. /// -/// When [value] is replaced with something that is not equal to the old -/// value as evaluated by the equality operator ==, this class notifies its +/// When [value] is replaced with a new value that is **not equal** to the old +/// value as evaluated by the equality operator (`==`), this class notifies its /// listeners. /// /// ## Limitations /// -/// Because this class only notifies listeners when the [value]'s _identity_ -/// changes, listeners will not be notified when mutable state within the -/// value itself changes. +/// Notifications are triggered based on **equality (`==`)**, not on mutations +/// within the value itself. As a result, changes to mutable objects that do not +/// affect their equality will not cause listeners to be notified. /// -/// For example, a `ValueNotifier>` will not notify its listeners -/// when the _contents_ of the list are changed. +/// For example, a `ValueNotifier>` will not notify listeners when +/// the contents of the existing list are modified in-place; it only notifies +/// when a new value is assigned to the `value` property (i.e. `value = newValue`), +/// where equality is determined by `==`. /// -/// As a result, this class is best used with only immutable data types. +/// Because of this behavior, [ValueNotifier] is best used with immutable data +/// types. /// -/// For mutable data types, consider extending [ChangeNotifier] directly. +/// For mutable data types, consider extending [ChangeNotifier] directly and +/// calling [notifyListeners] manually when changes occur. class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { From 6cd45de98ac7cff8cc22055a7a7a2b8a4e1d7cc8 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:04:07 -0800 Subject: [PATCH 107/109] Update doc in foundation to match the style guide (#181972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit doc should start with one sentence ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- packages/listen/lib/src/listen.dart | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index f5ab99c0..e64570de 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -60,8 +60,8 @@ export 'dart:ui' show VoidCallback; /// notifications whenever any of a list of other [Listenable]s trigger their /// notifications. abstract class Listenable { - /// Abstract const constructor. This constructor enables subclasses to provide - /// const constructors so that they can be used in const expressions. + /// This constructor enables subclasses to provide const constructors so that + /// they can be used in const expressions. const Listenable(); /// Return a [Listenable] that triggers when any of the given [Listenable]s @@ -92,12 +92,14 @@ abstract class Listenable { /// rebuild whenever a [ValueListenable] object triggers its notifications, /// providing the builder with the value of the object. abstract class ValueListenable extends Listenable { - /// Abstract const constructor. This constructor enables subclasses to provide - /// const constructors so that they can be used in const expressions. + /// This constructor enables subclasses to provide const constructors so that + /// they can be used in const expressions. const ValueListenable(); - /// The current value of the object. When the value changes, the callbacks - /// registered with [addListener] will be invoked. + /// The current value of the object. + /// + /// When the value changes, the callbacks registered with [addListener] will be + /// invoked. T get value; } @@ -360,9 +362,10 @@ mixin class ChangeNotifier implements Listenable { } } - /// Discards any resources used by the object. After this is called, the - /// object is not in a usable state and should be discarded (calls to - /// [addListener] will throw after the object is disposed). + /// Discards any resources used by the object. + /// + /// After this is called, the object is not in a usable state and should be + /// discarded (calls to [addListener] will throw after the object is disposed). /// /// This method should only be called by the object's owner. /// From a2354b4a0446161d4b38c3fe35bec158f01d10bb Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Thu, 11 Jun 2026 15:02:38 -0700 Subject: [PATCH 108/109] Adds listenable package --- .github/labeler.yml | 6 + README.md | 1 + packages/listen/.gitignore | 9 + packages/listen/AUTHORS | 6 + packages/listen/CHANGELOG.md | 3 + packages/listen/LICENSE | 25 +++ packages/listen/README.md | 110 ++++++++++ packages/listen/example/lib/counter.dart | 35 ++++ .../listen/example/lib/list_notifier.dart | 43 ++++ .../listen/example/lib/listenable_merge.dart | 24 +++ packages/listen/example/lib/main.dart | 20 ++ .../listen/example/lib/readme_excerpts.dart | 91 +++++++++ .../listen/example/lib/value_notifier.dart | 21 ++ packages/listen/example/pubspec.yaml | 14 ++ .../listen/example/test/example_test.dart | 32 +++ packages/listen/lib/listen.dart | 8 + packages/listen/lib/src/listen.dart | 190 +++++++++--------- packages/listen/pubspec.yaml | 21 ++ packages/listen/test/listen_test.dart | 118 ++++++----- 19 files changed, 623 insertions(+), 154 deletions(-) create mode 100644 packages/listen/.gitignore create mode 100644 packages/listen/AUTHORS create mode 100644 packages/listen/CHANGELOG.md create mode 100644 packages/listen/LICENSE create mode 100644 packages/listen/README.md create mode 100644 packages/listen/example/lib/counter.dart create mode 100644 packages/listen/example/lib/list_notifier.dart create mode 100644 packages/listen/example/lib/listenable_merge.dart create mode 100644 packages/listen/example/lib/main.dart create mode 100644 packages/listen/example/lib/readme_excerpts.dart create mode 100644 packages/listen/example/lib/value_notifier.dart create mode 100644 packages/listen/example/pubspec.yaml create mode 100644 packages/listen/example/test/example_test.dart create mode 100644 packages/listen/lib/listen.dart create mode 100644 packages/listen/pubspec.yaml diff --git a/.github/labeler.yml b/.github/labeler.yml index 2bfcbae8..ee179ac2 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -27,8 +27,14 @@ - any-glob-to-any-file: - packages/vector_math/**/* +'p: listen': + - changed-files: + - any-glob-to-any-file: + - packages/listen/**/* + 'triage-framework': - changed-files: - any-glob-to-any-file: - packages/vector_math/**/* - packages/flutter_hook_config/**/* + - packages/listen/**/* diff --git a/README.md b/README.md index 5d54b042..5295bd67 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,4 @@ These are the packages hosted in this repository: | [mustache\_template](./third_party/packages/mustache_template/) | [![pub package](https://img.shields.io/pub/v/mustache_template.svg)](https://pub.dev/packages/mustache_template) | [![pub points](https://img.shields.io/pub/points/mustache_template)](https://pub.dev/packages/mustache_template/score) | [![downloads](https://img.shields.io/pub/dm/mustache_template)](https://pub.dev/packages/mustache_template/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20mustache_template?label=)](https://github.com/flutter/flutter/labels/p%3A%20mustache_template) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20mustache_template?label=)](https://github.com/flutter/core-packages/labels/p%3A%20mustache_template) | | [standard\_message\_codec](./packages/standard_message_codec/) | [![pub package](https://img.shields.io/pub/v/standard_message_codec.svg)](https://pub.dev/packages/standard_message_codec) | [![pub points](https://img.shields.io/pub/points/standard_message_codec)](https://pub.dev/packages/standard_message_codec/score) | [![downloads](https://img.shields.io/pub/dm/standard_message_codec)](https://pub.dev/packages/standard_message_codec/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20standard_message_codec?label=)](https://github.com/flutter/flutter/labels/p%3A%20standard_message_codec) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20standard_message_codec?label=)](https://github.com/flutter/core-packages/labels/p%3A%20standard_message_codec) | | [vector\_math](./packages/vector_math/) | [![pub package](https://img.shields.io/pub/v/vector_math.svg)](https://pub.dev/packages/vector_math) | [![pub points](https://img.shields.io/pub/points/vector_math)](https://pub.dev/packages/vector_math/score) | [![downloads](https://img.shields.io/pub/dm/vector_math)](https://pub.dev/packages/vector_math/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20vector_math?label=)](https://github.com/flutter/flutter/labels/p%3A%20vector_math) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20vector_math?label=)](https://github.com/flutter/core-packages/labels/p%3A%20vector_math) | +| [listen](./packages/listen/) | [![pub package](https://img.shields.io/pub/v/listen.svg)](https://pub.dev/packages/listen) | [![pub points](https://img.shields.io/pub/points/listen)](https://pub.dev/packages/listen/score) | [![downloads](https://img.shields.io/pub/dm/listen)](https://pub.dev/packages/listen/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20listen?label=)](https://github.com/flutter/flutter/labels/p%3A%20listen) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20listen?label=)](https://github.com/flutter/core-packages/labels/p%3A%20listen) | diff --git a/packages/listen/.gitignore b/packages/listen/.gitignore new file mode 100644 index 00000000..fd4a666c --- /dev/null +++ b/packages/listen/.gitignore @@ -0,0 +1,9 @@ +.children +.project +.DS_Store +packages +pubspec.lock +.pub +.packages +.dart_tool +.idea diff --git a/packages/listen/AUTHORS b/packages/listen/AUTHORS new file mode 100644 index 00000000..e8063a8c --- /dev/null +++ b/packages/listen/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/packages/listen/CHANGELOG.md b/packages/listen/CHANGELOG.md new file mode 100644 index 00000000..a0712a79 --- /dev/null +++ b/packages/listen/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +- Initial version. diff --git a/packages/listen/LICENSE b/packages/listen/LICENSE new file mode 100644 index 00000000..29b709da --- /dev/null +++ b/packages/listen/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/listen/README.md b/packages/listen/README.md new file mode 100644 index 00000000..ec6aa0c3 --- /dev/null +++ b/packages/listen/README.md @@ -0,0 +1,110 @@ + + +# listen + +A lightweight synchronous listener registry for pure Dart applications. + +## Getting started + +Add this package to your `pubspec.yaml` dependencies. + +## Usage + +Import the package in your Dart code: + + +```dart +import 'package:listen/listen.dart'; + +``` + +### Using ValueNotifier + +`ValueNotifier` wraps a single value and notifies listeners whenever the value changes: + + +```dart +void valueNotifierExample() { + final counter = ValueNotifier(0); + + counter.addListener(() { + print('Value changed: ${counter.value}'); + }); + + counter.value = 5; // Prints: Value changed: 5 + counter.value = 10; // Prints: Value changed: 10 + counter.value = 10; // Does not print because the value is == 10 + + counter.dispose(); +} + +``` + +### Using ChangeNotifier + +Extend or mix in `ChangeNotifier` to manage state and notify listeners manually: + + +```dart +/// A [ChangeNotifier] subclass that encapsulates a list of items and notifies +/// listeners whenever items are added or removed. +class ItemListNotifier extends ChangeNotifier { + final List _items = []; + + /// The unmodifiable list of current items. + List get items => List.unmodifiable(_items); + + /// Adds an [item] to the list and notifies listeners. + void addItem(String item) { + _items.add(item); + notifyListeners(); + } + + /// Removes an [item] from the list and notifies listeners if it was present. + void removeItem(String item) { + if (_items.remove(item)) { + notifyListeners(); + } + } +} + +/// Demonstrates [ChangeNotifier] usage for README. +void changeNotifierExample() { + final listNotifier = ItemListNotifier(); + + listNotifier.addListener(() { + print('Current items: ${listNotifier.items}'); + }); + + listNotifier.addItem('Apple'); // Prints: Current items: [Apple] + listNotifier.addItem('Banana'); // Prints: Current items: [Apple, Banana] + listNotifier.removeItem('Apple'); // Prints: Current items: [Banana] + + listNotifier.dispose(); +} + +``` + +### Merging listenables + +Use `Listenable.merge` to listen to multiple objects simultaneously: + + +```dart +void mergeExample() { + final first = ValueNotifier('Hello'); + final second = ValueNotifier('World'); + + final merged = Listenable.merge([first, second]); + + merged.addListener(() { + print('Merged listenable triggered: ${first.value} ${second.value}'); + }); + + first.value = 'Hi'; // Prints: Merged listenable triggered: Hi World + second.value = 'Dart'; // Prints: Merged listenable triggered: Hi Dart + + first.dispose(); + second.dispose(); +} +``` diff --git a/packages/listen/example/lib/counter.dart b/packages/listen/example/lib/counter.dart new file mode 100644 index 00000000..ca1e572b --- /dev/null +++ b/packages/listen/example/lib/counter.dart @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'package:listen/listen.dart'; + +/// A simple counter that extends [ChangeNotifier] to notify listeners +/// whenever its value changes. +class Counter extends ChangeNotifier { + int _count = 0; + + /// The current count value. + int get count => _count; + + /// Increments the count by one and notifies listeners. + void increment() { + _count++; + notifyListeners(); + } +} + +void main() { + final counter = Counter(); + + counter.addListener(() { + print('Counter value changed to: ${counter.count}'); + }); + + counter.increment(); // Prints: Counter value changed to: 1 + counter.increment(); // Prints: Counter value changed to: 2 + + counter.dispose(); +} diff --git a/packages/listen/example/lib/list_notifier.dart b/packages/listen/example/lib/list_notifier.dart new file mode 100644 index 00000000..c514c8dd --- /dev/null +++ b/packages/listen/example/lib/list_notifier.dart @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'package:listen/listen.dart'; + +/// A [ChangeNotifier] subclass that encapsulates a list of items and notifies +/// listeners whenever items are added or removed. +class ItemListNotifier extends ChangeNotifier { + final List _items = []; + + /// The unmodifiable list of current items. + List get items => List.unmodifiable(_items); + + /// Adds an [item] to the list and notifies listeners. + void addItem(String item) { + _items.add(item); + notifyListeners(); + } + + /// Removes an [item] from the list and notifies listeners if it was present. + void removeItem(String item) { + if (_items.remove(item)) { + notifyListeners(); + } + } +} + +void main() { + final listNotifier = ItemListNotifier(); + + listNotifier.addListener(() { + print('Current items: ${listNotifier.items}'); + }); + + listNotifier.addItem('Apple'); // Prints: Current items: [Apple] + listNotifier.addItem('Banana'); // Prints: Current items: [Apple, Banana] + listNotifier.removeItem('Apple'); // Prints: Current items: [Banana] + + listNotifier.dispose(); +} diff --git a/packages/listen/example/lib/listenable_merge.dart b/packages/listen/example/lib/listenable_merge.dart new file mode 100644 index 00000000..00948411 --- /dev/null +++ b/packages/listen/example/lib/listenable_merge.dart @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'package:listen/listen.dart'; + +void main() { + final first = ValueNotifier('Hello'); + final second = ValueNotifier('World'); + + final merged = Listenable.merge([first, second]); + + merged.addListener(() { + print('Merged listenable triggered: ${first.value} ${second.value}'); + }); + + first.value = 'Hi'; // Prints: Merged listenable triggered: Hi World + second.value = 'Dart'; // Prints: Merged listenable triggered: Hi Dart + + first.dispose(); + second.dispose(); +} diff --git a/packages/listen/example/lib/main.dart b/packages/listen/example/lib/main.dart new file mode 100644 index 00000000..a335d532 --- /dev/null +++ b/packages/listen/example/lib/main.dart @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'package:listen/listen.dart'; + +void main() { + final counter = ValueNotifier(0); + + counter.addListener(() { + print('Counter changed to: ${counter.value}'); + }); + + counter.value = 1; // Prints: Counter changed to: 1 + counter.value = 2; // Prints: Counter changed to: 2 + + counter.dispose(); +} diff --git a/packages/listen/example/lib/readme_excerpts.dart b/packages/listen/example/lib/readme_excerpts.dart new file mode 100644 index 00000000..c8dee104 --- /dev/null +++ b/packages/listen/example/lib/readme_excerpts.dart @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file exists solely to host compiled excerpts for README.md, and is not +// intended for use as an actual example application. + +// ignore_for_file: avoid_print + +// #docregion Import +import 'package:listen/listen.dart'; + +// #enddocregion Import + +/// Demonstrates [ValueNotifier] usage for README. +// #docregion ValueNotifier +void valueNotifierExample() { + final counter = ValueNotifier(0); + + counter.addListener(() { + print('Value changed: ${counter.value}'); + }); + + counter.value = 5; // Prints: Value changed: 5 + counter.value = 10; // Prints: Value changed: 10 + counter.value = 10; // Does not print because the value is == 10 + + counter.dispose(); +} + +// #enddocregion ValueNotifier + +// #docregion ChangeNotifier +/// A [ChangeNotifier] subclass that encapsulates a list of items and notifies +/// listeners whenever items are added or removed. +class ItemListNotifier extends ChangeNotifier { + final List _items = []; + + /// The unmodifiable list of current items. + List get items => List.unmodifiable(_items); + + /// Adds an [item] to the list and notifies listeners. + void addItem(String item) { + _items.add(item); + notifyListeners(); + } + + /// Removes an [item] from the list and notifies listeners if it was present. + void removeItem(String item) { + if (_items.remove(item)) { + notifyListeners(); + } + } +} + +/// Demonstrates [ChangeNotifier] usage for README. +void changeNotifierExample() { + final listNotifier = ItemListNotifier(); + + listNotifier.addListener(() { + print('Current items: ${listNotifier.items}'); + }); + + listNotifier.addItem('Apple'); // Prints: Current items: [Apple] + listNotifier.addItem('Banana'); // Prints: Current items: [Apple, Banana] + listNotifier.removeItem('Apple'); // Prints: Current items: [Banana] + + listNotifier.dispose(); +} + +// #enddocregion ChangeNotifier + +/// Demonstrates [Listenable.merge] usage for README. +// #docregion Merge +void mergeExample() { + final first = ValueNotifier('Hello'); + final second = ValueNotifier('World'); + + final merged = Listenable.merge([first, second]); + + merged.addListener(() { + print('Merged listenable triggered: ${first.value} ${second.value}'); + }); + + first.value = 'Hi'; // Prints: Merged listenable triggered: Hi World + second.value = 'Dart'; // Prints: Merged listenable triggered: Hi Dart + + first.dispose(); + second.dispose(); +} +// #enddocregion Merge diff --git a/packages/listen/example/lib/value_notifier.dart b/packages/listen/example/lib/value_notifier.dart new file mode 100644 index 00000000..8b39bebc --- /dev/null +++ b/packages/listen/example/lib/value_notifier.dart @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: avoid_print + +import 'package:listen/listen.dart'; + +void main() { + final counter = ValueNotifier(0); + + counter.addListener(() { + print('Value changed: ${counter.value}'); + }); + + counter.value = 5; // Prints: Value changed: 5 + counter.value = 10; // Prints: Value changed: 10 + counter.value = 10; // Does not print because the value is == 10 + + counter.dispose(); +} diff --git a/packages/listen/example/pubspec.yaml b/packages/listen/example/pubspec.yaml new file mode 100644 index 00000000..d0180981 --- /dev/null +++ b/packages/listen/example/pubspec.yaml @@ -0,0 +1,14 @@ +name: listen_example +description: Examples for the listen package. +publish_to: none +version: 0.0.1 + +environment: + sdk: ^3.10.0 + +dependencies: + listen: + path: ../ + +dev_dependencies: + test: ^1.25.9 diff --git a/packages/listen/example/test/example_test.dart b/packages/listen/example/test/example_test.dart new file mode 100644 index 00000000..87af4ac7 --- /dev/null +++ b/packages/listen/example/test/example_test.dart @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:listen_example/counter.dart' as counter; +import 'package:listen_example/list_notifier.dart' as list_notifier; +import 'package:listen_example/listenable_merge.dart' as listenable_merge; +import 'package:listen_example/main.dart' as basic_main; +import 'package:listen_example/value_notifier.dart' as value_notifier; +import 'package:test/test.dart'; + +void main() { + test('counter example runs without error', () { + expect(counter.main, returnsNormally); + }); + + test('list notifier example runs without error', () { + expect(list_notifier.main, returnsNormally); + }); + + test('listenable merge example runs without error', () { + expect(listenable_merge.main, returnsNormally); + }); + + test('value notifier example runs without error', () { + expect(value_notifier.main, returnsNormally); + }); + + test('basic main example runs without error', () { + expect(basic_main.main, returnsNormally); + }); +} diff --git a/packages/listen/lib/listen.dart b/packages/listen/lib/listen.dart new file mode 100644 index 00000000..99bd9d9f --- /dev/null +++ b/packages/listen/lib/listen.dart @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A lightweight synchronous listener registry. +library; + +export 'src/listen.dart'; diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index e64570de..e5305b56 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -1,37 +1,40 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// @docImport 'package:flutter/animation.dart'; -/// @docImport 'package:flutter/widgets.dart'; -library; - -import 'dart:ui' show VoidCallback; +// TODO(chunhtai): remove this once https://github.com/dart-lang/sdk/pull/63646 is landed. +// ignore_for_file: doc_directive_unknown import 'package:meta/meta.dart'; -import 'assertions.dart'; -import 'debug.dart'; -import 'diagnostics.dart'; -import 'memory_allocations.dart'; +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); + +/// Signature of callbacks that are called when an error is thrown by a listener. +typedef ErrorCallback = void Function(String message, StackTrace? stackTrace); + +/// Signature of callbacks that are called when an object is created. +/// +/// The `className` is the name of the class, and `object` is the object that was created. +typedef ObjectCreatedCallback = void Function(String className, Object object); -export 'dart:ui' show VoidCallback; +/// Signature of callbacks that are called when an object is disposed. +/// +/// The `object` is the object that was disposed. +typedef ObjectDisposedCallback = void Function(Object object); /// An object that maintains a list of listeners. /// /// The listeners are typically used to notify clients that the object has been /// updated. /// -/// There are two variants of this interface: +/// There is one variants of this interface: /// /// * [ValueListenable], an interface that augments the [Listenable] interface /// with the concept of a _current value_. /// -/// * [Animation], an interface that augments the [ValueListenable] interface -/// to add the concept of direction (forward or reverse). -/// -/// Many classes in the Flutter API use or implement these interfaces. The -/// following subclasses are especially relevant: +/// Many classes in this package implement these interfaces. The following +/// subclasses are especially relevant: /// /// * [ChangeNotifier], which can be subclassed or mixed in to create objects /// that implement the [Listenable] interface. @@ -43,19 +46,6 @@ export 'dart:ui' show VoidCallback; /// and "fire notifications" are used interchangeably. /// /// See also: -/// -/// * [AnimatedBuilder], a widget that uses a builder callback to rebuild -/// whenever a given [Listenable] triggers its notifications. This widget is -/// commonly used with [Animation] subclasses, hence its name, but is by no -/// means limited to animations, as it can be used with any [Listenable]. It -/// is a subclass of [AnimatedWidget], which can be used to create widgets -/// that are driven from a [Listenable]. -/// * [ValueListenableBuilder], a widget that uses a builder callback to -/// rebuild whenever a [ValueListenable] object triggers its notifications, -/// providing the builder with the value of the object. -/// * [InheritedNotifier], an abstract superclass for widgets that use a -/// [Listenable]'s notifications to trigger rebuilds in descendant widgets -/// that declare a dependency on them, using the [InheritedWidget] mechanism. /// * [Listenable.merge], which creates a [Listenable] that triggers /// notifications whenever any of a list of other [Listenable]s trigger their /// notifications. @@ -71,8 +61,31 @@ abstract class Listenable { /// Doing so will lead to memory leaks or exceptions. /// /// The iterable may contain nulls; they are ignored. + /// + /// {@example example/lib/listenable_merge.dart} factory Listenable.merge(Iterable listenables) = _MergingListenable; + /// Error callback that is called when an error is thrown by a listener. + /// + /// By default, errors are thrown as [StateError]. + static ErrorCallback onError = (String message, StackTrace? stackTrace) { + throw StateError(message); + }; + + /// Called when a new object is created. + /// + /// This can be useful for tracking memory leaks. + /// + /// Only called in debug mode. + static ObjectCreatedCallback debugMaybeDispatchCreated = (String _, Object _) {}; + + /// Called when an object is disposed. + /// + /// This can be useful for tracking memory leaks. + /// + /// Only called in debug mode. + static ObjectDisposedCallback debugMaybeDispatchDisposed = (Object _) {}; + /// Register a closure to be called when the object notifies its listeners. void addListener(VoidCallback listener); @@ -83,14 +96,8 @@ abstract class Listenable { /// An interface for subclasses of [Listenable] that expose a [value]. /// -/// This interface is implemented by [ValueNotifier] and [Animation], and -/// allows other APIs to accept either of those implementations interchangeably. -/// -/// See also: -/// -/// * [ValueListenableBuilder], a widget that uses a builder callback to -/// rebuild whenever a [ValueListenable] object triggers its notifications, -/// providing the builder with the value of the object. +/// This interface is implemented by [ValueNotifier] and allows other APIs +/// to accept either of those implementations interchangeably. abstract class ValueListenable extends Listenable { /// This constructor enables subclasses to provide const constructors so that /// they can be used in const expressions. @@ -112,26 +119,17 @@ abstract class ValueListenable extends Listenable { /// ## Using ChangeNotifier subclasses for data models /// /// A data structure can extend or mix in [ChangeNotifier] to implement the -/// [Listenable] interface and thus become usable with widgets that listen for -/// changes to [Listenable]s, such as [ListenableBuilder]. +/// [Listenable] interface. /// -/// {@tool dartpad} -/// The following example implements a simple counter that utilizes a -/// [ListenableBuilder] to limit rebuilds to only the [Text] widget containing -/// the count. The current count is stored in a [ChangeNotifier] subclass, which -/// rebuilds the [ListenableBuilder]'s contents when its value is changed. +/// The following example implements a simple counter whose current count is +/// stored in a [ChangeNotifier] subclass, notifying clients when the value changes: /// -/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.2.dart ** -/// {@end-tool} +/// {@example example/lib/counter.dart} /// -/// {@tool dartpad} -/// In this case, the [ChangeNotifier] subclass encapsulates a list, and notifies -/// the clients any time an item is added to the list. This example only supports -/// adding items; as an exercise, consider adding buttons to remove items from -/// the list as well. +/// In this case, the [ChangeNotifier] subclass encapsulates a list, notifying +/// clients whenever an item is added or removed: /// -/// ** See code in examples/api/lib/widgets/transitions/listenable_builder.3.dart ** -/// {@end-tool} +/// {@example example/lib/list_notifier.dart} /// /// See also: /// @@ -152,8 +150,8 @@ mixin class ChangeNotifier implements Listenable { int _reentrantlyRemovedListeners = 0; bool _debugDisposed = false; - /// If true, the event [ObjectCreated] for this instance was dispatched to - /// [FlutterMemoryAllocations]. + /// If true, the creation of this instance was dispatched to + /// [Listenable.debugMaybeDispatchCreated]. /// /// As [ChangeNotifier] is used as mixin, it does not have constructor, /// so we use [addListener] to dispatch the event. @@ -162,7 +160,6 @@ mixin class ChangeNotifier implements Listenable { /// Used by subclasses to assert that the [ChangeNotifier] has not yet been /// disposed. /// - /// {@tool snippet} /// The [debugAssertNotDisposed] function should only be called inside of an /// assert, as in this example. /// @@ -174,17 +171,17 @@ mixin class ChangeNotifier implements Listenable { /// } /// } /// ``` - /// {@end-tool} // This is static and not an instance method because too many people try to // implement ChangeNotifier instead of extending it (and so it is too breaking // to add a method, especially for debug). static bool debugAssertNotDisposed(ChangeNotifier notifier) { assert(() { if (notifier._debugDisposed) { - throw FlutterError( + Listenable.onError( 'A ${notifier.runtimeType} was used after being disposed.\n' 'Once you have called dispose() on a ${notifier.runtimeType}, it ' 'can no longer be used.', + StackTrace.current, ); } return true; @@ -212,10 +209,7 @@ mixin class ChangeNotifier implements Listenable { @protected bool get hasListeners => _count > 0; - /// Dispatches event of the [object] creation to [FlutterMemoryAllocations.instance]. - /// - /// If the event was already dispatched or [kFlutterMemoryAllocationsEnabled] - /// is false, the method is noop. + /// Dispatches the event of the [object] creation to [Listenable.debugMaybeDispatchCreated]. /// /// Tools like leak_tracker use the event of object creation to help /// developers identify the owner of the object, for troubleshooting purposes, @@ -228,14 +222,11 @@ mixin class ChangeNotifier implements Listenable { /// To make debugging easier, invoke [ChangeNotifier.maybeDispatchObjectCreation] /// in constructor of the class. It will help /// to identify the owner. - /// - /// Make sure to invoke it with condition `if (kFlutterMemoryAllocationsEnabled) ...` - /// so that the method is tree-shaken away when the flag is false. @protected static void maybeDispatchObjectCreation(ChangeNotifier object) { assert(() { if (!object._debugCreationDispatched) { - debugMaybeDispatchCreated('foundation', 'ChangeNotifier', object); + Listenable.debugMaybeDispatchCreated('ChangeNotifier', object); object._debugCreationDispatched = true; } return true; @@ -250,7 +241,6 @@ mixin class ChangeNotifier implements Listenable { /// /// 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 @@ -262,7 +252,6 @@ mixin class ChangeNotifier implements Listenable { /// 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: /// @@ -272,9 +261,10 @@ mixin class ChangeNotifier implements Listenable { void addListener(VoidCallback listener) { assert(ChangeNotifier.debugAssertNotDisposed(this)); - if (kFlutterMemoryAllocationsEnabled) { - maybeDispatchObjectCreation(this); - } + assert(() { + ChangeNotifier.maybeDispatchObjectCreation(this); + return true; + }()); if (_count == _listeners.length) { if (_count == 0) { @@ -329,7 +319,14 @@ mixin class ChangeNotifier implements Listenable { /// /// This method returns immediately if [dispose] has been called. /// - /// {@macro 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 not be called. + /// + /// 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: /// @@ -384,7 +381,7 @@ mixin class ChangeNotifier implements Listenable { assert(() { _debugDisposed = true; if (_debugCreationDispatched) { - assert(debugMaybeDispatchDisposed(this)); + Listenable.debugMaybeDispatchDisposed(this); } return true; }()); @@ -400,7 +397,7 @@ mixin class ChangeNotifier implements Listenable { /// not be visited after they are removed. /// /// Exceptions thrown by listeners will be caught and reported using - /// [FlutterError.reportError]. + /// [Listenable.onError]. /// /// This method must not be called after [dispose] has been called. /// @@ -434,21 +431,7 @@ mixin class ChangeNotifier implements Listenable { try { _listeners[i]?.call(); } catch (exception, stack) { - FlutterError.reportError( - FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'foundation library', - context: ErrorDescription('while dispatching notifications for $runtimeType'), - informationCollector: () => [ - DiagnosticsProperty( - 'The $runtimeType sending notification was', - this, - style: DiagnosticsTreeStyle.errorProperty, - ), - ], - ), - ); + Listenable.onError(exception.toString(), stack); } } @@ -539,12 +522,15 @@ class _MergingListenable extends Listenable { /// /// For mutable data types, consider extending [ChangeNotifier] directly and /// calling [notifyListeners] manually when changes occur. +/// +/// {@example example/lib/value_notifier.dart} class ValueNotifier extends ChangeNotifier implements ValueListenable { /// Creates a [ChangeNotifier] that wraps this value. ValueNotifier(this._value) { - if (kFlutterMemoryAllocationsEnabled) { + assert(() { ChangeNotifier.maybeDispatchObjectCreation(this); - } + return true; + }()); } /// The current value stored in this notifier. @@ -564,5 +550,23 @@ class ValueNotifier extends ChangeNotifier implements ValueListenable { } @override - String toString() => '${describeIdentity(this)}($value)'; + String toString() => '${_describeIdentity(this)}($value)'; +} + +/// Returns a 5 character long hexadecimal string generated from +/// [Object.hashCode]'s 20 least-significant bits. +String _shortHash(Object? object) { + return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); +} + +/// Returns the runtime type of the given `object`.. +String _describeIdentity(Object? object) => + '${_objectRuntimeType(object, '')}#${_shortHash(object)}'; + +String _objectRuntimeType(Object? object, String optimizedValue) { + assert(() { + optimizedValue = object.runtimeType.toString(); + return true; + }()); + return optimizedValue; } diff --git a/packages/listen/pubspec.yaml b/packages/listen/pubspec.yaml new file mode 100644 index 00000000..7d611245 --- /dev/null +++ b/packages/listen/pubspec.yaml @@ -0,0 +1,21 @@ +# Copyright 2013 The Flutter Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +name: listen +description: A lightweight synchronous listener registry for pure Dart applications. +repository: https://github.com/flutter/core-packages/tree/main/packages/listen +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+listen%22 +version: 0.1.0 + +environment: + sdk: ^3.10.0 + +dependencies: + meta: ^1.11.0 + +dev_dependencies: + test: ^1.25.9 + +topics: + - state-management + - listenable diff --git a/packages/listen/test/listen_test.dart b/packages/listen/test/listen_test.dart index 1b7bfc5d..8acc6320 100644 --- a/packages/listen/test/listen_test.dart +++ b/packages/listen/test/listen_test.dart @@ -1,9 +1,9 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; +import 'package:listen/listen.dart'; +import 'package:test/test.dart'; class TestNotifier extends ChangeNotifier { void notify() { @@ -26,12 +26,6 @@ class A { } class B extends A with ChangeNotifier { - B() { - if (kFlutterMemoryAllocationsEnabled) { - ChangeNotifier.maybeDispatchObjectCreation(this); - } - } - @override void test() { notifyListeners(); @@ -41,9 +35,7 @@ class B extends A with ChangeNotifier { class Counter with ChangeNotifier { Counter() { - if (kFlutterMemoryAllocationsEnabled) { - ChangeNotifier.maybeDispatchObjectCreation(this); - } + ChangeNotifier.maybeDispatchObjectCreation(this); } int get value => _value; @@ -61,7 +53,23 @@ class Counter with ChangeNotifier { } void main() { - testWidgets('ChangeNotifier can not dispose in callback', (WidgetTester tester) async { + final ErrorCallback originalOnError = Listenable.onError; + String? lastErrorMessage; + + setUp(() { + lastErrorMessage = null; + Listenable.onError = (String message, StackTrace? stackTrace) { + lastErrorMessage = message; + }; + }); + tearDown(() { + Listenable.onError = originalOnError; + if (lastErrorMessage != null) { + throw StateError('Unexpected error in test: $lastErrorMessage'); + } + }); + + test('ChangeNotifier can not dispose in callback', () async { final test = TestNotifier(); var callbackDidFinish = false; void foo() { @@ -73,14 +81,14 @@ void main() { test.notify(); - final error = tester.takeException() as AssertionError; - expect(error.toString().contains('dispose()'), isTrue); + expect(lastErrorMessage, contains('dispose()')); // Make sure it crashes during dispose call. expect(callbackDidFinish, isFalse); test.dispose(); + lastErrorMessage = null; }); - testWidgets('ChangeNotifier', (WidgetTester tester) async { + test('ChangeNotifier', () { final log = []; void listener() { log.add('listener'); @@ -101,6 +109,15 @@ void main() { final test = TestNotifier(); + final ErrorCallback original = Listenable.onError; + final List errorMessages = []; + Listenable.onError = (String message, StackTrace? stackTrace) { + errorMessages.add(message); + }; + addTearDown(() { + Listenable.onError = original; + }); + test.addListener(listener); test.addListener(listener); test.notify(); @@ -150,7 +167,7 @@ void main() { test.addListener(badListener); test.notify(); expect(log, ['listener', 'listener2', 'listener1', 'badListener']); - expect(tester.takeException(), isArgumentError); + expect(errorMessages.removeAt(0), contains('Invalid argument')); log.clear(); test.addListener(listener1); @@ -160,7 +177,7 @@ void main() { test.addListener(listener2); test.notify(); expect(log, ['badListener', 'listener1', 'listener2']); - expect(tester.takeException(), isArgumentError); + expect(errorMessages.removeAt(0), contains('Invalid argument')); log.clear(); test.dispose(); }); @@ -369,15 +386,18 @@ void main() { test('Cannot use a disposed ChangeNotifier except for remove listener', () { final source = TestNotifier(); source.dispose(); - expect(() { - source.addListener(() {}); - }, throwsFlutterError); - expect(() { - source.dispose(); - }, throwsFlutterError); - expect(() { - source.notify(); - }, throwsFlutterError); + + source.addListener(() {}); + expect(lastErrorMessage, contains('TestNotifier was used after being disposed.')); + lastErrorMessage = null; + + source.dispose(); + expect(lastErrorMessage, contains('TestNotifier was used after being disposed.')); + lastErrorMessage = null; + + source.notify(); + expect(lastErrorMessage, contains('TestNotifier was used after being disposed.')); + lastErrorMessage = null; }); test('Can remove listener on a disposed ChangeNotifier', () { @@ -494,46 +514,22 @@ void main() { test('Throws FlutterError when disposed and called', () { final testNotifier = TestNotifier(); testNotifier.dispose(); - FlutterError? error; - try { - testNotifier.dispose(); - } on FlutterError catch (e) { - error = e; - } - expect(error, isNotNull); - expect(error, isFlutterError); - expect( - error!.toStringDeep(), - equalsIgnoringHashCodes( - 'FlutterError\n' - ' A TestNotifier was used after being disposed.\n' - ' Once you have called dispose() on a TestNotifier, it can no\n' - ' longer be used.\n', - ), - ); + + testNotifier.dispose(); + + expect(lastErrorMessage, contains('TestNotifier was used after being disposed.')); + lastErrorMessage = null; }); test('Calling debugAssertNotDisposed works as intended', () { final testNotifier = TestNotifier(); expect(ChangeNotifier.debugAssertNotDisposed(testNotifier), isTrue); testNotifier.dispose(); - FlutterError? error; - try { - ChangeNotifier.debugAssertNotDisposed(testNotifier); - } on FlutterError catch (e) { - error = e; - } - expect(error, isNotNull); - expect(error, isFlutterError); - expect( - error!.toStringDeep(), - equalsIgnoringHashCodes( - 'FlutterError\n' - ' A TestNotifier was used after being disposed.\n' - ' Once you have called dispose() on a TestNotifier, it can no\n' - ' longer be used.\n', - ), - ); + + ChangeNotifier.debugAssertNotDisposed(testNotifier); + + expect(lastErrorMessage, contains('TestNotifier was used after being disposed.')); + lastErrorMessage = null; }); test('notifyListener can be called recursively', () { From da26a373161bb4a43376c786c758049cdbd7a02b Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Tue, 30 Jun 2026 18:55:50 -0700 Subject: [PATCH 109/109] addressing comments --- README.md | 2 +- packages/listen/CHANGELOG.md | 4 ++-- packages/listen/README.md | 23 +++++++------------ .../listen/example/lib/readme_excerpts.dart | 10 ++++---- .../listen/example/test/example_test.dart | 15 ++++++++++++ packages/listen/lib/listen.dart | 2 +- packages/listen/lib/src/listen.dart | 15 ++++-------- packages/listen/pubspec.yaml | 4 ++-- 8 files changed, 38 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 5295bd67..f1400049 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ These are the packages hosted in this repository: |---------|-----|--------|-------|--------|---------------| | [flutter\_hook\_config](./packages/flutter_hook_config/) | [![pub package](https://img.shields.io/pub/v/flutter_hook_config.svg)](https://pub.dev/packages/flutter_hook_config) | [![pub points](https://img.shields.io/pub/points/flutter_hook_config)](https://pub.dev/packages/flutter_hook_config/score) | [![downloads](https://img.shields.io/pub/dm/flutter_hook_config)](https://pub.dev/packages/flutter_hook_config/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20flutter_hook_config?label=)](https://github.com/flutter/flutter/labels/p%3A%20flutter_hook_config) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20flutter_hook_config?label=)](https://github.com/flutter/core-packages/labels/p%3A%20flutter_hook_config) | | [flutter\_template\_images](./packages/flutter_template_images/) | [![pub package](https://img.shields.io/pub/v/flutter_template_images.svg)](https://pub.dev/packages/flutter_template_images) | [![pub points](https://img.shields.io/pub/points/flutter_template_images)](https://pub.dev/packages/flutter_template_images/score) | [![downloads](https://img.shields.io/pub/dm/flutter_template_images)](https://pub.dev/packages/flutter_template_images/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20flutter_template_images?label=)](https://github.com/flutter/flutter/labels/p%3A%20flutter_template_images) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20flutter_template_images?label=)](https://github.com/flutter/core-packages/labels/p%3A%20flutter_template_images) | +| [listen](./packages/listen/) | [![pub package](https://img.shields.io/pub/v/listen.svg)](https://pub.dev/packages/listen) | [![pub points](https://img.shields.io/pub/points/listen)](https://pub.dev/packages/listen/score) | [![downloads](https://img.shields.io/pub/dm/listen)](https://pub.dev/packages/listen/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20listen?label=)](https://github.com/flutter/flutter/labels/p%3A%20listen) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20listen?label=)](https://github.com/flutter/core-packages/labels/p%3A%20listen) | | [multicast\_dns](./packages/multicast_dns/) | [![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](https://pub.dev/packages/multicast_dns) | [![pub points](https://img.shields.io/pub/points/multicast_dns)](https://pub.dev/packages/multicast_dns/score) | [![downloads](https://img.shields.io/pub/dm/multicast_dns)](https://pub.dev/packages/multicast_dns/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20multicast_dns?label=)](https://github.com/flutter/flutter/labels/p%3A%20multicast_dns) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20multicast_dns?label=)](https://github.com/flutter/core-packages/labels/p%3A%20multicast_dns) | | [mustache\_template](./third_party/packages/mustache_template/) | [![pub package](https://img.shields.io/pub/v/mustache_template.svg)](https://pub.dev/packages/mustache_template) | [![pub points](https://img.shields.io/pub/points/mustache_template)](https://pub.dev/packages/mustache_template/score) | [![downloads](https://img.shields.io/pub/dm/mustache_template)](https://pub.dev/packages/mustache_template/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20mustache_template?label=)](https://github.com/flutter/flutter/labels/p%3A%20mustache_template) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20mustache_template?label=)](https://github.com/flutter/core-packages/labels/p%3A%20mustache_template) | | [standard\_message\_codec](./packages/standard_message_codec/) | [![pub package](https://img.shields.io/pub/v/standard_message_codec.svg)](https://pub.dev/packages/standard_message_codec) | [![pub points](https://img.shields.io/pub/points/standard_message_codec)](https://pub.dev/packages/standard_message_codec/score) | [![downloads](https://img.shields.io/pub/dm/standard_message_codec)](https://pub.dev/packages/standard_message_codec/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20standard_message_codec?label=)](https://github.com/flutter/flutter/labels/p%3A%20standard_message_codec) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20standard_message_codec?label=)](https://github.com/flutter/core-packages/labels/p%3A%20standard_message_codec) | | [vector\_math](./packages/vector_math/) | [![pub package](https://img.shields.io/pub/v/vector_math.svg)](https://pub.dev/packages/vector_math) | [![pub points](https://img.shields.io/pub/points/vector_math)](https://pub.dev/packages/vector_math/score) | [![downloads](https://img.shields.io/pub/dm/vector_math)](https://pub.dev/packages/vector_math/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20vector_math?label=)](https://github.com/flutter/flutter/labels/p%3A%20vector_math) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20vector_math?label=)](https://github.com/flutter/core-packages/labels/p%3A%20vector_math) | -| [listen](./packages/listen/) | [![pub package](https://img.shields.io/pub/v/listen.svg)](https://pub.dev/packages/listen) | [![pub points](https://img.shields.io/pub/points/listen)](https://pub.dev/packages/listen/score) | [![downloads](https://img.shields.io/pub/dm/listen)](https://pub.dev/packages/listen/score) | [![GitHub issues by-label](https://img.shields.io/github/issues/flutter/flutter/p%3A%20listen?label=)](https://github.com/flutter/flutter/labels/p%3A%20listen) | [![GitHub pull requests by-label](https://img.shields.io/github/issues-pr/flutter/core-packages/p%3A%20listen?label=)](https://github.com/flutter/core-packages/labels/p%3A%20listen) | diff --git a/packages/listen/CHANGELOG.md b/packages/listen/CHANGELOG.md index a0712a79..5bb866eb 100644 --- a/packages/listen/CHANGELOG.md +++ b/packages/listen/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.1.0 +## 1.0.0-beta.1 -- Initial version. +- Moves source code from `flutter/flutter` to `flutter/core-packages`. diff --git a/packages/listen/README.md b/packages/listen/README.md index ec6aa0c3..9e2a6f21 100644 --- a/packages/listen/README.md +++ b/packages/listen/README.md @@ -2,22 +2,10 @@ # listen -A lightweight synchronous listener registry for pure Dart applications. - -## Getting started - -Add this package to your `pubspec.yaml` dependencies. +A package to notify state changes to interested listeners in pure Dart. ## Usage -Import the package in your Dart code: - - -```dart -import 'package:listen/listen.dart'; - -``` - ### Using ValueNotifier `ValueNotifier` wraps a single value and notifies listeners whenever the value changes: @@ -44,7 +32,7 @@ void valueNotifierExample() { Extend or mix in `ChangeNotifier` to manage state and notify listeners manually: - + ```dart /// A [ChangeNotifier] subclass that encapsulates a list of items and notifies /// listeners whenever items are added or removed. @@ -68,7 +56,12 @@ class ItemListNotifier extends ChangeNotifier { } } -/// Demonstrates [ChangeNotifier] usage for README. +``` + +Then, listen to changes and update state: + + +```dart void changeNotifierExample() { final listNotifier = ItemListNotifier(); diff --git a/packages/listen/example/lib/readme_excerpts.dart b/packages/listen/example/lib/readme_excerpts.dart index c8dee104..c0fb9c69 100644 --- a/packages/listen/example/lib/readme_excerpts.dart +++ b/packages/listen/example/lib/readme_excerpts.dart @@ -7,11 +7,8 @@ // ignore_for_file: avoid_print -// #docregion Import import 'package:listen/listen.dart'; -// #enddocregion Import - /// Demonstrates [ValueNotifier] usage for README. // #docregion ValueNotifier void valueNotifierExample() { @@ -30,7 +27,7 @@ void valueNotifierExample() { // #enddocregion ValueNotifier -// #docregion ChangeNotifier +// #docregion ChangeNotifierClass /// A [ChangeNotifier] subclass that encapsulates a list of items and notifies /// listeners whenever items are added or removed. class ItemListNotifier extends ChangeNotifier { @@ -53,7 +50,10 @@ class ItemListNotifier extends ChangeNotifier { } } +// #enddocregion ChangeNotifierClass + /// Demonstrates [ChangeNotifier] usage for README. +// #docregion ChangeNotifierUsage void changeNotifierExample() { final listNotifier = ItemListNotifier(); @@ -68,7 +68,7 @@ void changeNotifierExample() { listNotifier.dispose(); } -// #enddocregion ChangeNotifier +// #enddocregion ChangeNotifierUsage /// Demonstrates [Listenable.merge] usage for README. // #docregion Merge diff --git a/packages/listen/example/test/example_test.dart b/packages/listen/example/test/example_test.dart index 87af4ac7..7f30e30d 100644 --- a/packages/listen/example/test/example_test.dart +++ b/packages/listen/example/test/example_test.dart @@ -6,6 +6,7 @@ import 'package:listen_example/counter.dart' as counter; import 'package:listen_example/list_notifier.dart' as list_notifier; import 'package:listen_example/listenable_merge.dart' as listenable_merge; import 'package:listen_example/main.dart' as basic_main; +import 'package:listen_example/readme_excerpts.dart' as readme_excerpts; import 'package:listen_example/value_notifier.dart' as value_notifier; import 'package:test/test.dart'; @@ -29,4 +30,18 @@ void main() { test('basic main example runs without error', () { expect(basic_main.main, returnsNormally); }); + + group('readme excerpts', () { + test('valueNotifierExample runs without error', () { + expect(readme_excerpts.valueNotifierExample, returnsNormally); + }); + + test('changeNotifierExample runs without error', () { + expect(readme_excerpts.changeNotifierExample, returnsNormally); + }); + + test('mergeExample runs without error', () { + expect(readme_excerpts.mergeExample, returnsNormally); + }); + }); } diff --git a/packages/listen/lib/listen.dart b/packages/listen/lib/listen.dart index 99bd9d9f..04d4d892 100644 --- a/packages/listen/lib/listen.dart +++ b/packages/listen/lib/listen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// A lightweight synchronous listener registry. +/// Notify state changes to interested listeners. library; export 'src/listen.dart'; diff --git a/packages/listen/lib/src/listen.dart b/packages/listen/lib/src/listen.dart index e5305b56..2c292316 100644 --- a/packages/listen/lib/src/listen.dart +++ b/packages/listen/lib/src/listen.dart @@ -28,24 +28,17 @@ typedef ObjectDisposedCallback = void Function(Object object); /// The listeners are typically used to notify clients that the object has been /// updated. /// -/// There is one variants of this interface: +/// The terms "notify clients", "send notifications", "trigger notifications", +/// and "fire notifications" are used interchangeably. +/// +/// See also: /// /// * [ValueListenable], an interface that augments the [Listenable] interface /// with the concept of a _current value_. -/// -/// Many classes in this package implement these interfaces. The following -/// subclasses are especially relevant: -/// /// * [ChangeNotifier], which can be subclassed or mixed in to create objects /// that implement the [Listenable] interface. -/// /// * [ValueNotifier], which implements the [ValueListenable] interface with /// a mutable value that triggers the notifications when modified. -/// -/// The terms "notify clients", "send notifications", "trigger notifications", -/// and "fire notifications" are used interchangeably. -/// -/// See also: /// * [Listenable.merge], which creates a [Listenable] that triggers /// notifications whenever any of a list of other [Listenable]s trigger their /// notifications. diff --git a/packages/listen/pubspec.yaml b/packages/listen/pubspec.yaml index 7d611245..c9e39a6b 100644 --- a/packages/listen/pubspec.yaml +++ b/packages/listen/pubspec.yaml @@ -2,10 +2,10 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. name: listen -description: A lightweight synchronous listener registry for pure Dart applications. +description: A package to notify state changes to interested listeners in pure Dart. repository: https://github.com/flutter/core-packages/tree/main/packages/listen issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+listen%22 -version: 0.1.0 +version: 1.0.0-beta.1 environment: sdk: ^3.10.0