Skip to content

Commit 671d8ea

Browse files
authored
Relands "Add runWidget to bootstrap a widget tree without a default View" (#142344)
Reverts flutter/flutter#142339 In the original change one of the tests included the same view twice which resulted in a different failure than the expected one. The second commit contains the fix for this. I don't understand how this wasn't caught presubmit on CI.
1 parent a5ad088 commit 671d8ea

13 files changed

+367
-209
lines changed

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

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import 'widget_inspector.dart';
2424

2525
export 'dart:ui' show AppLifecycleState, Locale;
2626

27+
// Examples can assume:
28+
// late FlutterView myFlutterView;
29+
// class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => const Placeholder(); }
30+
2731
/// Interface for classes that register with the Widgets layer binding.
2832
///
2933
/// This can be used by any class, not just widgets. It provides an interface
@@ -1152,21 +1156,28 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
11521156
}
11531157
}
11541158

1155-
/// Inflate the given widget and attach it to the screen.
1159+
/// Inflate the given widget and attach it to the view.
1160+
///
1161+
// TODO(goderbauer): Update the paragraph below to include the Window widget once that exists.
1162+
/// The [runApp] method renders the provided `app` widget into the
1163+
/// [PlatformDispatcher.implicitView] by wrapping it in a [View] widget, which
1164+
/// will bootstrap the render tree for the app. Apps that want to control which
1165+
/// [FlutterView] they render into can use [runWidget] instead.
11561166
///
11571167
/// The widget is given constraints during layout that force it to fill the
1158-
/// entire screen. If you wish to align your widget to one side of the screen
1168+
/// entire view. If you wish to align your widget to one side of the view
11591169
/// (e.g., the top), consider using the [Align] widget. If you wish to center
11601170
/// your widget, you can also use the [Center] widget.
11611171
///
1162-
/// Calling [runApp] again will detach the previous root widget from the screen
1172+
/// Calling [runApp] again will detach the previous root widget from the view
11631173
/// and attach the given widget in its place. The new widget tree is compared
11641174
/// against the previous widget tree and any differences are applied to the
11651175
/// underlying render tree, similar to what happens when a [StatefulWidget]
11661176
/// rebuilds after calling [State.setState].
11671177
///
11681178
/// Initializes the binding using [WidgetsFlutterBinding] if necessary.
11691179
///
1180+
/// {@template flutter.widgets.runApp.shutdown}
11701181
/// ## Application shutdown
11711182
///
11721183
/// This widget tree is not torn down when the application shuts down, because
@@ -1178,29 +1189,32 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
11781189
/// Applications are responsible for ensuring that they are well-behaved
11791190
/// even in the face of a rapid unscheduled termination.
11801191
///
1181-
/// To artificially cause the entire widget tree to be disposed, consider
1182-
/// calling [runApp] with a widget such as [SizedBox.shrink].
1183-
///
11841192
/// To listen for platform shutdown messages (and other lifecycle changes),
11851193
/// consider the [AppLifecycleListener] API.
1194+
/// {@endtemplate}
11861195
///
1187-
/// ## Dismissing Flutter UI via platform native methods
1196+
/// To artificially cause the entire widget tree to be disposed, consider
1197+
/// calling [runApp] with a widget such as [SizedBox.shrink].
11881198
///
11891199
/// {@template flutter.widgets.runApp.dismissal}
1200+
/// ## Dismissing Flutter UI via platform native methods
1201+
///
11901202
/// An application may have both Flutter and non-Flutter UI in it. If the
11911203
/// application calls non-Flutter methods to remove Flutter based UI such as
11921204
/// platform native API to manipulate the platform native navigation stack,
11931205
/// the framework does not know if the developer intends to eagerly free
11941206
/// resources or not. The widget tree remains mounted and ready to render
11951207
/// as soon as it is displayed again.
1208+
/// {@endtemplate}
11961209
///
11971210
/// To release resources more eagerly, establish a [platform channel](https://flutter.dev/platform-channels/)
11981211
/// and use it to call [runApp] with a widget such as [SizedBox.shrink] when
11991212
/// the framework should dispose of the active widget tree.
1200-
/// {@endtemplate}
12011213
///
12021214
/// See also:
12031215
///
1216+
/// * [runWidget], which bootstraps a widget tree without assuming the
1217+
/// [FlutterView] into which it will be rendered.
12041218
/// * [WidgetsBinding.attachRootWidget], which creates the root widget for the
12051219
/// widget hierarchy.
12061220
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
@@ -1209,9 +1223,75 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
12091223
/// ensure the widget, element, and render trees are all built.
12101224
void runApp(Widget app) {
12111225
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
1212-
assert(binding.debugCheckZone('runApp'));
1226+
_runWidget(binding.wrapWithDefaultView(app), binding, 'runApp');
1227+
}
1228+
1229+
/// Inflate the given widget and bootstrap the widget tree.
1230+
///
1231+
// TODO(goderbauer): Update the paragraph below to include the Window widget once that exists.
1232+
/// Unlike [runApp], this method does not define a [FlutterView] into which the
1233+
/// provided `app` widget is rendered into. It is up to the caller to include at
1234+
/// least one [View] widget in the provided `app` widget that will bootstrap a
1235+
/// render tree and define the [FlutterView] into which content is rendered.
1236+
/// [RenderObjectWidget]s without an ancestor [View] widget will result in an
1237+
/// exception. Apps that want to render into the default view without dealing
1238+
/// with view management should consider calling [runApp] instead.
1239+
///
1240+
/// {@tool snippet}
1241+
/// The sample shows how to utilize [runWidget] to specify the [FlutterView]
1242+
/// into which the `MyApp` widget will be drawn:
1243+
///
1244+
/// ```dart
1245+
/// runWidget(
1246+
/// View(
1247+
/// view: myFlutterView,
1248+
/// child: const MyApp(),
1249+
/// ),
1250+
/// );
1251+
/// ```
1252+
/// {@end-tool}
1253+
///
1254+
/// Calling [runWidget] again will detach the previous root widget and attach
1255+
/// the given widget in its place. The new widget tree is compared against the
1256+
/// previous widget tree and any differences are applied to the underlying
1257+
/// render tree, similar to what happens when a [StatefulWidget] rebuilds after
1258+
/// calling [State.setState].
1259+
///
1260+
/// Initializes the binding using [WidgetsFlutterBinding] if necessary.
1261+
///
1262+
/// {@macro flutter.widgets.runApp.shutdown}
1263+
///
1264+
/// To artificially cause the entire widget tree to be disposed, consider
1265+
/// calling [runWidget] with a [ViewCollection] that does not specify any
1266+
/// [ViewCollection.views].
1267+
///
1268+
/// ## Dismissing Flutter UI via platform native methods
1269+
///
1270+
/// {@macro flutter.widgets.runApp.dismissal}
1271+
///
1272+
/// To release resources more eagerly, establish a [platform channel](https://flutter.dev/platform-channels/)
1273+
/// and use it to remove the [View] whose widget resources should be released
1274+
/// from the `app` widget tree provided to [runWidget].
1275+
///
1276+
/// See also:
1277+
///
1278+
/// * [runApp], which bootstraps a widget tree and renders it into a default
1279+
/// [FlutterView].
1280+
/// * [WidgetsBinding.attachRootWidget], which creates the root widget for the
1281+
/// widget hierarchy.
1282+
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
1283+
/// element for the element hierarchy.
1284+
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
1285+
/// ensure the widget, element, and render trees are all built.
1286+
void runWidget(Widget app) {
1287+
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
1288+
_runWidget(app, binding, 'runWidget');
1289+
}
1290+
1291+
void _runWidget(Widget app, WidgetsBinding binding, String debugEntryPoint) {
1292+
assert(binding.debugCheckZone(debugEntryPoint));
12131293
binding
1214-
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
1294+
..scheduleAttachRootWidget(app)
12151295
..scheduleWarmUpFrame();
12161296
}
12171297

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,10 +1325,11 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
13251325
/// To listen for platform shutdown messages (and other lifecycle changes),
13261326
/// consider the [AppLifecycleListener] API.
13271327
///
1328-
/// ### Dismissing Flutter UI via platform native methods
1329-
///
13301328
/// {@macro flutter.widgets.runApp.dismissal}
13311329
///
1330+
/// See the method used to bootstrap the app (e.g. [runApp] or [runWidget])
1331+
/// for suggestions on how to release resources more eagerly.
1332+
///
13321333
/// See also:
13331334
///
13341335
/// * [deactivate], which is called prior to [dispose].
@@ -6970,9 +6971,16 @@ abstract class RenderTreeRootElement extends RenderObjectElement {
69706971
'however, expects that a child will be attached.',
69716972
),
69726973
ErrorHint(
6973-
'Try moving the subtree that contains the ${toStringShort()} widget into the '
6974-
'view property of a ViewAnchor widget or to the root of the widget tree, where '
6975-
'it is not expected to attach its RenderObject to a parent.',
6974+
'Try moving the subtree that contains the ${toStringShort()} widget '
6975+
'to a location where it is not expected to attach its RenderObject '
6976+
'to a parent. This could mean moving the subtree into the view '
6977+
'property of a "ViewAnchor" widget or - if the subtree is the root of '
6978+
'your widget tree - passing it to "runWidget" instead of "runApp".',
6979+
),
6980+
ErrorHint(
6981+
'If you are seeing this error in a test and the subtree containing '
6982+
'the ${toStringShort()} widget is passed to "WidgetTester.pumpWidget", '
6983+
'consider setting the "wrapWithView" parameter of that method to false.'
69766984
),
69776985
],
69786986
);

