Skip to content

Commit f9a76ae

Browse files
authored
Throw StateError when implicitView is null on wrapWithDefaultView. (flutter#155734)
This PR tweaks `wrapWithDefaultView` (used by `runApp`) to raise a StateError with a legible error message when the `platformDispatcher.implicitView` is missing (for example, when enabling multi-view embedding on the web), instead of crashing with an unexpected `nullCheck` a few lines below. * Before: <img width="619" alt="Screenshot 2024-09-25 at 7 33 47�PM" src="https://github.com/user-attachments/assets/4897dd3c-bdd0-4217-9f23-7eee9fab4999"> * After: <img width="613" alt="Screenshot 2024-09-26 at 5 01 49�PM" src="https://github.com/user-attachments/assets/3febb91d-a8c3-41b6-bf34-c2c8743b637c"> ## Issues * Fixes flutter#153198 ## Tests Added a test to ensure the assertion is thrown when the `implicitView` is missing. Had to hack a little because I couldn't find any clean way of overriding the `implicitView`. The problem is that the flutter_test bindings [use `runApp` internally](https://github.com/flutter/flutter/blob/8925e1ffdfe880b06a8bc5877f017083d6652f5b/packages/flutter_test/lib/src/binding.dart#L1020) a couple of times, so I can only disable the implicitView inside the test body (and must re-enable it before returning). Not sure if it's the best way, but it seems to do the trick for this simple test case!
1 parent cad7418 commit f9a76ae

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,10 +1225,27 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
12251225
///
12261226
/// The [View] determines into what [FlutterView] the app is rendered into.
12271227
/// This is currently [PlatformDispatcher.implicitView] from [platformDispatcher].
1228+
/// This method will throw a [StateError] if the [PlatformDispatcher.implicitView]
1229+
/// is null.
12281230
///
12291231
/// The `rootWidget` widget provided to this method must not already be
12301232
/// wrapped in a [View].
12311233
Widget wrapWithDefaultView(Widget rootWidget) {
1234+
if (platformDispatcher.implicitView == null) {
1235+
throw StateError(
1236+
'The app requested a view, but the platform did not provide one.\n'
1237+
'This is likely because the app called `runApp` to render its root '
1238+
'widget, which expects the platform to provide a default view to '
1239+
'render into (the "implicit" view).\n'
1240+
'However, the platform likely has multi-view mode enabled, which does '
1241+
'not create this default "implicit" view.\n'
1242+
'Try using `runWidget` instead of `runApp` to start your app.\n'
1243+
'`runWidget` allows you to provide a `View` widget, without requiring '
1244+
'a default view.'
1245+
'${kIsWeb?"\nSee: https://flutter.dev/to/web-multiview-runwidget" : ""}'
1246+
);
1247+
}
1248+
12321249
return View(
12331250
view: platformDispatcher.implicitView!,
12341251
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: pipelineOwner,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 'package:flutter/material.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
import 'multi_view_testing.dart';
9+
10+
void main() {
11+
// Overrides the default test bindings so we have the ability to hide the
12+
// implicitView inside the body of a test.
13+
final NoImplicitViewWidgetsBinding binding = NoImplicitViewWidgetsBinding();
14+
15+
testWidgets('NoImplicitViewWidgetsBinding self-test', (WidgetTester tester) async {
16+
expect(tester.platformDispatcher.implicitView, isNotNull);
17+
18+
// Hide the implicitView from the test harness.
19+
binding.hideImplicitView();
20+
21+
expect(tester.platformDispatcher.implicitView, isNull);
22+
23+
// Ensure the test harness finds the implicitView.
24+
binding.showImplicitView();
25+
});
26+
27+
testWidgets('null implicitView - runApp throws assertion, suggests to use `runWidget`.', (WidgetTester tester) async {
28+
// Hide the implicitView from the test harness.
29+
binding.hideImplicitView();
30+
31+
expect(() {
32+
runApp(Container());
33+
}, throwsA(
34+
isA<StateError>().having(
35+
(StateError error) => error.message,
36+
'description',
37+
contains('Try using `runWidget` instead of `runApp`'), ),
38+
),
39+
);
40+
41+
// Ensure the test harness finds the implicitView.
42+
binding.showImplicitView();
43+
});
44+
}

packages/flutter/test/widgets/multi_view_testing.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,48 @@ class FakeView extends TestFlutterView {
3131
// not produce consistent semantics trees.
3232
}
3333
}
34+
35+
/// A test platform dispatcher that can show/hide its underlying `implicitView`,
36+
/// depending on the value of the [implicitViewHidden] flag.
37+
class NoImplicitViewPlatformDispatcher extends TestPlatformDispatcher {
38+
NoImplicitViewPlatformDispatcher({ required super.platformDispatcher }) : superPlatformDispatcher = platformDispatcher;
39+
40+
final PlatformDispatcher superPlatformDispatcher;
41+
42+
bool implicitViewHidden = false;
43+
44+
@override
45+
TestFlutterView? get implicitView {
46+
return implicitViewHidden
47+
? null
48+
: superPlatformDispatcher.implicitView as TestFlutterView?;
49+
}
50+
}
51+
52+
/// Test Flutter Bindings that allow tests to hide/show the `implicitView`
53+
/// of their [NoImplicitViewPlatformDispatcher] `platformDispatcher`.
54+
///
55+
/// This is used to test that [runApp] throws an assertion error with an
56+
/// explanation when used when the `implicitView` is disabled (like in Flutter
57+
/// web when multi-view is enabled).
58+
///
59+
/// Because of how [testWidgets] uses `runApp` internally to manage the lifecycle
60+
/// of a test, the implicitView must be disabled/reenabled inside of the body of
61+
/// the [WidgetTesterCallback] under test. In practice: the implicitView is disabled
62+
/// in the first line of the test, and reenabled in the last.
63+
///
64+
/// See: multi_view_no_implicitView_binding_test.dart
65+
class NoImplicitViewWidgetsBinding extends AutomatedTestWidgetsFlutterBinding {
66+
late final NoImplicitViewPlatformDispatcher _platformDispatcher = NoImplicitViewPlatformDispatcher(platformDispatcher: super.platformDispatcher);
67+
68+
@override
69+
NoImplicitViewPlatformDispatcher get platformDispatcher => _platformDispatcher;
70+
71+
void hideImplicitView() {
72+
platformDispatcher.implicitViewHidden = true;
73+
}
74+
75+
void showImplicitView() {
76+
platformDispatcher.implicitViewHidden = false;
77+
}
78+
}

0 commit comments

Comments
 (0)