diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 9b1e990d24..5c5d0678be 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -706,8 +706,7 @@ class WidgetsApp extends StatefulWidget { _WidgetsAppState createState() => _WidgetsAppState(); } -class _WidgetsAppState extends State implements WidgetsBindingObserver { - +class _WidgetsAppState extends State with WidgetsBindingObserver { // STATE LIFECYCLE @override @@ -731,13 +730,6 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv super.dispose(); } - @override - void didChangeAppLifecycleState(AppLifecycleState state) { } - - @override - void didHaveMemoryPressure() { } - - // NAVIGATOR GlobalKey _navigator; @@ -995,46 +987,6 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv yield DefaultWidgetsLocalizations.delegate; } - // ACCESSIBILITY - - @override - void didChangeAccessibilityFeatures() { - setState(() { - // The properties of window have changed. We use them in our build - // function, so we need setState(), but we don't cache anything locally. - }); - } - - - // METRICS - - @override - void didChangeMetrics() { - setState(() { - // The properties of window have changed. We use them in our build - // function, so we need setState(), but we don't cache anything locally. - }); - } - - @override - void didChangeTextScaleFactor() { - setState(() { - // The textScaleFactor property of window has changed. We reference - // window in our build function, so we need to call setState(), but - // we don't need to cache anything locally. - }); - } - - // RENDERING - @override - void didChangePlatformBrightness() { - setState(() { - // The platformBrightness property of window has changed. We reference - // window in our build function, so we need to call setState(), but - // we don't need to cache anything locally. - }); - } - // BUILDER bool _debugCheckLocalizations(Locale appLocale) { @@ -1201,8 +1153,7 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv }, child: DefaultFocusTraversal( policy: ReadingOrderTraversalPolicy(), - child: MediaQuery( - data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), + child: _MediaQueryFromWindow( child: Localizations( locale: appLocale, delegates: _localizationsDelegates.toList(), @@ -1213,3 +1164,77 @@ class _WidgetsAppState extends State implements WidgetsBindingObserv ); } } + +/// Builds [MediaQuery] from `window` by listening to [WidgetsBinding]. +/// +/// It is performed in a standalone widget to rebuild **only** [MediaQuery] and +/// its dependents when `window` changes, instead of rebuilding the entire widget tree. +class _MediaQueryFromWindow extends StatefulWidget { + const _MediaQueryFromWindow({Key key, this.child}) : super(key: key); + + final Widget child; + + @override + _MediaQueryFromWindowsState createState() => _MediaQueryFromWindowsState(); +} + +class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + // ACCESSIBILITY + + @override + void didChangeAccessibilityFeatures() { + setState(() { + // The properties of window have changed. We use them in our build + // function, so we need setState(), but we don't cache anything locally. + }); + } + + // METRICS + + @override + void didChangeMetrics() { + setState(() { + // The properties of window have changed. We use them in our build + // function, so we need setState(), but we don't cache anything locally. + }); + } + + @override + void didChangeTextScaleFactor() { + setState(() { + // The textScaleFactor property of window has changed. We reference + // window in our build function, so we need to call setState(), but + // we don't need to cache anything locally. + }); + } + + // RENDERING + @override + void didChangePlatformBrightness() { + setState(() { + // The platformBrightness property of window has changed. We reference + // window in our build function, so we need to call setState(), but + // we don't need to cache anything locally. + }); + } + + @override + Widget build(BuildContext context) { + return MediaQuery( + data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), + child: widget.child, + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } +} diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 3ed7b9b5a9..82afb60424 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -2,9 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:mockito/mockito.dart'; class StateMarker extends StatefulWidget { const StateMarker({ Key key, this.child }) : super(key: key); @@ -392,6 +394,67 @@ void main() { ); }); + testWidgets("WidgetsApp don't rebuild routes when MediaQuery updates", (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/37878 + int routeBuildCount = 0; + int dependentBuildCount = 0; + + await tester.pumpWidget(WidgetsApp( + color: const Color.fromARGB(255, 255, 255, 255), + onGenerateRoute: (_) { + return PageRouteBuilder(pageBuilder: (_, __, ___) { + routeBuildCount++; + return Builder( + builder: (BuildContext context) { + dependentBuildCount++; + MediaQuery.of(context); + return Container(); + }, + ); + }); + }, + )); + + expect(routeBuildCount, equals(1)); + expect(dependentBuildCount, equals(1)); + + // didChangeMetrics + tester.binding.window.physicalSizeTestValue = const Size(42, 42); + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + + await tester.pump(); + + expect(routeBuildCount, equals(1)); + expect(dependentBuildCount, equals(2)); + + // didChangeTextScaleFactor + tester.binding.window.textScaleFactorTestValue = 42; + addTearDown(tester.binding.window.clearTextScaleFactorTestValue); + + await tester.pump(); + + expect(routeBuildCount, equals(1)); + expect(dependentBuildCount, equals(3)); + + // didChangePlatformBrightness + tester.binding.window.platformBrightnessTestValue = Brightness.dark; + addTearDown(tester.binding.window.clearPlatformBrightnessTestValue); + + await tester.pump(); + + expect(routeBuildCount, equals(1)); + expect(dependentBuildCount, equals(4)); + + // didChangeAccessibilityFeatures + tester.binding.window.accessibilityFeaturesTestValue = MockAccessibilityFeature(); + addTearDown(tester.binding.window.clearAccessibilityFeaturesTestValue); + + await tester.pump(); + + expect(routeBuildCount, equals(1)); + expect(dependentBuildCount, equals(5)); + }); + testWidgets('Can get text scale from media query', (WidgetTester tester) async { double textScaleFactor; await tester.pumpWidget(MaterialApp( @@ -726,3 +789,5 @@ void main() { expect(themeAfterBrightnessChange.brightness, Brightness.dark); }); } + +class MockAccessibilityFeature extends Mock implements AccessibilityFeatures {}