Skip to content

Commit

Permalink
Reland "Inject current FlutterView into tree and make available via `…
Browse files Browse the repository at this point in the history
…View.of(context)` (#116924)" (#117244)

* Reland "Inject current `FlutterView` into tree and make available via `View.of(context)` (#116924)"

This reverts commit 9102f2f.

* remove window placeholder
  • Loading branch information
goderbauer committed Dec 17, 2022
1 parent 0a2a1d9 commit bf5fdb9
Show file tree
Hide file tree
Showing 21 changed files with 351 additions and 108 deletions.
22 changes: 20 additions & 2 deletions packages/flutter/lib/src/widgets/binding.dart
Expand Up @@ -19,6 +19,7 @@ import 'framework.dart';
import 'platform_menu_bar.dart';
import 'router.dart';
import 'service_extensions.dart';
import 'view.dart';
import 'widget_inspector.dart';

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

/// Used by [runApp] to wrap the provided `rootWidget` in the default [View].
///
/// The [View] determines into what [FlutterView] the app is rendered into.
/// For backwards-compatibility reasons, this method currently chooses
/// [window] (which is a [FlutterView]) as the rendering target. This will
/// change in a future version of Flutter.
///
/// The `rootWidget` widget provided to this method must not already be
/// wrapped in a [View].
Widget wrapWithDefaultView(Widget rootWidget) {
return View(
view: window,
child: rootWidget,
);
}

/// Schedules a [Timer] for attaching the root widget.
///
/// This is called by [runApp] to configure the widget tree. Consider using
Expand Down Expand Up @@ -1014,8 +1031,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
/// ensure the widget, element, and render trees are all built.
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}

Expand Down
94 changes: 94 additions & 0 deletions packages/flutter/lib/src/widgets/view.dart
@@ -0,0 +1,94 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' show FlutterView;

import 'framework.dart';
import 'lookup_boundary.dart';

/// Injects a [FlutterView] into the tree and makes it available to descendants
/// within the same [LookupBoundary] via [View.of] and [View.maybeOf].
///
/// In a future version of Flutter, the functionality of this widget will be
/// extended to actually bootstrap the render tree that is going to be rendered
/// into the provided [view]. This will enable rendering content into multiple
/// [FlutterView]s from a single widget tree.
///
/// Each [FlutterView] can be associated with at most one [View] widget in the
/// widget tree. Two or more [View] widgets configured with the same
/// [FlutterView] must never exist within the same widget tree at the same time.
/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives
/// its identity from the [view] provided to this widget.
class View extends InheritedWidget {
/// Injects the provided [view] into the widget tree.
View({required this.view, required super.child}) : super(key: GlobalObjectKey(view));

/// The [FlutterView] to be injected into the tree.
final FlutterView view;

@override
bool updateShouldNotify(View oldWidget) => view != oldWidget.view;

/// Returns the [FlutterView] that the provided `context` will render into.
///
/// Returns null if the `context` is not associated with a [FlutterView].
///
/// The method creates a dependency on the `context`, which will be informed
/// when the identity of the [FlutterView] changes (i.e. the `context` is
/// moved to render into a different [FlutterView] then before). The context
/// will not be informed when the properties on the [FlutterView] itself
/// change their values. To access the property values of a [FlutterView] it
/// is best practise to use [MediaQuery.maybeOf] instead, which will ensure
/// that the `context` is informed when the view properties change.
///
/// See also:
///
/// * [View.of], which throws instead of returning null if no [FlutterView]
/// is found.
static FlutterView? maybeOf(BuildContext context) {
return LookupBoundary.dependOnInheritedWidgetOfExactType<View>(context)?.view;
}

/// Returns the [FlutterView] that the provided `context` will render into.
///
/// Throws if the `context` is not associated with a [FlutterView].
///
/// The method creates a dependency on the `context`, which will be informed
/// when the identity of the [FlutterView] changes (i.e. the `context` is
/// moved to render into a different [FlutterView] then before). The context
/// will not be informed when the properties on the [FlutterView] itself
/// change their values. To access the property values of a [FlutterView] it
/// is best practise to use [MediaQuery.of] instead, which will ensure that
/// the `context` is informed when the view properties change.
///
/// See also:
///
/// * [View.maybeOf], which throws instead of returning null if no
/// [FlutterView] is found.
static FlutterView of(BuildContext context) {
final FlutterView? result = maybeOf(context);
assert(() {
if (result == null) {
final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType<View>(context);
final List<DiagnosticsNode> information = <DiagnosticsNode>[
if (hiddenByBoundary) ...<DiagnosticsNode>[
ErrorSummary('View.of() was called with a context that does not have access to a View widget.'),
ErrorDescription('The context provided to View.of() does have a View widget ancestor, but it is hidden by a LookupBoundary.'),
] else ...<DiagnosticsNode>[
ErrorSummary('View.of() was called with a context that does not contain a View widget.'),
ErrorDescription('No View widget ancestor could be found starting from the context that was passed to View.of().'),
],
ErrorDescription(
'The context used was:\n'
' $context',
),
ErrorHint('This usually means that the provided context is not associated with a View.'),
];
throw FlutterError.fromParts(information);
}
return true;
}());
return result!;
}
}
8 changes: 8 additions & 0 deletions packages/flutter/lib/src/widgets/window.dart
@@ -0,0 +1,8 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Placeholder to be used in a future version of Flutter.
abstract class Window {
const Window._();
}
2 changes: 2 additions & 0 deletions packages/flutter/lib/widgets.dart
Expand Up @@ -148,6 +148,8 @@ export 'src/widgets/transitions.dart';
export 'src/widgets/tween_animation_builder.dart';
export 'src/widgets/unique_widget.dart';
export 'src/widgets/value_listenable_builder.dart';
// TODO(goderbauer): Enable once clean-up in google3 is done.
// export 'src/widgets/view.dart';
export 'src/widgets/viewport.dart';
export 'src/widgets/visibility.dart';
export 'src/widgets/widget_inspector.dart';
Expand Down
20 changes: 12 additions & 8 deletions packages/flutter/test/material/debug_test.dart
Expand Up @@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('debugCheckHasMaterial control test', (WidgetTester tester) async {
await tester.pumpWidget(const Chip(label: Text('label')));
await tester.pumpWidget(const Center(child: Chip(label: Text('label'))));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception as FlutterError;
Expand All @@ -25,7 +25,7 @@ void main() {
expect(error.diagnostics[3], isA<DiagnosticsProperty<Element>>());
expect(error.diagnostics[4], isA<DiagnosticsBlock>());
expect(
error.toStringDeep(),
error.toStringDeep(), startsWith(
'FlutterError\n'
' No Material widget found.\n'
' Chip widgets require a Material widget ancestor within the\n'
Expand All @@ -42,12 +42,13 @@ void main() {
' The specific widget that could not find a Material ancestor was:\n'
' Chip\n'
' The ancestors of this widget were:\n'
' [root]\n',
);
' Center\n'
// End of ancestor chain omitted, not relevant for test.
));
});

testWidgets('debugCheckHasMaterialLocalizations control test', (WidgetTester tester) async {
await tester.pumpWidget(const BackButton());
await tester.pumpWidget(const Center(child: BackButton()));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception as FlutterError;
Expand All @@ -64,7 +65,7 @@ void main() {
expect(error.diagnostics[4], isA<DiagnosticsProperty<Element>>());
expect(error.diagnostics[5], isA<DiagnosticsBlock>());
expect(
error.toStringDeep(),
error.toStringDeep(), startsWith(
'FlutterError\n'
' No MaterialLocalizations found.\n'
' BackButton widgets require MaterialLocalizations to be provided\n'
Expand All @@ -78,8 +79,9 @@ void main() {
' ancestor was:\n'
' BackButton\n'
' The ancestors of this widget were:\n'
' [root]\n',
);
' Center\n'
// End of ancestor chain omitted, not relevant for test.
));
});

testWidgets('debugCheckHasScaffold control test', (WidgetTester tester) async {
Expand Down Expand Up @@ -233,6 +235,7 @@ void main() {
' HeroControllerScope\n'
' ScrollConfiguration\n'
' MaterialApp\n'
' View-[GlobalObjectKey TestWindow#00000]\n'
' [root]\n'
' Typically, the Scaffold widget is introduced by the MaterialApp\n'
' or WidgetsApp widget at the top of your application widget tree.\n'
Expand Down Expand Up @@ -377,6 +380,7 @@ void main() {
' Scaffold-[LabeledGlobalKey<ScaffoldState>#00000]\n'
' MediaQuery\n'
' Directionality\n'
' View-[GlobalObjectKey TestWindow#00000]\n'
' [root]\n'
' Typically, the ScaffoldMessenger widget is introduced by the\n'
' MaterialApp at the top of your application widget tree.\n'
Expand Down
1 change: 1 addition & 0 deletions packages/flutter/test/material/scaffold_test.dart
Expand Up @@ -2458,6 +2458,7 @@ void main() {
' Scaffold\n'
' MediaQuery\n'
' Directionality\n'
' View-[GlobalObjectKey TestWindow#e6136]\n'
' [root]\n'
' Typically, the ScaffoldMessenger widget is introduced by the\n'
' MaterialApp at the top of your application widget tree.\n',
Expand Down
1 change: 0 additions & 1 deletion packages/flutter/test/material/text_field_test.dart
Expand Up @@ -7431,7 +7431,6 @@ void main() {
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(exception.toString(), startsWith('No Material widget found.'));
expect(exception.toString(), endsWith(':\n $textField\nThe ancestors of this widget were:\n [root]'));
});

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

0 comments on commit bf5fdb9

Please sign in to comment.