diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 14235ac8a8660..7c8bf3b8df209 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -623,7 +623,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override Future handleRequestAppExit() async { bool didCancel = false; - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.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.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.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.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.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? locales) { - for (final WidgetsBindingObserver observer in _observers) { + for (final WidgetsBindingObserver observer in List.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.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 handlePopRoute() async { for (final WidgetsBindingObserver observer in List.of(_observers)) { if (await observer.didPopRoute()) { @@ -741,6 +743,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// [SystemChannels.navigation]. @protected @mustCallSuper + @visibleForTesting Future handlePushRoute(String route) async { final RouteInformation routeInformation = RouteInformation(uri: Uri.parse(route)); for (final WidgetsBindingObserver observer in List.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.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.of(_observers)) { observer.didHaveMemoryPressure(); } } diff --git a/packages/flutter/test/widgets/binding_test.dart b/packages/flutter/test/widgets/binding_test.dart index 928fc1fa948c2..b42df5fe09430 100644 --- a/packages/flutter/test/widgets/binding_test.dart +++ b/packages/flutter/test/widgets/binding_test.dart @@ -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? 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 didPopRoute() { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future.value(true); + } + + @override + Future didPushRoute(String route) { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future.value(true); + } + + @override + Future didPushRouteInformation(RouteInformation routeInformation) { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future.value(true); + } + + @override + Future didRequestAppExit() { + assert(active); + WidgetsBinding.instance.addObserver(this); + return Future.value(AppExitResponse.exit); + } +} + void main() { Future 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 {