Skip to content

Commit b3f99ff

Browse files
authored
Fix reentrancy with WidgetBindingObserver callbacks (#131774)
Part of flutter/flutter#131678 Fixes up callsites for WidgetsBindingObserver related callbacks.
1 parent aff8ef1 commit b3f99ff

File tree

2 files changed

+120
-9
lines changed

2 files changed

+120
-9
lines changed

packages/flutter/lib/src/widgets/binding.dart

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
623623
@override
624624
Future<AppExitResponse> handleRequestAppExit() async {
625625
bool didCancel = false;
626-
for (final WidgetsBindingObserver observer in _observers) {
626+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
627627
if ((await observer.didRequestAppExit()) == AppExitResponse.cancel) {
628628
didCancel = true;
629629
// Don't early return. For the case where someone is just using the
@@ -637,31 +637,31 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
637637
@override
638638
void handleMetricsChanged() {
639639
super.handleMetricsChanged();
640-
for (final WidgetsBindingObserver observer in _observers) {
640+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
641641
observer.didChangeMetrics();
642642
}
643643
}
644644

645645
@override
646646
void handleTextScaleFactorChanged() {
647647
super.handleTextScaleFactorChanged();
648-
for (final WidgetsBindingObserver observer in _observers) {
648+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
649649
observer.didChangeTextScaleFactor();
650650
}
651651
}
652652

653653
@override
654654
void handlePlatformBrightnessChanged() {
655655
super.handlePlatformBrightnessChanged();
656-
for (final WidgetsBindingObserver observer in _observers) {
656+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
657657
observer.didChangePlatformBrightness();
658658
}
659659
}
660660

661661
@override
662662
void handleAccessibilityFeaturesChanged() {
663663
super.handleAccessibilityFeaturesChanged();
664-
for (final WidgetsBindingObserver observer in _observers) {
664+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
665665
observer.didChangeAccessibilityFeatures();
666666
}
667667
}
@@ -673,6 +673,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
673673
/// See [dart:ui.PlatformDispatcher.onLocaleChanged].
674674
@protected
675675
@mustCallSuper
676+
@visibleForTesting
676677
void handleLocaleChanged() {
677678
dispatchLocalesChanged(platformDispatcher.locales);
678679
}
@@ -686,7 +687,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
686687
@protected
687688
@mustCallSuper
688689
void dispatchLocalesChanged(List<Locale>? locales) {
689-
for (final WidgetsBindingObserver observer in _observers) {
690+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
690691
observer.didChangeLocales(locales);
691692
}
692693
}
@@ -700,7 +701,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
700701
@protected
701702
@mustCallSuper
702703
void dispatchAccessibilityFeaturesChanged() {
703-
for (final WidgetsBindingObserver observer in _observers) {
704+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
704705
observer.didChangeAccessibilityFeatures();
705706
}
706707
}
@@ -720,6 +721,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
720721
/// This method exposes the `popRoute` notification from
721722
/// [SystemChannels.navigation].
722723
@protected
724+
@visibleForTesting
723725
Future<void> handlePopRoute() async {
724726
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
725727
if (await observer.didPopRoute()) {
@@ -741,6 +743,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
741743
/// [SystemChannels.navigation].
742744
@protected
743745
@mustCallSuper
746+
@visibleForTesting
744747
Future<void> handlePushRoute(String route) async {
745748
final RouteInformation routeInformation = RouteInformation(uri: Uri.parse(route));
746749
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
@@ -777,15 +780,15 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
777780
@override
778781
void handleAppLifecycleStateChanged(AppLifecycleState state) {
779782
super.handleAppLifecycleStateChanged(state);
780-
for (final WidgetsBindingObserver observer in _observers) {
783+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
781784
observer.didChangeAppLifecycleState(state);
782785
}
783786
}
784787

785788
@override
786789
void handleMemoryPressure() {
787790
super.handleMemoryPressure();
788-
for (final WidgetsBindingObserver observer in _observers) {
791+
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
789792
observer.didHaveMemoryPressure();
790793
}
791794
}

packages/flutter/test/widgets/binding_test.dart

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:ui';
6+
57
import 'package:flutter/foundation.dart';
68
import 'package:flutter/services.dart';
79
import 'package:flutter/widgets.dart';
@@ -45,6 +47,94 @@ class PushRouteInformationObserver with WidgetsBindingObserver {
4547
}
4648
}
4749

50+
// Implements to make sure all methods get coverage.
51+
class RentrantObserver implements WidgetsBindingObserver {
52+
RentrantObserver() {
53+
WidgetsBinding.instance.addObserver(this);
54+
}
55+
56+
bool active = true;
57+
58+
int removeSelf() {
59+
active = false;
60+
int count = 0;
61+
while (WidgetsBinding.instance.removeObserver(this)) {
62+
count += 1;
63+
}
64+
return count;
65+
}
66+
67+
@override
68+
void didChangeAccessibilityFeatures() {
69+
assert(active);
70+
WidgetsBinding.instance.addObserver(this);
71+
}
72+
73+
@override
74+
void didChangeAppLifecycleState(AppLifecycleState state) {
75+
assert(active);
76+
WidgetsBinding.instance.addObserver(this);
77+
}
78+
79+
@override
80+
void didChangeLocales(List<Locale>? locales) {
81+
assert(active);
82+
WidgetsBinding.instance.addObserver(this);
83+
}
84+
85+
@override
86+
void didChangeMetrics() {
87+
assert(active);
88+
WidgetsBinding.instance.addObserver(this);
89+
}
90+
91+
@override
92+
void didChangePlatformBrightness() {
93+
assert(active);
94+
WidgetsBinding.instance.addObserver(this);
95+
}
96+
97+
@override
98+
void didChangeTextScaleFactor() {
99+
assert(active);
100+
WidgetsBinding.instance.addObserver(this);
101+
}
102+
103+
@override
104+
void didHaveMemoryPressure() {
105+
assert(active);
106+
WidgetsBinding.instance.addObserver(this);
107+
}
108+
109+
@override
110+
Future<bool> didPopRoute() {
111+
assert(active);
112+
WidgetsBinding.instance.addObserver(this);
113+
return Future<bool>.value(true);
114+
}
115+
116+
@override
117+
Future<bool> didPushRoute(String route) {
118+
assert(active);
119+
WidgetsBinding.instance.addObserver(this);
120+
return Future<bool>.value(true);
121+
}
122+
123+
@override
124+
Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
125+
assert(active);
126+
WidgetsBinding.instance.addObserver(this);
127+
return Future<bool>.value(true);
128+
}
129+
130+
@override
131+
Future<AppExitResponse> didRequestAppExit() {
132+
assert(active);
133+
WidgetsBinding.instance.addObserver(this);
134+
return Future<AppExitResponse>.value(AppExitResponse.exit);
135+
}
136+
}
137+
48138
void main() {
49139
Future<void> setAppLifeCycleState(AppLifecycleState state) async {
50140
final ByteData? message =
@@ -53,6 +143,23 @@ void main() {
53143
.handlePlatformMessage('flutter/lifecycle', message, (_) { });
54144
}
55145

146+
testWidgets('Rentrant observer callbacks do not result in exceptions', (WidgetTester tester) async {
147+
final RentrantObserver observer = RentrantObserver();
148+
WidgetsBinding.instance.handleAccessibilityFeaturesChanged();
149+
WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
150+
WidgetsBinding.instance.handleLocaleChanged();
151+
WidgetsBinding.instance.handleMetricsChanged();
152+
WidgetsBinding.instance.handlePlatformBrightnessChanged();
153+
WidgetsBinding.instance.handleTextScaleFactorChanged();
154+
WidgetsBinding.instance.handleMemoryPressure();
155+
WidgetsBinding.instance.handlePopRoute();
156+
WidgetsBinding.instance.handlePushRoute('/');
157+
WidgetsBinding.instance.handleRequestAppExit();
158+
await tester.idle();
159+
expect(observer.removeSelf(), greaterThan(1));
160+
expect(observer.removeSelf(), 0);
161+
});
162+
56163
testWidgets('didHaveMemoryPressure callback', (WidgetTester tester) async {
57164
final MemoryPressureObserver observer = MemoryPressureObserver();
58165
WidgetsBinding.instance.addObserver(observer);
@@ -118,6 +225,7 @@ void main() {
118225

119226
observer.accumulatedStates.clear();
120227
await expectLater(() async => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError);
228+
WidgetsBinding.instance.removeObserver(observer);
121229
});
122230

123231
testWidgets('didPushRoute callback', (WidgetTester tester) async {

0 commit comments

Comments
 (0)