Skip to content

Commit a34e419

Browse files
authored
Inject current FlutterView into tree and make available via View.of(context) (#116924)
* enable View.of * tests * ++ * greg review * rewording * hide view from public
1 parent 86b62a3 commit a34e419

21 files changed

+352
-108
lines changed

packages/flutter/lib/src/widgets/binding.dart

+20-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import 'framework.dart';
1919
import 'platform_menu_bar.dart';
2020
import 'router.dart';
2121
import 'service_extensions.dart';
22+
import 'view.dart';
2223
import 'widget_inspector.dart';
2324

2425
export 'dart:ui' show AppLifecycleState, Locale;
@@ -896,6 +897,22 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
896897
@override
897898
bool get framesEnabled => super.framesEnabled && _readyToProduceFrames;
898899

900+
/// Used by [runApp] to wrap the provided `rootWidget` in the default [View].
901+
///
902+
/// The [View] determines into what [FlutterView] the app is rendered into.
903+
/// For backwards-compatibility reasons, this method currently chooses
904+
/// [window] (which is a [FlutterView]) as the rendering target. This will
905+
/// change in a future version of Flutter.
906+
///
907+
/// The `rootWidget` widget provided to this method must not already be
908+
/// wrapped in a [View].
909+
Widget wrapWithDefaultView(Widget rootWidget) {
910+
return View(
911+
view: window,
912+
child: rootWidget,
913+
);
914+
}
915+
899916
/// Schedules a [Timer] for attaching the root widget.
900917
///
901918
/// This is called by [runApp] to configure the widget tree. Consider using
@@ -1014,8 +1031,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
10141031
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
10151032
/// ensure the widget, element, and render trees are all built.
10161033
void runApp(Widget app) {
1017-
WidgetsFlutterBinding.ensureInitialized()
1018-
..scheduleAttachRootWidget(app)
1034+
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
1035+
binding
1036+
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
10191037
..scheduleWarmUpFrame();
10201038
}
10211039

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ui' show FlutterView;
6+
7+
import 'framework.dart';
8+
import 'lookup_boundary.dart';
9+
10+
/// Injects a [FlutterView] into the tree and makes it available to descendants
11+
/// within the same [LookupBoundary] via [View.of] and [View.maybeOf].
12+
///
13+
/// In a future version of Flutter, the functionality of this widget will be
14+
/// extended to actually bootstrap the render tree that is going to be rendered
15+
/// into the provided [view]. This will enable rendering content into multiple
16+
/// [FlutterView]s from a single widget tree.
17+
///
18+
/// Each [FlutterView] can be associated with at most one [View] widget in the
19+
/// widget tree. Two or more [View] widgets configured with the same
20+
/// [FlutterView] must never exist within the same widget tree at the same time.
21+
/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives
22+
/// its identity from the [view] provided to this widget.
23+
class View extends InheritedWidget {
24+
/// Injects the provided [view] into the widget tree.
25+
View({required this.view, required super.child}) : super(key: GlobalObjectKey(view));
26+
27+
/// The [FlutterView] to be injected into the tree.
28+
final FlutterView view;
29+
30+
@override
31+
bool updateShouldNotify(View oldWidget) => view != oldWidget.view;
32+
33+
/// Returns the [FlutterView] that the provided `context` will render into.
34+
///
35+
/// Returns null if the `context` is not associated with a [FlutterView].
36+
///
37+
/// The method creates a dependency on the `context`, which will be informed
38+
/// when the identity of the [FlutterView] changes (i.e. the `context` is
39+
/// moved to render into a different [FlutterView] then before). The context
40+
/// will not be informed when the properties on the [FlutterView] itself
41+
/// change their values. To access the property values of a [FlutterView] it
42+
/// is best practise to use [MediaQuery.maybeOf] instead, which will ensure
43+
/// that the `context` is informed when the view properties change.
44+
///
45+
/// See also:
46+
///
47+
/// * [View.of], which throws instead of returning null if no [FlutterView]
48+
/// is found.
49+
static FlutterView? maybeOf(BuildContext context) {
50+
return LookupBoundary.dependOnInheritedWidgetOfExactType<View>(context)?.view;
51+
}
52+
53+
/// Returns the [FlutterView] that the provided `context` will render into.
54+
///
55+
/// Throws if the `context` is not associated with a [FlutterView].
56+
///
57+
/// The method creates a dependency on the `context`, which will be informed
58+
/// when the identity of the [FlutterView] changes (i.e. the `context` is
59+
/// moved to render into a different [FlutterView] then before). The context
60+
/// will not be informed when the properties on the [FlutterView] itself
61+
/// change their values. To access the property values of a [FlutterView] it
62+
/// is best practise to use [MediaQuery.of] instead, which will ensure that
63+
/// the `context` is informed when the view properties change.
64+
///
65+
/// See also:
66+
///
67+
/// * [View.maybeOf], which throws instead of returning null if no
68+
/// [FlutterView] is found.
69+
static FlutterView of(BuildContext context) {
70+
final FlutterView? result = maybeOf(context);
71+
assert(() {
72+
if (result == null) {
73+
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<View>(context);
74+
final List<DiagnosticsNode> information = <DiagnosticsNode>[
75+
if (hiddenByBoundary) ...<DiagnosticsNode>[
76+
ErrorSummary('View.of() was called with a context that does not have access to a View widget.'),
77+
ErrorDescription('The context provided to View.of() does have a View widget ancestor, but it is hidden by a LookupBoundary.'),
78+
] else ...<DiagnosticsNode>[
79+
ErrorSummary('View.of() was called with a context that does not contain a View widget.'),
80+
ErrorDescription('No View widget ancestor could be found starting from the context that was passed to View.of().'),
81+
],
82+
ErrorDescription(
83+
'The context used was:\n'
84+
' $context',
85+
),
86+
ErrorHint('This usually means that the provided context is not associated with a View.'),
87+
];
88+
throw FlutterError.fromParts(information);
89+
}
90+
return true;
91+
}());
92+
return result!;
93+
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// Placeholder to be used in a future version of Flutter.
6+
abstract class Window {
7+
const Window._();
8+
}

packages/flutter/lib/widgets.dart

+3
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,11 @@ export 'src/widgets/transitions.dart';
148148
export 'src/widgets/tween_animation_builder.dart';
149149
export 'src/widgets/unique_widget.dart';
150150
export 'src/widgets/value_listenable_builder.dart';
151+
// TODO(goderbauer): Enable once clean-up in google3 is done.
152+
// export 'src/widgets/view.dart';
151153
export 'src/widgets/viewport.dart';
152154
export 'src/widgets/visibility.dart';
153155
export 'src/widgets/widget_inspector.dart';
154156
export 'src/widgets/widget_span.dart';
155157
export 'src/widgets/will_pop_scope.dart';
158+
export 'src/widgets/window.dart';

packages/flutter/test/material/debug_test.dart

+12-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
88

99
void main() {
1010
testWidgets('debugCheckHasMaterial control test', (WidgetTester tester) async {
11-
await tester.pumpWidget(const Chip(label: Text('label')));
11+
await tester.pumpWidget(const Center(child: Chip(label: Text('label'))));
1212
final dynamic exception = tester.takeException();
1313
expect(exception, isFlutterError);
1414
final FlutterError error = exception as FlutterError;
@@ -25,7 +25,7 @@ void main() {
2525
expect(error.diagnostics[3], isA<DiagnosticsProperty<Element>>());
2626
expect(error.diagnostics[4], isA<DiagnosticsBlock>());
2727
expect(
28-
error.toStringDeep(),
28+
error.toStringDeep(), startsWith(
2929
'FlutterError\n'
3030
' No Material widget found.\n'
3131
' Chip widgets require a Material widget ancestor within the\n'
@@ -42,12 +42,13 @@ void main() {
4242
' The specific widget that could not find a Material ancestor was:\n'
4343
' Chip\n'
4444
' The ancestors of this widget were:\n'
45-
' [root]\n',
46-
);
45+
' Center\n'
46+
// End of ancestor chain omitted, not relevant for test.
47+
));
4748
});
4849

4950
testWidgets('debugCheckHasMaterialLocalizations control test', (WidgetTester tester) async {
50-
await tester.pumpWidget(const BackButton());
51+
await tester.pumpWidget(const Center(child: BackButton()));
5152
final dynamic exception = tester.takeException();
5253
expect(exception, isFlutterError);
5354
final FlutterError error = exception as FlutterError;
@@ -64,7 +65,7 @@ void main() {
6465
expect(error.diagnostics[4], isA<DiagnosticsProperty<Element>>());
6566
expect(error.diagnostics[5], isA<DiagnosticsBlock>());
6667
expect(
67-
error.toStringDeep(),
68+
error.toStringDeep(), startsWith(
6869
'FlutterError\n'
6970
' No MaterialLocalizations found.\n'
7071
' BackButton widgets require MaterialLocalizations to be provided\n'
@@ -78,8 +79,9 @@ void main() {
7879
' ancestor was:\n'
7980
' BackButton\n'
8081
' The ancestors of this widget were:\n'
81-
' [root]\n',
82-
);
82+
' Center\n'
83+
// End of ancestor chain omitted, not relevant for test.
84+
));
8385
});
8486

8587
testWidgets('debugCheckHasScaffold control test', (WidgetTester tester) async {
@@ -233,6 +235,7 @@ void main() {
233235
' HeroControllerScope\n'
234236
' ScrollConfiguration\n'
235237
' MaterialApp\n'
238+
' View-[GlobalObjectKey TestWindow#00000]\n'
236239
' [root]\n'
237240
' Typically, the Scaffold widget is introduced by the MaterialApp\n'
238241
' or WidgetsApp widget at the top of your application widget tree.\n'
@@ -377,6 +380,7 @@ void main() {
377380
' Scaffold-[LabeledGlobalKey<ScaffoldState>#00000]\n'
378381
' MediaQuery\n'
379382
' Directionality\n'
383+
' View-[GlobalObjectKey TestWindow#00000]\n'
380384
' [root]\n'
381385
' Typically, the ScaffoldMessenger widget is introduced by the\n'
382386
' MaterialApp at the top of your application widget tree.\n'

packages/flutter/test/material/scaffold_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -2458,6 +2458,7 @@ void main() {
24582458
' Scaffold\n'
24592459
' MediaQuery\n'
24602460
' Directionality\n'
2461+
' View-[GlobalObjectKey TestWindow#e6136]\n'
24612462
' [root]\n'
24622463
' Typically, the ScaffoldMessenger widget is introduced by the\n'
24632464
' MaterialApp at the top of your application widget tree.\n',

packages/flutter/test/material/text_field_test.dart

-1
Original file line numberDiff line numberDiff line change
@@ -7390,7 +7390,6 @@ void main() {
73907390
final dynamic exception = tester.takeException();
73917391
expect(exception, isFlutterError);
73927392
expect(exception.toString(), startsWith('No Material widget found.'));
7393-
expect(exception.toString(), endsWith(':\n $textField\nThe ancestors of this widget were:\n [root]'));
73947393
});
73957394

73967395
testWidgets('TextField loses focus when disabled', (WidgetTester tester) async {

0 commit comments

Comments
 (0)