Unverified Commit b13eacc7 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Fix Listenable.merge to not leak (#26313)

parent 6c6fdaff
...@@ -108,8 +108,10 @@ mixin AnimationLocalListenersMixin { ...@@ -108,8 +108,10 @@ mixin AnimationLocalListenersMixin {
/// ///
/// Listeners can be added with [addListener]. /// Listeners can be added with [addListener].
void removeListener(VoidCallback listener) { void removeListener(VoidCallback listener) {
_listeners.remove(listener); final bool removed = _listeners.remove(listener);
didUnregisterListener(); if (removed) {
didUnregisterListener();
}
} }
/// Calls all the listeners. /// Calls all the listeners.
...@@ -172,8 +174,10 @@ mixin AnimationLocalStatusListenersMixin { ...@@ -172,8 +174,10 @@ mixin AnimationLocalStatusListenersMixin {
/// ///
/// Listeners can be added with [addStatusListener]. /// Listeners can be added with [addStatusListener].
void removeStatusListener(AnimationStatusListener listener) { void removeStatusListener(AnimationStatusListener listener) {
_statusListeners.remove(listener); final bool removed = _statusListeners.remove(listener);
didUnregisterListener(); if (removed) {
didUnregisterListener();
}
} }
/// Calls all the status listeners. /// Calls all the status listeners.
......
...@@ -220,19 +220,23 @@ class ChangeNotifier implements Listenable { ...@@ -220,19 +220,23 @@ class ChangeNotifier implements Listenable {
} }
} }
class _MergingListenable extends ChangeNotifier { class _MergingListenable extends Listenable {
_MergingListenable(this._children) { _MergingListenable(this._children);
for (Listenable child in _children)
child?.addListener(notifyListeners);
}
final List<Listenable> _children; final List<Listenable> _children;
@override @override
void dispose() { void addListener(VoidCallback listener) {
for (Listenable child in _children) for (final Listenable child in _children) {
child?.removeListener(notifyListeners); child?.addListener(listener);
super.dispose(); }
}
@override
void removeListener(VoidCallback listener) {
for (final Listenable child in _children) {
child?.removeListener(listener);
}
} }
@override @override
......
// Copyright 2019 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_test/flutter_test.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
void main() {
test('AnimationLocalStatusListenersMixin with AnimationLazyListenerMixin - removing unregistered listener is no-op', () {
final _TestAnimationLocalStatusListeners uut = _TestAnimationLocalStatusListeners();
final AnimationStatusListener fakeListener = (AnimationStatus status) {};
uut.removeStatusListener(fakeListener);
expect(uut.callsToStart, 0);
expect(uut.callsToStop, 0);
});
test('AnimationLocalListenersMixin with AnimationLazyListenerMixin - removing unregistered listener is no-op', () {
final _TestAnimationLocalListeners uut = _TestAnimationLocalListeners();
final VoidCallback fakeListener = () {};
uut.removeListener(fakeListener);
expect(uut.callsToStart, 0);
expect(uut.callsToStop, 0);
});
}
class _TestAnimationLocalStatusListeners with AnimationLocalStatusListenersMixin, AnimationLazyListenerMixin {
int callsToStart = 0;
int callsToStop = 0;
@override
void didStartListening() {
callsToStart += 1;
}
@override
void didStopListening() {
callsToStop += 1;
}
}
class _TestAnimationLocalListeners with AnimationLocalListenersMixin, AnimationLazyListenerMixin {
int callsToStart = 0;
int callsToStop = 0;
@override
void didStartListening() {
callsToStart += 1;
}
@override
void didStopListening() {
callsToStop += 1;
}
}
...@@ -9,6 +9,8 @@ class TestNotifier extends ChangeNotifier { ...@@ -9,6 +9,8 @@ class TestNotifier extends ChangeNotifier {
void notify() { void notify() {
notifyListeners(); notifyListeners();
} }
bool get isListenedTo => hasListeners;
} }
class HasListenersTester<T> extends ValueNotifier<T> { class HasListenersTester<T> extends ValueNotifier<T> {
...@@ -180,12 +182,12 @@ void main() { ...@@ -180,12 +182,12 @@ void main() {
log.clear(); log.clear();
}); });
test('Can dispose merged notifier', () { test('Can remove from merged notifier', () {
final TestNotifier source1 = TestNotifier(); final TestNotifier source1 = TestNotifier();
final TestNotifier source2 = TestNotifier(); final TestNotifier source2 = TestNotifier();
final List<String> log = <String>[]; final List<String> log = <String>[];
final ChangeNotifier merged = Listenable.merge(<Listenable>[source1, source2]); final Listenable merged = Listenable.merge(<Listenable>[source1, source2]);
final VoidCallback listener = () { log.add('listener'); }; final VoidCallback listener = () { log.add('listener'); };
merged.addListener(listener); merged.addListener(listener);
...@@ -193,8 +195,8 @@ void main() { ...@@ -193,8 +195,8 @@ void main() {
source2.notify(); source2.notify();
expect(log, <String>['listener', 'listener']); expect(log, <String>['listener', 'listener']);
log.clear(); log.clear();
merged.dispose();
merged.removeListener(listener);
source1.notify(); source1.notify();
source2.notify(); source2.notify();
expect(log, isEmpty); expect(log, isEmpty);
...@@ -229,7 +231,7 @@ void main() { ...@@ -229,7 +231,7 @@ void main() {
final TestNotifier source1 = TestNotifier(); final TestNotifier source1 = TestNotifier();
final TestNotifier source2 = TestNotifier(); final TestNotifier source2 = TestNotifier();
ChangeNotifier listenableUnderTest = Listenable.merge(<Listenable>[]); Listenable listenableUnderTest = Listenable.merge(<Listenable>[]);
expect(listenableUnderTest.toString(), 'Listenable.merge([])'); expect(listenableUnderTest.toString(), 'Listenable.merge([])');
listenableUnderTest = Listenable.merge(<Listenable>[null]); listenableUnderTest = Listenable.merge(<Listenable>[null]);
...@@ -254,6 +256,26 @@ void main() { ...@@ -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(<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', () { test('hasListeners', () {
final HasListenersTester<bool> notifier = HasListenersTester<bool>(true); final HasListenersTester<bool> notifier = HasListenersTester<bool>(true);
expect(notifier.testHasListeners, isFalse); expect(notifier.testHasListeners, isFalse);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment