Unverified Commit b3f99ffe authored by Dan Field's avatar Dan Field Committed by GitHub

Fix reentrancy with WidgetBindingObserver callbacks (#131774)

Part of https://github.com/flutter/flutter/issues/131678

Fixes up callsites for WidgetsBindingObserver related callbacks.
parent aff8ef13
...@@ -623,7 +623,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -623,7 +623,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
Future<AppExitResponse> handleRequestAppExit() async { Future<AppExitResponse> handleRequestAppExit() async {
bool didCancel = false; bool didCancel = false;
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
if ((await observer.didRequestAppExit()) == AppExitResponse.cancel) { if ((await observer.didRequestAppExit()) == AppExitResponse.cancel) {
didCancel = true; didCancel = true;
// Don't early return. For the case where someone is just using the // Don't early return. For the case where someone is just using the
...@@ -637,7 +637,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -637,7 +637,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
void handleMetricsChanged() { void handleMetricsChanged() {
super.handleMetricsChanged(); super.handleMetricsChanged();
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeMetrics(); observer.didChangeMetrics();
} }
} }
...@@ -645,7 +645,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -645,7 +645,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
void handleTextScaleFactorChanged() { void handleTextScaleFactorChanged() {
super.handleTextScaleFactorChanged(); super.handleTextScaleFactorChanged();
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeTextScaleFactor(); observer.didChangeTextScaleFactor();
} }
} }
...@@ -653,7 +653,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -653,7 +653,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
void handlePlatformBrightnessChanged() { void handlePlatformBrightnessChanged() {
super.handlePlatformBrightnessChanged(); super.handlePlatformBrightnessChanged();
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangePlatformBrightness(); observer.didChangePlatformBrightness();
} }
} }
...@@ -661,7 +661,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -661,7 +661,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
void handleAccessibilityFeaturesChanged() { void handleAccessibilityFeaturesChanged() {
super.handleAccessibilityFeaturesChanged(); super.handleAccessibilityFeaturesChanged();
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeAccessibilityFeatures(); observer.didChangeAccessibilityFeatures();
} }
} }
...@@ -673,6 +673,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -673,6 +673,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// See [dart:ui.PlatformDispatcher.onLocaleChanged]. /// See [dart:ui.PlatformDispatcher.onLocaleChanged].
@protected @protected
@mustCallSuper @mustCallSuper
@visibleForTesting
void handleLocaleChanged() { void handleLocaleChanged() {
dispatchLocalesChanged(platformDispatcher.locales); dispatchLocalesChanged(platformDispatcher.locales);
} }
...@@ -686,7 +687,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -686,7 +687,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@protected @protected
@mustCallSuper @mustCallSuper
void dispatchLocalesChanged(List<Locale>? locales) { void dispatchLocalesChanged(List<Locale>? locales) {
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeLocales(locales); observer.didChangeLocales(locales);
} }
} }
...@@ -700,7 +701,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -700,7 +701,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@protected @protected
@mustCallSuper @mustCallSuper
void dispatchAccessibilityFeaturesChanged() { void dispatchAccessibilityFeaturesChanged() {
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeAccessibilityFeatures(); observer.didChangeAccessibilityFeatures();
} }
} }
...@@ -720,6 +721,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -720,6 +721,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// This method exposes the `popRoute` notification from /// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
@protected @protected
@visibleForTesting
Future<void> handlePopRoute() async { Future<void> handlePopRoute() async {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
if (await observer.didPopRoute()) { if (await observer.didPopRoute()) {
...@@ -741,6 +743,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -741,6 +743,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// [SystemChannels.navigation]. /// [SystemChannels.navigation].
@protected @protected
@mustCallSuper @mustCallSuper
@visibleForTesting
Future<void> handlePushRoute(String route) async { Future<void> handlePushRoute(String route) async {
final RouteInformation routeInformation = RouteInformation(uri: Uri.parse(route)); final RouteInformation routeInformation = RouteInformation(uri: Uri.parse(route));
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
...@@ -777,7 +780,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -777,7 +780,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
void handleAppLifecycleStateChanged(AppLifecycleState state) { void handleAppLifecycleStateChanged(AppLifecycleState state) {
super.handleAppLifecycleStateChanged(state); super.handleAppLifecycleStateChanged(state);
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeAppLifecycleState(state); observer.didChangeAppLifecycleState(state);
} }
} }
...@@ -785,7 +788,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -785,7 +788,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
void handleMemoryPressure() { void handleMemoryPressure() {
super.handleMemoryPressure(); super.handleMemoryPressure();
for (final WidgetsBindingObserver observer in _observers) { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didHaveMemoryPressure(); observer.didHaveMemoryPressure();
} }
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -45,6 +47,94 @@ class PushRouteInformationObserver with WidgetsBindingObserver { ...@@ -45,6 +47,94 @@ class PushRouteInformationObserver with WidgetsBindingObserver {
} }
} }
// Implements to make sure all methods get coverage.
class RentrantObserver implements WidgetsBindingObserver {
RentrantObserver() {
WidgetsBinding.instance.addObserver(this);
}
bool active = true;
int removeSelf() {
active = false;
int count = 0;
while (WidgetsBinding.instance.removeObserver(this)) {
count += 1;
}
return count;
}
@override
void didChangeAccessibilityFeatures() {
assert(active);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
assert(active);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeLocales(List<Locale>? locales) {
assert(active);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeMetrics() {
assert(active);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangePlatformBrightness() {
assert(active);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeTextScaleFactor() {
assert(active);
WidgetsBinding.instance.addObserver(this);
}
@override
void didHaveMemoryPressure() {
assert(active);
WidgetsBinding.instance.addObserver(this);
}
@override
Future<bool> didPopRoute() {
assert(active);
WidgetsBinding.instance.addObserver(this);
return Future<bool>.value(true);
}
@override
Future<bool> didPushRoute(String route) {
assert(active);
WidgetsBinding.instance.addObserver(this);
return Future<bool>.value(true);
}
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
assert(active);
WidgetsBinding.instance.addObserver(this);
return Future<bool>.value(true);
}
@override
Future<AppExitResponse> didRequestAppExit() {
assert(active);
WidgetsBinding.instance.addObserver(this);
return Future<AppExitResponse>.value(AppExitResponse.exit);
}
}
void main() { void main() {
Future<void> setAppLifeCycleState(AppLifecycleState state) async { Future<void> setAppLifeCycleState(AppLifecycleState state) async {
final ByteData? message = final ByteData? message =
...@@ -53,6 +143,23 @@ void main() { ...@@ -53,6 +143,23 @@ void main() {
.handlePlatformMessage('flutter/lifecycle', message, (_) { }); .handlePlatformMessage('flutter/lifecycle', message, (_) { });
} }
testWidgets('Rentrant observer callbacks do not result in exceptions', (WidgetTester tester) async {
final RentrantObserver observer = RentrantObserver();
WidgetsBinding.instance.handleAccessibilityFeaturesChanged();
WidgetsBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
WidgetsBinding.instance.handleLocaleChanged();
WidgetsBinding.instance.handleMetricsChanged();
WidgetsBinding.instance.handlePlatformBrightnessChanged();
WidgetsBinding.instance.handleTextScaleFactorChanged();
WidgetsBinding.instance.handleMemoryPressure();
WidgetsBinding.instance.handlePopRoute();
WidgetsBinding.instance.handlePushRoute('/');
WidgetsBinding.instance.handleRequestAppExit();
await tester.idle();
expect(observer.removeSelf(), greaterThan(1));
expect(observer.removeSelf(), 0);
});
testWidgets('didHaveMemoryPressure callback', (WidgetTester tester) async { testWidgets('didHaveMemoryPressure callback', (WidgetTester tester) async {
final MemoryPressureObserver observer = MemoryPressureObserver(); final MemoryPressureObserver observer = MemoryPressureObserver();
WidgetsBinding.instance.addObserver(observer); WidgetsBinding.instance.addObserver(observer);
...@@ -118,6 +225,7 @@ void main() { ...@@ -118,6 +225,7 @@ void main() {
observer.accumulatedStates.clear(); observer.accumulatedStates.clear();
await expectLater(() async => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError); await expectLater(() async => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError);
WidgetsBinding.instance.removeObserver(observer);
}); });
testWidgets('didPushRoute callback', (WidgetTester tester) async { testWidgets('didPushRoute callback', (WidgetTester tester) async {
......
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