Fix reentrancy with WidgetBindingObserver callbacks (#131774)

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

Fixes up callsites for WidgetsBindingObserver related callbacks.
This commit is contained in:
Dan Field 2023-08-02 10:29:50 -07:00 committed by GitHub
parent aff8ef13d4
commit b3f99ffe61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 120 additions and 9 deletions

View file

@ -623,7 +623,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override
Future<AppExitResponse> handleRequestAppExit() async {
bool didCancel = false;
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
if ((await observer.didRequestAppExit()) == AppExitResponse.cancel) {
didCancel = true;
// Don't early return. For the case where someone is just using the
@ -637,7 +637,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override
void handleMetricsChanged() {
super.handleMetricsChanged();
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeMetrics();
}
}
@ -645,7 +645,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override
void handleTextScaleFactorChanged() {
super.handleTextScaleFactorChanged();
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeTextScaleFactor();
}
}
@ -653,7 +653,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override
void handlePlatformBrightnessChanged() {
super.handlePlatformBrightnessChanged();
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangePlatformBrightness();
}
}
@ -661,7 +661,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override
void handleAccessibilityFeaturesChanged() {
super.handleAccessibilityFeaturesChanged();
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeAccessibilityFeatures();
}
}
@ -673,6 +673,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// See [dart:ui.PlatformDispatcher.onLocaleChanged].
@protected
@mustCallSuper
@visibleForTesting
void handleLocaleChanged() {
dispatchLocalesChanged(platformDispatcher.locales);
}
@ -686,7 +687,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@protected
@mustCallSuper
void dispatchLocalesChanged(List<Locale>? locales) {
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeLocales(locales);
}
}
@ -700,7 +701,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@protected
@mustCallSuper
void dispatchAccessibilityFeaturesChanged() {
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeAccessibilityFeatures();
}
}
@ -720,6 +721,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation].
@protected
@visibleForTesting
Future<void> handlePopRoute() async {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
if (await observer.didPopRoute()) {
@ -741,6 +743,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// [SystemChannels.navigation].
@protected
@mustCallSuper
@visibleForTesting
Future<void> handlePushRoute(String route) async {
final RouteInformation routeInformation = RouteInformation(uri: Uri.parse(route));
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
@ -777,7 +780,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override
void handleAppLifecycleStateChanged(AppLifecycleState state) {
super.handleAppLifecycleStateChanged(state);
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didChangeAppLifecycleState(state);
}
}
@ -785,7 +788,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override
void handleMemoryPressure() {
super.handleMemoryPressure();
for (final WidgetsBindingObserver observer in _observers) {
for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) {
observer.didHaveMemoryPressure();
}
}

View file

@ -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:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@ -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() {
Future<void> setAppLifeCycleState(AppLifecycleState state) async {
final ByteData? message =
@ -53,6 +143,23 @@ void main() {
.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 {
final MemoryPressureObserver observer = MemoryPressureObserver();
WidgetsBinding.instance.addObserver(observer);
@ -118,6 +225,7 @@ void main() {
observer.accumulatedStates.clear();
await expectLater(() async => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError);
WidgetsBinding.instance.removeObserver(observer);
});
testWidgets('didPushRoute callback', (WidgetTester tester) async {