mirror of
https://github.com/flutter/flutter
synced 2024-08-24 18:36:03 +00:00
Add runWidget to bootstrap a widget tree without a default View (#141484)
The existing `runApp` bootstraps the widget tree and renders the provided widget into the default view (which is currently the implicit View from `PlatformDispatcher.implicitView` and - in the future - may be a default-created window). Apps, that want more control over the View they are rendered in, need a new way to bootstrap the widget tree: `runWidget`. It does not make any assumptions about the View the provided widget is rendered into. Instead, it is up to the caller to include a View widget in the provided widget tree that specifies where content should be rendered. In the future, this may enable developers to create a custom window for their app instead of relying on the default-created one.
This commit is contained in:
parent
91f0878fed
commit
5b44596c5f
|
@ -24,6 +24,10 @@ import 'widget_inspector.dart';
|
|||
|
||||
export 'dart:ui' show AppLifecycleState, Locale;
|
||||
|
||||
// Examples can assume:
|
||||
// late FlutterView myFlutterView;
|
||||
// class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => const Placeholder(); }
|
||||
|
||||
/// Interface for classes that register with the Widgets layer binding.
|
||||
///
|
||||
/// This can be used by any class, not just widgets. It provides an interface
|
||||
|
@ -1152,14 +1156,20 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
|||
}
|
||||
}
|
||||
|
||||
/// Inflate the given widget and attach it to the screen.
|
||||
/// Inflate the given widget and attach it to the view.
|
||||
///
|
||||
// TODO(goderbauer): Update the paragraph below to include the Window widget once that exists.
|
||||
/// The [runApp] method renders the provided `app` widget into the
|
||||
/// [PlatformDispatcher.implicitView] by wrapping it in a [View] widget, which
|
||||
/// will bootstrap the render tree for the app. Apps that want to control which
|
||||
/// [FlutterView] they render into can use [runWidget] instead.
|
||||
///
|
||||
/// The widget is given constraints during layout that force it to fill the
|
||||
/// entire screen. If you wish to align your widget to one side of the screen
|
||||
/// entire view. If you wish to align your widget to one side of the view
|
||||
/// (e.g., the top), consider using the [Align] widget. If you wish to center
|
||||
/// your widget, you can also use the [Center] widget.
|
||||
///
|
||||
/// Calling [runApp] again will detach the previous root widget from the screen
|
||||
/// Calling [runApp] again will detach the previous root widget from the view
|
||||
/// and attach the given widget in its place. The new widget tree is compared
|
||||
/// against the previous widget tree and any differences are applied to the
|
||||
/// underlying render tree, similar to what happens when a [StatefulWidget]
|
||||
|
@ -1167,6 +1177,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
|||
///
|
||||
/// Initializes the binding using [WidgetsFlutterBinding] if necessary.
|
||||
///
|
||||
/// {@template flutter.widgets.runApp.shutdown}
|
||||
/// ## Application shutdown
|
||||
///
|
||||
/// This widget tree is not torn down when the application shuts down, because
|
||||
|
@ -1178,29 +1189,32 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
|||
/// Applications are responsible for ensuring that they are well-behaved
|
||||
/// even in the face of a rapid unscheduled termination.
|
||||
///
|
||||
/// To listen for platform shutdown messages (and other lifecycle changes),
|
||||
/// consider the [AppLifecycleListener] API.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// To artificially cause the entire widget tree to be disposed, consider
|
||||
/// calling [runApp] with a widget such as [SizedBox.shrink].
|
||||
///
|
||||
/// To listen for platform shutdown messages (and other lifecycle changes),
|
||||
/// consider the [AppLifecycleListener] API.
|
||||
///
|
||||
/// {@template flutter.widgets.runApp.dismissal}
|
||||
/// ## Dismissing Flutter UI via platform native methods
|
||||
///
|
||||
/// {@template flutter.widgets.runApp.dismissal}
|
||||
/// An application may have both Flutter and non-Flutter UI in it. If the
|
||||
/// application calls non-Flutter methods to remove Flutter based UI such as
|
||||
/// platform native API to manipulate the platform native navigation stack,
|
||||
/// the framework does not know if the developer intends to eagerly free
|
||||
/// resources or not. The widget tree remains mounted and ready to render
|
||||
/// as soon as it is displayed again.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// To release resources more eagerly, establish a [platform channel](https://flutter.dev/platform-channels/)
|
||||
/// and use it to call [runApp] with a widget such as [SizedBox.shrink] when
|
||||
/// the framework should dispose of the active widget tree.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [runWidget], which bootstraps a widget tree without assuming the
|
||||
/// [FlutterView] into which it will be rendered.
|
||||
/// * [WidgetsBinding.attachRootWidget], which creates the root widget for the
|
||||
/// widget hierarchy.
|
||||
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
|
||||
|
@ -1209,9 +1223,75 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
|||
/// ensure the widget, element, and render trees are all built.
|
||||
void runApp(Widget app) {
|
||||
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
|
||||
assert(binding.debugCheckZone('runApp'));
|
||||
_runWidget(binding.wrapWithDefaultView(app), binding, 'runApp');
|
||||
}
|
||||
|
||||
/// Inflate the given widget and bootstrap the widget tree.
|
||||
///
|
||||
// TODO(goderbauer): Update the paragraph below to include the Window widget once that exists.
|
||||
/// Unlike [runApp], this method does not define a [FlutterView] into which the
|
||||
/// provided `app` widget is rendered into. It is up to the caller to include at
|
||||
/// least one [View] widget in the provided `app` widget that will bootstrap a
|
||||
/// render tree and define the [FlutterView] into which content is rendered.
|
||||
/// [RenderObjectWidget]s without an ancestor [View] widget will result in an
|
||||
/// exception. Apps that want to render into the default view without dealing
|
||||
/// with view management should consider calling [runApp] instead.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// The sample shows how to utilize [runWidget] to specify the [FlutterView]
|
||||
/// into which the `MyApp` widget will be drawn:
|
||||
///
|
||||
/// ```dart
|
||||
/// runWidget(
|
||||
/// View(
|
||||
/// view: myFlutterView,
|
||||
/// child: const MyApp(),
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// Calling [runWidget] again will detach the previous root widget and attach
|
||||
/// the given widget in its place. The new widget tree is compared against the
|
||||
/// previous widget tree and any differences are applied to the underlying
|
||||
/// render tree, similar to what happens when a [StatefulWidget] rebuilds after
|
||||
/// calling [State.setState].
|
||||
///
|
||||
/// Initializes the binding using [WidgetsFlutterBinding] if necessary.
|
||||
///
|
||||
/// {@macro flutter.widgets.runApp.shutdown}
|
||||
///
|
||||
/// To artificially cause the entire widget tree to be disposed, consider
|
||||
/// calling [runWidget] with a [ViewCollection] that does not specify any
|
||||
/// [ViewCollection.views].
|
||||
///
|
||||
/// ## Dismissing Flutter UI via platform native methods
|
||||
///
|
||||
/// {@macro flutter.widgets.runApp.dismissal}
|
||||
///
|
||||
/// To release resources more eagerly, establish a [platform channel](https://flutter.dev/platform-channels/)
|
||||
/// and use it to remove the [View] whose widget resources should be released
|
||||
/// from the `app` widget tree provided to [runWidget].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [runApp], which bootstraps a widget tree and renders it into a default
|
||||
/// [FlutterView].
|
||||
/// * [WidgetsBinding.attachRootWidget], which creates the root widget for the
|
||||
/// widget hierarchy.
|
||||
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
|
||||
/// element for the element hierarchy.
|
||||
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
|
||||
/// ensure the widget, element, and render trees are all built.
|
||||
void runWidget(Widget app) {
|
||||
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
|
||||
_runWidget(app, binding, 'runWidget');
|
||||
}
|
||||
|
||||
void _runWidget(Widget app, WidgetsBinding binding, String debugEntryPoint) {
|
||||
assert(binding.debugCheckZone(debugEntryPoint));
|
||||
binding
|
||||
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
|
||||
..scheduleAttachRootWidget(app)
|
||||
..scheduleWarmUpFrame();
|
||||
}
|
||||
|
||||
|
|
|
@ -1325,10 +1325,11 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
|
|||
/// To listen for platform shutdown messages (and other lifecycle changes),
|
||||
/// consider the [AppLifecycleListener] API.
|
||||
///
|
||||
/// ### Dismissing Flutter UI via platform native methods
|
||||
///
|
||||
/// {@macro flutter.widgets.runApp.dismissal}
|
||||
///
|
||||
/// See the method used to bootstrap the app (e.g. [runApp] or [runWidget])
|
||||
/// for suggestions on how to release resources more eagerly.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [deactivate], which is called prior to [dispose].
|
||||
|
@ -6970,9 +6971,16 @@ abstract class RenderTreeRootElement extends RenderObjectElement {
|
|||
'however, expects that a child will be attached.',
|
||||
),
|
||||
ErrorHint(
|
||||
'Try moving the subtree that contains the ${toStringShort()} widget into the '
|
||||
'view property of a ViewAnchor widget or to the root of the widget tree, where '
|
||||
'it is not expected to attach its RenderObject to a parent.',
|
||||
'Try moving the subtree that contains the ${toStringShort()} widget '
|
||||
'to a location where it is not expected to attach its RenderObject '
|
||||
'to a parent. This could mean moving the subtree into the view '
|
||||
'property of a "ViewAnchor" widget or - if the subtree is the root of '
|
||||
'your widget tree - passing it to "runWidget" instead of "runApp".',
|
||||
),
|
||||
ErrorHint(
|
||||
'If you are seeing this error in a test and the subtree containing '
|
||||
'the ${toStringShort()} widget is passed to "WidgetTester.pumpWidget", '
|
||||
'consider setting the "wrapWithView" parameter of that method to false.'
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -13,9 +13,9 @@ void main() {
|
|||
..physicalConstraints = ViewConstraints.tight(const Size(1008.0, 2198.0))
|
||||
..devicePixelRatio = 1.912500023841858;
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: view,
|
||||
child: const SizedBox(),
|
||||
),
|
||||
|
@ -35,9 +35,3 @@ class FlutterViewSpy extends TestFlutterView {
|
|||
sizes.add(size);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
|
||||
tester.binding.attachRootWidget(widget);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
|
|
@ -98,9 +98,9 @@ void main() {
|
|||
testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async {
|
||||
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
|
||||
// which introduces a MediaQuery ancestor.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
late FlutterError error;
|
||||
try {
|
||||
|
@ -343,9 +343,3 @@ void main() {
|
|||
expect(renderObject.debugLayer?.debugCreator, isNotNull);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
|
||||
tester.binding.attachRootWidget(widget);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
|
|
@ -47,9 +47,9 @@ void main() {
|
|||
late final FlutterError error;
|
||||
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
|
||||
// which introduces a MediaQuery ancestor.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
try {
|
||||
MediaQuery.of(context);
|
||||
|
@ -111,9 +111,9 @@ void main() {
|
|||
bool tested = false;
|
||||
// Cannot use tester.pumpWidget here because it wraps the widget in a View,
|
||||
// which introduces a MediaQuery ancestor.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
final MediaQueryData? data = MediaQuery.maybeOf(context);
|
||||
expect(data, isNull);
|
||||
|
@ -287,9 +287,9 @@ void main() {
|
|||
|
||||
late MediaQueryData data;
|
||||
MediaQueryData? outerData;
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
outerData = MediaQuery.maybeOf(context);
|
||||
return MediaQuery.fromView(
|
||||
|
@ -342,9 +342,9 @@ void main() {
|
|||
late MediaQueryData data;
|
||||
MediaQueryData? outerData;
|
||||
int rebuildCount = 0;
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
outerData = MediaQuery.maybeOf(context);
|
||||
return MediaQuery.fromView(
|
||||
|
@ -1531,9 +1531,3 @@ void main() {
|
|||
]
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
|
||||
tester.binding.attachRootWidget(widget);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ void main() {
|
|||
|
||||
final RootWidget rootWidget = RootWidget(
|
||||
child: View(
|
||||
view: tester.view,
|
||||
view: FakeFlutterView(tester.view),
|
||||
child: const ColoredBox(color: Colors.orange),
|
||||
),
|
||||
);
|
||||
|
@ -36,4 +36,52 @@ void main() {
|
|||
expect(tester.binding.rootElement!.widget, equals(rootWidget));
|
||||
expect(tester.element(find.byType(ColoredBox)).owner, equals(tester.binding.buildOwner));
|
||||
});
|
||||
|
||||
testWidgets('runApp throws if given a View', (WidgetTester tester) async {
|
||||
runApp(
|
||||
View(
|
||||
view: FakeFlutterView(tester.view),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
tester.takeException(),
|
||||
isFlutterError.having(
|
||||
(FlutterError e) => e.message,
|
||||
'message',
|
||||
contains('passing it to "runWidget" instead of "runApp"'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('runWidget throws if not given a View', (WidgetTester tester) async {
|
||||
runWidget(const SizedBox.shrink());
|
||||
expect(
|
||||
tester.takeException(),
|
||||
isFlutterError.having(
|
||||
(FlutterError e) => e.message,
|
||||
'message',
|
||||
contains('Try wrapping your widget in a View widget'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('runWidget does not throw if given a View', (WidgetTester tester) async {
|
||||
runWidget(
|
||||
View(
|
||||
view: FakeFlutterView(tester.view),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
expect(find.byType(View), findsOne);
|
||||
});
|
||||
|
||||
testWidgets('can call runWidget with an empty ViewCollection', (WidgetTester tester) async {
|
||||
runWidget(const ViewCollection(views: <Widget>[]));
|
||||
expect(find.byType(ViewCollection), findsOne);
|
||||
});
|
||||
}
|
||||
|
||||
class FakeFlutterView extends TestFlutterView {
|
||||
FakeFlutterView(TestFlutterView view) : super(view: view, display: view.display, platformDispatcher: view.platformDispatcher);
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ void main() {
|
|||
child: const TestWidget(),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: widget,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
widget,
|
||||
);
|
||||
|
||||
expect(find.text('Hello'), findsOneWidget);
|
||||
|
@ -29,9 +29,9 @@ void main() {
|
|||
expect(find.text('World'), findsOneWidget);
|
||||
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World');
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[widget],
|
||||
),
|
||||
);
|
||||
|
@ -40,9 +40,9 @@ void main() {
|
|||
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World');
|
||||
|
||||
tester.state<TestWidgetState>(find.byType(TestWidget)).text = 'FooBar';
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: widget,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
widget,
|
||||
);
|
||||
expect(find.text('World'), findsNothing);
|
||||
expect(find.text('FooBar'), findsOneWidget);
|
||||
|
@ -65,9 +65,9 @@ void main() {
|
|||
child: TestWidget(key: key2),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[view1, view2],
|
||||
),
|
||||
);
|
||||
|
@ -91,9 +91,9 @@ void main() {
|
|||
expect(renderParagraphTexts(), <String>['Guten', 'Abend']);
|
||||
|
||||
tester.state<TestWidgetState>(find.byKey(key2)).text = 'Morgen';
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[view1, ViewCollection(views: <Widget>[view2])],
|
||||
),
|
||||
);
|
||||
|
@ -202,9 +202,3 @@ class TestWidgetState extends State<TestWidget> {
|
|||
return Text(text, textDirection: TextDirection.ltr);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
|
||||
tester.binding.attachRootWidget(widget);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ import 'multi_view_testing.dart';
|
|||
void main() {
|
||||
testWidgets('Providing a RenderObjectWidget directly to the RootWidget fails', (WidgetTester tester) async {
|
||||
// No render tree exists to attach the RenderObjectWidget to.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: const ColoredBox(color: Colors.red),
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
const ColoredBox(color: Colors.red),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isFlutterError.having(
|
||||
|
@ -31,18 +31,18 @@ void main() {
|
|||
color: Colors.red,
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
child: globalKeyedWidget,
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: globalKeyedWidget,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
globalKeyedWidget,
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isFlutterError.having(
|
||||
|
@ -91,15 +91,15 @@ void main() {
|
|||
child: const ColoredBox(color: Colors.red),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: globalKeyedView,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
globalKeyedView,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
child: globalKeyedView,
|
||||
),
|
||||
|
@ -155,9 +155,9 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('ViewAnchor cannot be used at the top of the widget tree (outside of View)', (WidgetTester tester) async {
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: const ViewAnchor(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
const ViewAnchor(
|
||||
child: SizedBox(),
|
||||
),
|
||||
);
|
||||
|
@ -175,18 +175,18 @@ void main() {
|
|||
child: const SizedBox(),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
child: globalKeyedViewAnchor,
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: globalKeyedViewAnchor,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
globalKeyedViewAnchor,
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isFlutterError.having(
|
||||
|
@ -197,9 +197,9 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('View can be used at the top of the widget tree', (WidgetTester tester) async {
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
child: Container(),
|
||||
),
|
||||
|
@ -214,9 +214,9 @@ void main() {
|
|||
child: const ColoredBox(color: Colors.red),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
child: ViewAnchor(
|
||||
view: globalKeyView, // This one has trouble when deactivating
|
||||
|
@ -228,9 +228,9 @@ void main() {
|
|||
expect(find.byType(SizedBox), findsOneWidget);
|
||||
expect(find.byType(ColoredBox), findsOneWidget);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: globalKeyView,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
globalKeyView,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(SizedBox), findsNothing);
|
||||
|
@ -238,9 +238,9 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('ViewCollection can be used at the top of the widget tree', (WidgetTester tester) async {
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: tester.view,
|
||||
|
@ -291,9 +291,9 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('ViewCollection cannot have render object widgets as children', (WidgetTester tester) async {
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: const ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
const ViewCollection(
|
||||
views: <Widget>[
|
||||
ColoredBox(color: Colors.red),
|
||||
],
|
||||
|
@ -319,9 +319,9 @@ void main() {
|
|||
child: const ColoredBox(color: Colors.red),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
greenView,
|
||||
ViewCollection(
|
||||
|
@ -335,9 +335,9 @@ void main() {
|
|||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(ColoredBox), findsNWidgets(2));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
redView,
|
||||
ViewCollection(
|
||||
|
@ -371,9 +371,9 @@ void main() {
|
|||
return result;
|
||||
}
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: greenView,
|
||||
|
@ -412,9 +412,9 @@ void main() {
|
|||
expect(leafRenderObject[redView.viewId], isNot(isA<RenderConstrainedBox>()));
|
||||
|
||||
// Move the child.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: greenView,
|
||||
|
@ -476,9 +476,9 @@ void main() {
|
|||
return result;
|
||||
}
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: greenView,
|
||||
|
@ -517,9 +517,9 @@ void main() {
|
|||
expect(leafRenderObject[greenView.viewId], isNot(isA<RenderConstrainedBox>()));
|
||||
|
||||
// Move the child.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: greenView,
|
||||
|
@ -571,9 +571,9 @@ void main() {
|
|||
key: GlobalKey(),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
key: greenKey,
|
||||
|
@ -610,9 +610,9 @@ void main() {
|
|||
final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!));
|
||||
|
||||
// Move the child and remove its view.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
key: greenKey,
|
||||
|
@ -652,9 +652,9 @@ void main() {
|
|||
key: GlobalKey(),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
key: greenKey,
|
||||
|
@ -691,9 +691,9 @@ void main() {
|
|||
final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!));
|
||||
|
||||
// Move the child and remove its view.
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
key: redKey,
|
||||
|
@ -1009,9 +1009,9 @@ void main() {
|
|||
final FlutterView redView = tester.view;
|
||||
final FlutterView greenView = FakeView(tester.view);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: redView,
|
||||
|
@ -1029,9 +1029,9 @@ void main() {
|
|||
expect(findsColoredBox(Colors.red), findsOneWidget);
|
||||
final RenderObject box = tester.renderObject(findsColoredBox(Colors.green));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: redView,
|
||||
|
@ -1052,17 +1052,17 @@ void main() {
|
|||
child: const SizedBox(),
|
||||
);
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: view,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
view,
|
||||
);
|
||||
|
||||
final RenderObject renderView = tester.renderObject(find.byType(View));
|
||||
final RenderObject renderSizedBox = tester.renderObject(find.byType(SizedBox));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[view],
|
||||
),
|
||||
);
|
||||
|
@ -1070,9 +1070,9 @@ void main() {
|
|||
expect(tester.renderObject(find.byType(View)), same(renderView));
|
||||
expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: view,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
view,
|
||||
);
|
||||
|
||||
expect(tester.renderObject(find.byType(View)), same(renderView));
|
||||
|
@ -1114,9 +1114,9 @@ void main() {
|
|||
child: const SizedBox(),
|
||||
)
|
||||
);
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: view,
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
view,
|
||||
);
|
||||
|
||||
final RenderObject renderSemantics = tester.renderObject(find.bySemanticsLabel('Hello'));
|
||||
|
@ -1124,9 +1124,9 @@ void main() {
|
|||
expect(semantics.id, 1);
|
||||
expect(renderSemantics.debugSemantics, same(semantics));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
view,
|
||||
],
|
||||
|
@ -1144,9 +1144,3 @@ void main() {
|
|||
Finder findsColoredBox(Color color) {
|
||||
return find.byWidgetPredicate((Widget widget) => widget is ColoredBox && widget.color == color);
|
||||
}
|
||||
|
||||
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
|
||||
tester.binding.attachRootWidget(widget);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
|
|
@ -77,9 +77,9 @@ void main() {
|
|||
PipelineOwner? outsideParent;
|
||||
PipelineOwner? insideParent;
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
outsideView = View.maybeOf(context);
|
||||
outsideParent = View.pipelineOwnerOf(context);
|
||||
|
@ -114,9 +114,9 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('cannot have multiple views with same FlutterView', (WidgetTester tester) async {
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: tester.view,
|
||||
|
@ -224,9 +224,9 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('visitChildren of ViewCollection visits all children', (WidgetTester tester) async {
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: tester.view,
|
||||
|
@ -250,9 +250,9 @@ void main() {
|
|||
});
|
||||
expect(children, hasLength(3));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: ViewCollection(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
ViewCollection(
|
||||
views: <Widget>[
|
||||
View(
|
||||
view: tester.view,
|
||||
|
@ -271,9 +271,9 @@ void main() {
|
|||
group('renderObject getter', () {
|
||||
testWidgets('ancestors of view see RenderView as renderObject', (WidgetTester tester) async {
|
||||
late BuildContext builderContext;
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
builderContext = context;
|
||||
return View(
|
||||
|
@ -293,9 +293,9 @@ void main() {
|
|||
|
||||
testWidgets('ancestors of ViewCollection get null for renderObject', (WidgetTester tester) async {
|
||||
late BuildContext builderContext;
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: Builder(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
builderContext = context;
|
||||
return ViewCollection(
|
||||
|
@ -345,9 +345,9 @@ void main() {
|
|||
});
|
||||
|
||||
testWidgets('correctly switches between view configurations', (WidgetTester tester) async {
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
||||
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
||||
|
@ -359,9 +359,9 @@ void main() {
|
|||
expect(renderView.owner, same(tester.binding.pipelineOwner));
|
||||
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
child: const SizedBox(),
|
||||
),
|
||||
|
@ -371,9 +371,9 @@ void main() {
|
|||
expect(renderView.owner, isNot(same(tester.binding.pipelineOwner)));
|
||||
expect(tester.renderObject(find.byType(SizedBox)).owner, isNot(same(tester.binding.pipelineOwner)));
|
||||
|
||||
await pumpWidgetWithoutViewWrapper(
|
||||
tester: tester,
|
||||
widget: View(
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
|
||||
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
|
||||
|
@ -513,12 +513,6 @@ void main() {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
|
||||
tester.binding.attachRootWidget(widget);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
||||
class SpyRenderWidget extends SizedBox {
|
||||
const SpyRenderWidget({super.key, required this.label, required this.log, super.child});
|
||||
|
||||
|
|
|
@ -581,15 +581,23 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
|
|||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// By default, the provided `widget` is rendered into [WidgetTester.view],
|
||||
/// whose properties tests can modify to simulate different scenarios (e.g.
|
||||
/// running on a large/small screen). Tests that want to control the
|
||||
/// [FlutterView] into which content is rendered can set `wrapWithView` to
|
||||
/// false and use [View] widgets in the provided `widget` tree to specify the
|
||||
/// desired [FlutterView]s.
|
||||
///
|
||||
/// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
|
||||
/// this method works when the test is run with `flutter run`.
|
||||
Future<void> pumpWidget(
|
||||
Widget widget, {
|
||||
Duration? duration,
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
bool wrapWithView = true,
|
||||
}) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
binding.attachRootWidget(binding.wrapWithDefaultView(widget));
|
||||
binding.attachRootWidget(wrapWithView ? binding.wrapWithDefaultView(widget) : widget);
|
||||
binding.scheduleFrame();
|
||||
return binding.pump(duration, phase);
|
||||
});
|
||||
|
|
|
@ -112,7 +112,8 @@ Future<void> pumpViews({required WidgetTester tester, required List<Widget> vie
|
|||
),
|
||||
];
|
||||
|
||||
tester.binding.attachRootWidget(
|
||||
return tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ViewCollection(
|
||||
|
@ -120,6 +121,4 @@ Future<void> pumpViews({required WidgetTester tester, required List<Widget> vie
|
|||
),
|
||||
),
|
||||
);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
|
|
@ -208,7 +208,8 @@ Future<void> pumpViews({required WidgetTester tester}) {
|
|||
),
|
||||
];
|
||||
|
||||
tester.binding.attachRootWidget(
|
||||
return tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ViewCollection(
|
||||
|
@ -216,6 +217,4 @@ Future<void> pumpViews({required WidgetTester tester}) {
|
|||
),
|
||||
),
|
||||
);
|
||||
tester.binding.scheduleFrame();
|
||||
return tester.binding.pump();
|
||||
}
|
||||
|
|
|
@ -638,6 +638,56 @@ void main() {
|
|||
.checkMockMessageHandler(SystemChannels.accessibility.name, null), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('wrapWithView: false does not include View', (WidgetTester tester) async {
|
||||
FlutterView? flutterView;
|
||||
View? view;
|
||||
int builderCount = 0;
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
builderCount++;
|
||||
flutterView = View.maybeOf(context);
|
||||
view = context.findAncestorWidgetOfExactType<View>();
|
||||
return const ViewCollection(views: <Widget>[]);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(builderCount, 1);
|
||||
expect(view, isNull);
|
||||
expect(flutterView, isNull);
|
||||
expect(find.byType(View), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('passing a view to pumpWidget with wrapWithView: true throws', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
View(
|
||||
view: tester.view,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
tester.takeException(),
|
||||
isFlutterError.having(
|
||||
(FlutterError e) => e.message,
|
||||
'message',
|
||||
contains('consider setting the "wrapWithView" parameter of that method to false'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('can pass a View to pumpWidget when wrapWithView: false', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
wrapWithView: false,
|
||||
View(
|
||||
view: tester.view,
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
expect(find.byType(View), findsOne);
|
||||
});
|
||||
}
|
||||
|
||||
class FakeMatcher extends AsyncMatcher {
|
||||
|
|
Loading…
Reference in a new issue