Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Override MediaQuery for WidgetsApp #81295

Merged
merged 17 commits into from Jun 23, 2021
Merged
104 changes: 19 additions & 85 deletions packages/flutter/lib/src/widgets/app.dart
Expand Up @@ -236,6 +236,9 @@ typedef InitialRouteListFactory = List<Route<dynamic>> Function(String initialRo
/// It is used by both [MaterialApp] and [CupertinoApp] to implement base
/// functionality for an app.
///
/// If a [MediaQuery] is not available above [WidgetsApp], a [MediaQuery] is
/// built using [MediaQuery.fromWindow].
///
/// Find references to many of the widgets that [WidgetsApp] wraps in the "See
/// also" section.
///
Expand All @@ -247,6 +250,8 @@ typedef InitialRouteListFactory = List<Route<dynamic>> 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
Expand Down Expand Up @@ -1635,6 +1640,19 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {

assert(_debugCheckLocalizations(appLocale));

Widget child = Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
);

final MediaQueryData? data = MediaQuery.maybeOf(context);
if (data == null) {
child = MediaQuery.fromWindow(
child: child,
);
}

return RootRestorationScope(
restorationId: widget.restorationScopeId,
child: Shortcuts(
Expand All @@ -1648,13 +1666,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
child: DefaultTextEditingActions(
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: _MediaQueryFromWindow(
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
),
),
child: child,
),
),
),
Expand All @@ -1663,81 +1675,3 @@ class _WidgetsAppState extends State<WidgetsApp> 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();
}
}
124 changes: 124 additions & 0 deletions packages/flutter/lib/src/widgets/media_query.dart
Expand Up @@ -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';

Expand Down Expand Up @@ -787,6 +788,32 @@ class MediaQuery extends InheritedWidget {
);
}

/// Creates a [_MediaQueryFromWindow] which builds and updates a
/// [MediaQuery] 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.
///
/// See also:
///
/// * [_MediaQueryFromWindow], the underlying widget.
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
Expand Down Expand Up @@ -922,3 +949,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();
}
}
33 changes: 33 additions & 0 deletions packages/flutter/test/widgets/app_test.dart
Expand Up @@ -455,6 +455,39 @@ void main() {
const Locale('zh'),
);
});

testWidgets('WidgetsApp creates a MediaQuery', (WidgetTester tester) async {
late BuildContext capturedContext;
await tester.pumpWidget(
WidgetsApp(
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 one is already available', (WidgetTester tester) async {
late BuildContext capturedContext;
final UniqueKey uniqueKey = UniqueKey();
await tester.pumpWidget(
MediaQuery(
key: uniqueKey,
data: const MediaQueryData(),
child: WidgetsApp(
builder: (BuildContext context, Widget? child) {
capturedContext = context;
return const Placeholder();
},
color: const Color(0xFF123456),
),
),
);
expect(capturedContext.dependOnInheritedWidgetOfExactType<MediaQuery>()?.key, uniqueKey);
});
}

typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
Expand Down
45 changes: 45 additions & 0 deletions packages/flutter/test/widgets/media_query_test.dart
Expand Up @@ -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<MediaQuery>() != null;
return MediaQuery.fromWindow(
child: Builder(
builder: (BuildContext context) {
hasMediaQueryAsParentInside =
context.findAncestorWidgetOfExactType<MediaQuery>() != 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));
});
}