packages/flutter/test/rendering/view_constraints_test.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ void main() {
1313
..physicalConstraints = ViewConstraints.tight(const Size(1008.0, 2198.0))
1414
..devicePixelRatio = 1.912500023841858;
1515

16-
await pumpWidgetWithoutViewWrapper(
17-
tester: tester,
18-
widget: View(
16+
await tester.pumpWidget(
17+
wrapWithView: false,
18+
View(
1919
view: view,
2020
child: const SizedBox(),
2121
),
@@ -35,9 +35,3 @@ class FlutterViewSpy extends TestFlutterView {
3535
sizes.add(size);
3636
}
3737
}
38-
39-
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
40-
tester.binding.attachRootWidget(widget);
41-
tester.binding.scheduleFrame();
42-
return tester.binding.pump();
43-
}

packages/flutter/test/widgets/debug_test.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ void main() {
9898
testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async {
9999
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
100100
// which introduces a MediaQuery ancestor.
101-
await pumpWidgetWithoutViewWrapper(
102-
tester: tester,
103-
widget: Builder(
101+
await tester.pumpWidget(
102+
wrapWithView: false,
103+
Builder(
104104
builder: (BuildContext context) {
105105
late FlutterError error;
106106
try {
@@ -343,9 +343,3 @@ void main() {
343343
expect(renderObject.debugLayer?.debugCreator, isNotNull);
344344
});
345345
}
346-
347-
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
348-
tester.binding.attachRootWidget(widget);
349-
tester.binding.scheduleFrame();
350-
return tester.binding.pump();
351-
}

packages/flutter/test/widgets/media_query_test.dart

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ void main() {
4747
late final FlutterError error;
4848
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
4949
// which introduces a MediaQuery ancestor.
50-
await pumpWidgetWithoutViewWrapper(
51-
tester: tester,
52-
widget: Builder(
50+
await tester.pumpWidget(
51+
wrapWithView: false,
52+
Builder(
5353
builder: (BuildContext context) {
5454
try {
5555
MediaQuery.of(context);
@@ -111,9 +111,9 @@ void main() {
111111
bool tested = false;
112112
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
113113
// which introduces a MediaQuery ancestor.
114-
await pumpWidgetWithoutViewWrapper(
115-
tester: tester,
116-
widget: Builder(
114+
await tester.pumpWidget(
115+
wrapWithView: false,
116+
Builder(
117117
builder: (BuildContext context) {
118118
final MediaQueryData? data = MediaQuery.maybeOf(context);
119119
expect(data, isNull);
@@ -287,9 +287,9 @@ void main() {
287287

288288
late MediaQueryData data;
289289
MediaQueryData? outerData;
290-
await pumpWidgetWithoutViewWrapper(
291-
tester: tester,
292-
widget: Builder(
290+
await tester.pumpWidget(
291+
wrapWithView: false,
292+
Builder(
293293
builder: (BuildContext context) {
294294
outerData = MediaQuery.maybeOf(context);
295295
return MediaQuery.fromView(
@@ -342,9 +342,9 @@ void main() {
342342
late MediaQueryData data;
343343
MediaQueryData? outerData;
344344
int rebuildCount = 0;
345-
await pumpWidgetWithoutViewWrapper(
346-
tester: tester,
347-
widget: Builder(
345+
await tester.pumpWidget(
346+
wrapWithView: false,
347+
Builder(
348348
builder: (BuildContext context) {
349349
outerData = MediaQuery.maybeOf(context);
350350
return MediaQuery.fromView(
@@ -1531,9 +1531,3 @@ void main() {
15311531
]
15321532
));
15331533
}
1534-
1535-
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
1536-
tester.binding.attachRootWidget(widget);
1537-
tester.binding.scheduleFrame();
1538-
return tester.binding.pump();
1539-
}

packages/flutter/test/widgets/multi_view_binding_test.dart

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ void main() {
2626

2727
final RootWidget rootWidget = RootWidget(
2828
child: View(
29-
view: tester.view,
29+
view: FakeFlutterView(tester.view),
3030
child: const ColoredBox(color: Colors.orange),
3131
),
3232
);
@@ -36,4 +36,52 @@ void main() {
3636
expect(tester.binding.rootElement!.widget, equals(rootWidget));
3737
expect(tester.element(find.byType(ColoredBox)).owner, equals(tester.binding.buildOwner));
3838
});
39+
40+
testWidgets('runApp throws if given a View', (WidgetTester tester) async {
41+
runApp(
42+
View(
43+
view: FakeFlutterView(tester.view),
44+
child: const SizedBox.shrink(),
45+
),
46+
);
47+
expect(
48+
tester.takeException(),
49+
isFlutterError.having(
50+
(FlutterError e) => e.message,
51+
'message',
52+
contains('passing it to "runWidget" instead of "runApp"'),
53+
),
54+
);
55+
});
56+
57+
testWidgets('runWidget throws if not given a View', (WidgetTester tester) async {
58+
runWidget(const SizedBox.shrink());
59+
expect(
60+
tester.takeException(),
61+
isFlutterError.having(
62+
(FlutterError e) => e.message,
63+
'message',
64+
contains('Try wrapping your widget in a View widget'),
65+
),
66+
);
67+
});
68+
69+
testWidgets('runWidget does not throw if given a View', (WidgetTester tester) async {
70+
runWidget(
71+
View(
72+
view: FakeFlutterView(tester.view),
73+
child: const SizedBox.shrink(),
74+
),
75+
);
76+
expect(find.byType(View), findsOne);
77+
});
78+
79+
testWidgets('can call runWidget with an empty ViewCollection', (WidgetTester tester) async {
80+
runWidget(const ViewCollection(views: <Widget>[]));
81+
expect(find.byType(ViewCollection), findsOne);
82+
});
83+
}
84+
85+
class FakeFlutterView extends TestFlutterView {
86+
FakeFlutterView(TestFlutterView view) : super(view: view, display: view.display, platformDispatcher: view.platformDispatcher);
3987
}

0 commit comments

Comments
 (0)