diff --git a/packages/flutter/lib/src/cupertino/app.dart b/packages/flutter/lib/src/cupertino/app.dart index 4e7285110f..b305e75c54 100644 --- a/packages/flutter/lib/src/cupertino/app.dart +++ b/packages/flutter/lib/src/cupertino/app.dart @@ -170,6 +170,7 @@ class CupertinoApp extends StatefulWidget { this.actions, this.restorationScopeId, this.scrollBehavior, + this.useInheritedMediaQuery = false, }) : assert(routes != null), assert(navigatorObservers != null), assert(title != null), @@ -210,6 +211,7 @@ class CupertinoApp extends StatefulWidget { this.actions, this.restorationScopeId, this.scrollBehavior, + this.useInheritedMediaQuery = false, }) : assert(title != null), assert(showPerformanceOverlay != null), assert(checkerboardRasterCacheImages != null), @@ -407,6 +409,9 @@ class CupertinoApp extends StatefulWidget { /// in a subtree. final ScrollBehavior? scrollBehavior; + /// {@macro flutter.widgets.widgetsApp.useInheritedMediaQuery} + final bool useInheritedMediaQuery; + @override State createState() => _CupertinoAppState(); @@ -530,6 +535,7 @@ class _CupertinoAppState extends State { shortcuts: widget.shortcuts, actions: widget.actions, restorationScopeId: widget.restorationScopeId, + useInheritedMediaQuery: widget.useInheritedMediaQuery, ); } return WidgetsApp( @@ -564,6 +570,7 @@ class _CupertinoAppState extends State { shortcuts: widget.shortcuts, actions: widget.actions, restorationScopeId: widget.restorationScopeId, + useInheritedMediaQuery: widget.useInheritedMediaQuery, ); } diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 8a48799579..0e5ce4d7f4 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -200,6 +200,7 @@ class MaterialApp extends StatefulWidget { this.actions, this.restorationScopeId, this.scrollBehavior, + this.useInheritedMediaQuery = false, }) : assert(routes != null), assert(navigatorObservers != null), assert(title != null), @@ -247,6 +248,7 @@ class MaterialApp extends StatefulWidget { this.actions, this.restorationScopeId, this.scrollBehavior, + this.useInheritedMediaQuery = false, }) : assert(routeInformationParser != null), assert(routerDelegate != null), assert(title != null), @@ -665,6 +667,9 @@ class MaterialApp extends StatefulWidget { /// * final bool debugShowMaterialGrid; + /// {@macro flutter.widgets.widgetsApp.useInheritedMediaQuery} + final bool useInheritedMediaQuery; + @override State createState() => _MaterialAppState(); @@ -858,6 +863,7 @@ class _MaterialAppState extends State { shortcuts: widget.shortcuts, actions: widget.actions, restorationScopeId: widget.restorationScopeId, + useInheritedMediaQuery: widget.useInheritedMediaQuery, ); } @@ -893,6 +899,7 @@ class _MaterialAppState extends State { shortcuts: widget.shortcuts, actions: widget.actions, restorationScopeId: widget.restorationScopeId, + useInheritedMediaQuery: widget.useInheritedMediaQuery, ); } diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index de53a989dd..56c5ca3720 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -236,6 +236,9 @@ typedef InitialRouteListFactory = List> Function(String initialRo /// It is used by both [MaterialApp] and [CupertinoApp] to implement base /// functionality for an app. /// +/// Builds a [MediaQuery] using [MediaQuery.fromWindow]. To use an inherited +/// [MediaQuery] instead, set [useInheritedMediaQuery] to true. +/// /// Find references to many of the widgets that [WidgetsApp] wraps in the "See /// also" section. /// @@ -247,6 +250,8 @@ typedef InitialRouteListFactory = List> Function(String initialRo /// without an explicit style. /// * [MediaQuery], which establishes a subtree in which media queries resolve /// to a [MediaQueryData]. +/// * [MediaQuery.fromWindow], which builds a [MediaQuery] with data derived +/// from [WidgetsBinding.window]. /// * [Localizations], which defines the [Locale] for its `child`. /// * [Title], a widget that describes this app in the operating system. /// * [Navigator], a widget that manages a set of child widgets with a stack @@ -327,6 +332,7 @@ class WidgetsApp extends StatefulWidget { this.shortcuts, this.actions, this.restorationScopeId, + this.useInheritedMediaQuery = false, }) : assert(navigatorObservers != null), assert(routes != null), assert( @@ -423,6 +429,7 @@ class WidgetsApp extends StatefulWidget { this.shortcuts, this.actions, this.restorationScopeId, + this.useInheritedMediaQuery = false, }) : assert( routeInformationParser != null && routerDelegate != null, @@ -1111,6 +1118,14 @@ class WidgetsApp extends StatefulWidget { /// {@endtemplate} final String? restorationScopeId; + /// {@template flutter.widgets.widgetsApp.useInheritedMediaQuery} + /// If true, an inherited MediaQuery will be used. If one is not available, + /// or this is false, one will be built from the window. + /// + /// Cannot be null, defaults to false. + /// {@endtemplate} + final bool useInheritedMediaQuery; + /// If true, forces the performance overlay to be visible in all instances. /// /// Used by the `showPerformanceOverlay` observatory extension. @@ -1635,6 +1650,19 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { assert(_debugCheckLocalizations(appLocale)); + Widget child = Localizations( + locale: appLocale, + delegates: _localizationsDelegates.toList(), + child: title, + ); + + final MediaQueryData? data = MediaQuery.maybeOf(context); + if (!widget.useInheritedMediaQuery || data == null) { + child = MediaQuery.fromWindow( + child: child, + ); + } + return RootRestorationScope( restorationId: widget.restorationScopeId, child: Shortcuts( @@ -1648,13 +1676,7 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { child: DefaultTextEditingActions( child: FocusTraversalGroup( policy: ReadingOrderTraversalPolicy(), - child: _MediaQueryFromWindow( - child: Localizations( - locale: appLocale, - delegates: _localizationsDelegates.toList(), - child: title, - ), - ), + child: child, ), ), ), @@ -1663,81 +1685,3 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { ); } } - -/// 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, required 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) { - MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window); - if (!kReleaseMode) { - data = data.copyWith(platformBrightness: debugBrightnessOverride); - } - return MediaQuery( - data: data, - child: widget.child, - ); - } - - @override - void dispose() { - WidgetsBinding.instance!.removeObserver(this); - super.dispose(); - } -} diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index ffbdf8f9cc..92423f52f3 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -9,6 +9,7 @@ import 'dart:ui' show Brightness; import 'package:flutter/foundation.dart'; import 'basic.dart'; +import 'binding.dart'; import 'debug.dart'; import 'framework.dart'; @@ -787,6 +788,28 @@ class MediaQuery extends InheritedWidget { ); } + /// Provides a [MediaQuery] which is built and updated using the latest + /// [WidgetsBinding.window] values. + /// + /// The [MediaQuery] is wrapped in a separate widget to ensure that only it + /// and its dependents are updated when `window` changes, instead of + /// rebuilding the whole widget tree. + /// + /// This should be inserted into the widget tree when the [MediaQuery] view + /// padding is consumed by a widget in such a way that the view padding is no + /// longer exposed to the widget's descendants or siblings. + /// + /// The [child] argument is required and must not be null. + static Widget fromWindow({ + Key? key, + required Widget child, + }) { + return _MediaQueryFromWindow( + key: key, + child: child, + ); + } + /// Contains information about the current media. /// /// For example, the [MediaQueryData.size] property contains the width and @@ -922,3 +945,100 @@ enum NavigationMode { /// focus (although they remain disabled) when traversed. directional, } + +/// Provides a [MediaQuery] which is built and updated using the latest +/// [WidgetsBinding.window] values. +/// +/// Receives `window` updates by listening to [WidgetsBinding]. +/// +/// The standalone widget ensures that it rebuilds **only** [MediaQuery] and +/// its dependents when `window` changes, instead of rebuilding the entire +/// widget tree. +/// +/// It is used by [WidgetsApp] if no other [MediaQuery] is available above it. +/// +/// See also: +/// +/// * [MediaQuery], which establishes a subtree in which media queries resolve +/// to a [MediaQueryData]. +class _MediaQueryFromWindow extends StatefulWidget { + /// Creates a [_MediaQueryFromWindow] that provides a [MediaQuery] to its + /// descendants using the `window` to keep [MediaQueryData] up to date. + /// + /// The [child] must not be null. + const _MediaQueryFromWindow({ + Key? key, + required this.child, + }) : super(key: key); + + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget child; + + @override + State<_MediaQueryFromWindow> createState() => _MediaQueryFromWindowState(); +} + +class _MediaQueryFromWindowState 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) { + MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window); + if (!kReleaseMode) { + data = data.copyWith(platformBrightness: debugBrightnessOverride); + } + return MediaQuery( + data: data, + child: widget.child, + ); + } + + @override + void dispose() { + WidgetsBinding.instance!.removeObserver(this); + super.dispose(); + } +} diff --git a/packages/flutter/test/cupertino/app_test.dart b/packages/flutter/test/cupertino/app_test.dart index fa987ef137..7942f2221a 100644 --- a/packages/flutter/test/cupertino/app_test.dart +++ b/packages/flutter/test/cupertino/app_test.dart @@ -208,6 +208,26 @@ void main() { expect(scrollBehavior.runtimeType, MockScrollBehavior); expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); }); + + testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { + late BuildContext capturedContext; + final UniqueKey uniqueKey = UniqueKey(); + await tester.pumpWidget( + MediaQuery( + key: uniqueKey, + data: const MediaQueryData(), + child: CupertinoApp( + useInheritedMediaQuery: true, + builder: (BuildContext context, Widget? child) { + capturedContext = context; + return const Placeholder(); + }, + color: const Color(0xFF123456), + ), + ), + ); + expect(capturedContext.dependOnInheritedWidgetOfExactType()?.key, uniqueKey); + }); } class MockScrollBehavior extends ScrollBehavior { diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 6828cdb9c4..80b042ba92 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -1069,6 +1069,26 @@ void main() { expect(scrollBehavior.runtimeType, MockScrollBehavior); expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); }); + + testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { + late BuildContext capturedContext; + final UniqueKey uniqueKey = UniqueKey(); + await tester.pumpWidget( + MediaQuery( + key: uniqueKey, + data: const MediaQueryData(), + child: MaterialApp( + useInheritedMediaQuery: true, + builder: (BuildContext context, Widget? child) { + capturedContext = context; + return const Placeholder(); + }, + color: const Color(0xFF123456), + ), + ), + ); + expect(capturedContext.dependOnInheritedWidgetOfExactType()?.key, uniqueKey); + }); } class MockScrollBehavior extends ScrollBehavior { diff --git a/packages/flutter/test/widgets/app_test.dart b/packages/flutter/test/widgets/app_test.dart index 63470ba880..f8c03ebf84 100644 --- a/packages/flutter/test/widgets/app_test.dart +++ b/packages/flutter/test/widgets/app_test.dart @@ -455,6 +455,56 @@ void main() { const Locale('zh'), ); }); + + testWidgets('WidgetsApp creates a MediaQuery if `useInheritedMediaQuery` is set to false', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + WidgetsApp( + useInheritedMediaQuery: false, + builder: (BuildContext context, Widget? child) { + capturedContext = context; + return const Placeholder(); + }, + color: const Color(0xFF123456), + ), + ); + expect(MediaQuery.of(capturedContext), isNotNull); + }); + + testWidgets('WidgetsApp does not create MediaQuery if `useInheritedMediaQuery` is set to true and one is available', (WidgetTester tester) async { + late BuildContext capturedContext; + final UniqueKey uniqueKey = UniqueKey(); + await tester.pumpWidget( + MediaQuery( + key: uniqueKey, + data: const MediaQueryData(), + child: WidgetsApp( + useInheritedMediaQuery: true, + builder: (BuildContext context, Widget? child) { + capturedContext = context; + return const Placeholder(); + }, + color: const Color(0xFF123456), + ), + ), + ); + expect(capturedContext.dependOnInheritedWidgetOfExactType()?.key, uniqueKey); + }); + + testWidgets('WidgetsApp does create a MediaQuery if `useInheritedMediaQuery` is set to true and none is available', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + WidgetsApp( + useInheritedMediaQuery: true, + builder: (BuildContext context, Widget? child) { + capturedContext = context; + return const Placeholder(); + }, + color: const Color(0xFF123456), + ), + ); + expect(MediaQuery.of(capturedContext), isNotNull); + }); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation); diff --git a/packages/flutter/test/widgets/media_query_test.dart b/packages/flutter/test/widgets/media_query_test.dart index 7b719491ea..d5608c1137 100644 --- a/packages/flutter/test/widgets/media_query_test.dart +++ b/packages/flutter/test/widgets/media_query_test.dart @@ -627,4 +627,49 @@ void main() { expect(outsideBoldTextOverride, false); expect(insideBoldTextOverride, true); }); + + testWidgets('MediaQuery.fromWindow creates a MediaQuery', (WidgetTester tester) async { + bool hasMediaQueryAsParentOutside = false; + bool hasMediaQueryAsParentInside = false; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + hasMediaQueryAsParentOutside = + context.findAncestorWidgetOfExactType() != null; + return MediaQuery.fromWindow( + child: Builder( + builder: (BuildContext context) { + hasMediaQueryAsParentInside = + context.findAncestorWidgetOfExactType() != null; + return const SizedBox(); + }, + ), + ); + }, + ), + ); + + expect(hasMediaQueryAsParentOutside, false); + expect(hasMediaQueryAsParentInside, true); + }); + + testWidgets('MediaQueryData.fromWindow is created using window values', (WidgetTester tester) + async { + final MediaQueryData windowData = MediaQueryData.fromWindow(WidgetsBinding.instance!.window); + late MediaQueryData fromWindowData; + + await tester.pumpWidget( + MediaQuery.fromWindow( + child: Builder( + builder: (BuildContext context) { + fromWindowData = MediaQuery.of(context); + return const SizedBox(); + }, + ), + ), + ); + + expect(windowData, equals(fromWindowData)); + }); }