mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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. <!-- Links --> [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
611 lines
15 KiB
Dart
611 lines
15 KiB
Dart
// 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.
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
class TestNotifier extends ChangeNotifier {
|
|
void notify() {
|
|
notifyListeners();
|
|
}
|
|
|
|
bool get isListenedTo => hasListeners;
|
|
}
|
|
|
|
class HasListenersTester<T> extends ValueNotifier<T> {
|
|
HasListenersTester(super.value);
|
|
bool get testHasListeners => hasListeners;
|
|
}
|
|
|
|
class A {
|
|
bool result = false;
|
|
void test() {
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
class B extends A with ChangeNotifier {
|
|
B() {
|
|
if (kFlutterMemoryAllocationsEnabled) {
|
|
ChangeNotifier.maybeDispatchObjectCreation(this);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void test() {
|
|
notifyListeners();
|
|
super.test();
|
|
}
|
|
}
|
|
|
|
class Counter with ChangeNotifier {
|
|
Counter() {
|
|
if (kFlutterMemoryAllocationsEnabled) {
|
|
ChangeNotifier.maybeDispatchObjectCreation(this);
|
|
}
|
|
}
|
|
|
|
int get value => _value;
|
|
int _value = 0;
|
|
set value(int value) {
|
|
if (_value != value) {
|
|
_value = value;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void notify() {
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('ChangeNotifier can not dispose in callback', (WidgetTester tester) async {
|
|
final test = TestNotifier();
|
|
var callbackDidFinish = false;
|
|
void foo() {
|
|
test.dispose();
|
|
callbackDidFinish = true;
|
|
}
|
|
|
|
test.addListener(foo);
|
|
|
|
test.notify();
|
|
|
|
final 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 {
|
|
final log = <String>[];
|
|
void listener() {
|
|
log.add('listener');
|
|
}
|
|
|
|
void listener1() {
|
|
log.add('listener1');
|
|
}
|
|
|
|
void listener2() {
|
|
log.add('listener2');
|
|
}
|
|
|
|
void badListener() {
|
|
log.add('badListener');
|
|
throw ArgumentError();
|
|
}
|
|
|
|
final test = TestNotifier();
|
|
|
|
test.addListener(listener);
|
|
test.addListener(listener);
|
|
test.notify();
|
|
expect(log, <String>['listener', 'listener']);
|
|
log.clear();
|
|
|
|
test.removeListener(listener);
|
|
test.notify();
|
|
expect(log, <String>['listener']);
|
|
log.clear();
|
|
|
|
test.removeListener(listener);
|
|
test.notify();
|
|
expect(log, <String>[]);
|
|
log.clear();
|
|
|
|
test.removeListener(listener);
|
|
test.notify();
|
|
expect(log, <String>[]);
|
|
log.clear();
|
|
|
|
test.addListener(listener);
|
|
test.notify();
|
|
expect(log, <String>['listener']);
|
|
log.clear();
|
|
|
|
test.addListener(listener1);
|
|
test.notify();
|
|
expect(log, <String>['listener', 'listener1']);
|
|
log.clear();
|
|
|
|
test.addListener(listener2);
|
|
test.notify();
|
|
expect(log, <String>['listener', 'listener1', 'listener2']);
|
|
log.clear();
|
|
|
|
test.removeListener(listener1);
|
|
test.notify();
|
|
expect(log, <String>['listener', 'listener2']);
|
|
log.clear();
|
|
|
|
test.addListener(listener1);
|
|
test.notify();
|
|
expect(log, <String>['listener', 'listener2', 'listener1']);
|
|
log.clear();
|
|
|
|
test.addListener(badListener);
|
|
test.notify();
|
|
expect(log, <String>['listener', 'listener2', 'listener1', 'badListener']);
|
|
expect(tester.takeException(), isArgumentError);
|
|
log.clear();
|
|
|
|
test.addListener(listener1);
|
|
test.removeListener(listener);
|
|
test.removeListener(listener1);
|
|
test.removeListener(listener2);
|
|
test.addListener(listener2);
|
|
test.notify();
|
|
expect(log, <String>['badListener', 'listener1', 'listener2']);
|
|
expect(tester.takeException(), isArgumentError);
|
|
log.clear();
|
|
test.dispose();
|
|
});
|
|
|
|
test('ChangeNotifier with mutating listener', () {
|
|
final test = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
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);
|
|
test.addListener(listener3);
|
|
test.notify();
|
|
expect(log, <String>['listener1', 'listener2']);
|
|
log.clear();
|
|
|
|
test.notify();
|
|
expect(log, <String>['listener2', 'listener4']);
|
|
log.clear();
|
|
|
|
test.notify();
|
|
expect(log, <String>['listener2', 'listener4', 'listener4']);
|
|
log.clear();
|
|
});
|
|
|
|
test('During notifyListeners, a listener was added and removed immediately', () {
|
|
final source = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
void listener3() {
|
|
log.add('listener3');
|
|
}
|
|
|
|
void 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, <String>['listener1']);
|
|
});
|
|
|
|
test('If a listener in the middle of the list of listeners removes itself, '
|
|
'notifyListeners still notifies all listeners', () {
|
|
final source = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
void selfRemovingListener() {
|
|
log.add('selfRemovingListener');
|
|
source.removeListener(selfRemovingListener);
|
|
}
|
|
|
|
void listener1() {
|
|
log.add('listener1');
|
|
}
|
|
|
|
source.addListener(listener1);
|
|
source.addListener(selfRemovingListener);
|
|
source.addListener(listener1);
|
|
|
|
source.notify();
|
|
|
|
expect(log, <String>['listener1', 'selfRemovingListener', 'listener1']);
|
|
});
|
|
|
|
test('If the first listener removes itself, notifyListeners still notify all listeners', () {
|
|
final source = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
void selfRemovingListener() {
|
|
log.add('selfRemovingListener');
|
|
source.removeListener(selfRemovingListener);
|
|
}
|
|
|
|
void listener1() {
|
|
log.add('listener1');
|
|
}
|
|
|
|
source.addListener(selfRemovingListener);
|
|
source.addListener(listener1);
|
|
|
|
source.notifyListeners();
|
|
|
|
expect(log, <String>['selfRemovingListener', 'listener1']);
|
|
});
|
|
|
|
test('Merging change notifiers', () {
|
|
final source1 = TestNotifier();
|
|
final source2 = TestNotifier();
|
|
final source3 = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
final merged = Listenable.merge(<Listenable>[source1, source2]);
|
|
void listener1() {
|
|
log.add('listener1');
|
|
}
|
|
|
|
void listener2() {
|
|
log.add('listener2');
|
|
}
|
|
|
|
merged.addListener(listener1);
|
|
source1.notify();
|
|
source2.notify();
|
|
source3.notify();
|
|
expect(log, <String>['listener1', 'listener1']);
|
|
log.clear();
|
|
|
|
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, <String>['listener1', 'listener2', 'listener1', 'listener2']);
|
|
log.clear();
|
|
});
|
|
|
|
test('Merging change notifiers supports any iterable', () {
|
|
final source1 = TestNotifier();
|
|
final source2 = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
final merged = Listenable.merge(<Listenable?>{source1, source2});
|
|
void listener() => log.add('listener');
|
|
|
|
merged.addListener(listener);
|
|
source1.notify();
|
|
source2.notify();
|
|
expect(log, <String>['listener', 'listener']);
|
|
log.clear();
|
|
});
|
|
|
|
test('Merging change notifiers ignores null', () {
|
|
final source1 = TestNotifier();
|
|
final source2 = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
final merged = Listenable.merge(<Listenable?>[null, source1, null, source2, null]);
|
|
void listener() {
|
|
log.add('listener');
|
|
}
|
|
|
|
merged.addListener(listener);
|
|
source1.notify();
|
|
source2.notify();
|
|
expect(log, <String>['listener', 'listener']);
|
|
log.clear();
|
|
});
|
|
|
|
test('Can remove from merged notifier', () {
|
|
final source1 = TestNotifier();
|
|
final source2 = TestNotifier();
|
|
final log = <String>[];
|
|
|
|
final merged = Listenable.merge(<Listenable>[source1, source2]);
|
|
void listener() {
|
|
log.add('listener');
|
|
}
|
|
|
|
merged.addListener(listener);
|
|
source1.notify();
|
|
source2.notify();
|
|
expect(log, <String>['listener', 'listener']);
|
|
log.clear();
|
|
|
|
merged.removeListener(listener);
|
|
source1.notify();
|
|
source2.notify();
|
|
expect(log, isEmpty);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
test('Can remove listener on a disposed ChangeNotifier', () {
|
|
final source = TestNotifier();
|
|
source.dispose();
|
|
source.removeListener(() {});
|
|
});
|
|
|
|
test('Can check hasListener on a disposed ChangeNotifier', () {
|
|
final source = HasListenersTester<int>(0);
|
|
source.addListener(() {});
|
|
expect(source.testHasListeners, isTrue);
|
|
source.dispose();
|
|
expect(source.testHasListeners, isFalse);
|
|
});
|
|
|
|
test('Value notifier', () {
|
|
final notifier = ValueNotifier<double>(2.0);
|
|
|
|
final log = <double>[];
|
|
void listener() {
|
|
log.add(notifier.value);
|
|
}
|
|
|
|
notifier.addListener(listener);
|
|
notifier.value = 3.0;
|
|
|
|
expect(log, equals(<double>[3.0]));
|
|
log.clear();
|
|
|
|
notifier.value = 3.0;
|
|
expect(log, isEmpty);
|
|
});
|
|
|
|
test('Listenable.merge toString', () {
|
|
final source1 = TestNotifier();
|
|
final source2 = TestNotifier();
|
|
|
|
var listenableUnderTest = Listenable.merge(<Listenable>[]);
|
|
expect(listenableUnderTest.toString(), 'Listenable.merge([])');
|
|
|
|
listenableUnderTest = Listenable.merge(<Listenable?>[null]);
|
|
expect(listenableUnderTest.toString(), 'Listenable.merge([null])');
|
|
|
|
listenableUnderTest = Listenable.merge(<Listenable>[source1]);
|
|
expect(listenableUnderTest.toString(), "Listenable.merge([Instance of 'TestNotifier'])");
|
|
|
|
listenableUnderTest = Listenable.merge(<Listenable>[source1, source2]);
|
|
expect(
|
|
listenableUnderTest.toString(),
|
|
"Listenable.merge([Instance of 'TestNotifier', Instance of 'TestNotifier'])",
|
|
);
|
|
|
|
listenableUnderTest = Listenable.merge(<Listenable?>[null, source2]);
|
|
expect(listenableUnderTest.toString(), "Listenable.merge([null, Instance of 'TestNotifier'])");
|
|
});
|
|
|
|
test('Listenable.merge does not leak', () {
|
|
// Regression test for https://github.com/flutter/flutter/issues/25163.
|
|
|
|
final source1 = TestNotifier();
|
|
final source2 = TestNotifier();
|
|
void fakeListener() {}
|
|
|
|
final listenableUnderTest = Listenable.merge(<Listenable>[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 notifier = HasListenersTester<bool>(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);
|
|
});
|
|
|
|
test('ChangeNotifier as a mixin', () {
|
|
// We document that this is a valid way to use this class.
|
|
final b = B();
|
|
var notifications = 0;
|
|
b.addListener(() {
|
|
notifications += 1;
|
|
});
|
|
expect(b.result, isFalse);
|
|
expect(notifications, 0);
|
|
b.test();
|
|
expect(b.result, isTrue);
|
|
expect(notifications, 1);
|
|
});
|
|
|
|
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',
|
|
),
|
|
);
|
|
});
|
|
|
|
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',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('notifyListener can be called recursively', () {
|
|
final counter = Counter();
|
|
final log = <String>[];
|
|
|
|
void listener1() {
|
|
log.add('listener1');
|
|
if (counter.value < 0) {
|
|
counter.value = 0;
|
|
}
|
|
}
|
|
|
|
counter.addListener(listener1);
|
|
counter.notify();
|
|
expect(log, <String>['listener1']);
|
|
log.clear();
|
|
|
|
counter.value = 3;
|
|
expect(log, <String>['listener1']);
|
|
log.clear();
|
|
|
|
counter.value = -2;
|
|
expect(log, <String>['listener1', 'listener1']);
|
|
log.clear();
|
|
});
|
|
|
|
test('Remove Listeners while notifying on a list which will not resize', () {
|
|
final test = TestNotifier();
|
|
final log = <String>[];
|
|
final listeners = <VoidCallback>[];
|
|
|
|
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 (var i = 0; i < 12; i++) {
|
|
void listener() {
|
|
log.add('listener$i');
|
|
}
|
|
|
|
listeners.add(listener);
|
|
test.addListener(listener);
|
|
}
|
|
|
|
final remainingListenerIndexes = <int>[0, 2, 5, 6, 7, 8, 9, 10, 11];
|
|
final List<String> 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 (var i = 0; i < remainingListenerIndexes.length; i++) {
|
|
test.removeListener(listeners[remainingListenerIndexes[i]]);
|
|
}
|
|
|
|
log.clear();
|
|
test.notify();
|
|
expect(log, <String>[]);
|
|
});
|
|
}
|