Skip to content

Commit d6afd93

Browse files
author
Albertus Angga Raharja
authored
Add more structure to errors (continuation of #34684) (#42640)
* Add structured errors in Animations, TabView, ChangeNotifier * Add structured error on MaterialPageRoute, BoxBorder, DecorationImagePainter, TextSpan * Add structured errors in Debug * Fix test errors * Add structured errors in Scaffold and Stepper * Add structured errors in part of Rendering Layer * Fix failing test due to FloatingPoint precision * Fix failing tests due to precision error and not using final * Fix failing test due to floating precision error with RegEx instead * Add structured error in CustomLayout and increase test coverage * Add structured error & its test in ListBody * Add structured error in ProxyBox and increase test coverage * Add structured error message in Viewport * Fix styles and add more assertions on ErrorHint and DiagnosticProperty * Add structured error in scheduler/binding and scheduler/ticker Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Add structured error in AssetBundle and TextInput Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Add structured errors in several widgets flutter#1 Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Remove unused import Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Add assertions on hint messages Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Fix catch spacing Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Add structured error in several widgets part 2 and increase code coverage Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Add structured error in flutter_test/widget_tester * Fix floating precision accuracy by using RegExp Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Remove todo to add tests in Scaffold showBottomSheet Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Fix reviews by indenting lines and fixing the assertion orders Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Fix failing tests due to renaming class Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Try skipping the NetworkBundleTest Signed-off-by: Albertus Angga Raharja <albertusangga@google.com> * Remove leading space in material/debug error hint Signed-off-by: Albertus Angga Raharja <albertusangga@google.com>
1 parent 92a4d3f commit d6afd93

11 files changed

Lines changed: 639 additions & 162 deletions

File tree

packages/flutter/lib/src/cupertino/tab_view.dart

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -192,29 +192,33 @@ class _CupertinoTabViewState extends State<CupertinoTabView> {
192192
Route<dynamic> _onUnknownRoute(RouteSettings settings) {
193193
assert(() {
194194
if (widget.onUnknownRoute == null) {
195-
throw FlutterError(
196-
'Could not find a generator for route $settings in the $runtimeType.\n'
197-
'Generators for routes are searched for in the following order:\n'
198-
' 1. For the "/" route, the "builder" property, if non-null, is used.\n'
199-
' 2. Otherwise, the "routes" table is used, if it has an entry for '
200-
'the route.\n'
201-
' 3. Otherwise, onGenerateRoute is called. It should return a '
202-
'non-null value for any valid route not handled by "builder" and "routes".\n'
203-
' 4. Finally if all else fails onUnknownRoute is called.\n'
204-
'Unfortunately, onUnknownRoute was not set.'
205-
);
195+
throw FlutterError.fromParts(<DiagnosticsNode>[
196+
ErrorSummary('Could not find a generator for route $settings in the $runtimeType.'),
197+
ErrorDescription(
198+
'Generators for routes are searched for in the following order:\n'
199+
' 1. For the "/" route, the "builder" property, if non-null, is used.\n'
200+
' 2. Otherwise, the "routes" table is used, if it has an entry for '
201+
'the route.\n'
202+
' 3. Otherwise, onGenerateRoute is called. It should return a '
203+
'non-null value for any valid route not handled by "builder" and "routes".\n'
204+
' 4. Finally if all else fails onUnknownRoute is called.\n'
205+
'Unfortunately, onUnknownRoute was not set.'
206+
)
207+
]);
206208
}
207209
return true;
208210
}());
209211
final Route<dynamic> result = widget.onUnknownRoute(settings);
210212
assert(() {
211213
if (result == null) {
212-
throw FlutterError(
213-
'The onUnknownRoute callback returned null.\n'
214-
'When the $runtimeType requested the route $settings from its '
215-
'onUnknownRoute callback, the callback returned null. Such callbacks '
216-
'must never return null.'
217-
);
214+
throw FlutterError.fromParts(<DiagnosticsNode>[
215+
ErrorSummary('The onUnknownRoute callback returned null.'),
216+
ErrorDescription(
217+
'When the $runtimeType requested the route $settings from its '
218+
'onUnknownRoute callback, the callback returned null. Such callbacks '
219+
'must never return null.'
220+
)
221+
]);
218222
}
219223
return true;
220224
}());

packages/flutter/lib/src/material/debug.dart

Lines changed: 45 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/foundation.dart';
56
import 'package:flutter/widgets.dart';
67

78
import 'material.dart';
@@ -24,45 +25,26 @@ import 'scaffold.dart' show Scaffold;
2425
bool debugCheckHasMaterial(BuildContext context) {
2526
assert(() {
2627
if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) {
27-
final StringBuffer message = StringBuffer();
28-
message.writeln('No Material widget found.');
29-
message.writeln(
30-
'${context.widget.runtimeType} widgets require a Material '
31-
'widget ancestor.'
28+
throw FlutterError.fromParts(<DiagnosticsNode>[
29+
ErrorSummary('No Material widget found.'),
30+
ErrorDescription(
31+
'${context.widget.runtimeType} widgets require a Material '
32+
'widget ancestor.\n'
33+
'In material design, most widgets are conceptually "printed" on '
34+
'a sheet of material. In Flutter\'s material library, that '
35+
'material is represented by the Material widget. It is the '
36+
'Material widget that renders ink splashes, for instance. '
37+
'Because of this, many material library widgets require that '
38+
'there be a Material widget in the tree above them.'
39+
),
40+
ErrorHint(
41+
'To introduce a Material widget, you can either directly '
42+
'include one, or use a widget that contains Material itself, '
43+
'such as a Card, Dialog, Drawer, or Scaffold.',
44+
),
45+
...context.describeMissingAncestor(expectedAncestorType: Material)
46+
]
3247
);
33-
message.writeln(
34-
'In material design, most widgets are conceptually "printed" on '
35-
'a sheet of material. In Flutter\'s material library, that '
36-
'material is represented by the Material widget. It is the '
37-
'Material widget that renders ink splashes, for instance. '
38-
'Because of this, many material library widgets require that '
39-
'there be a Material widget in the tree above them.'
40-
);
41-
message.writeln(
42-
'To introduce a Material widget, you can either directly '
43-
'include one, or use a widget that contains Material itself, '
44-
'such as a Card, Dialog, Drawer, or Scaffold.'
45-
);
46-
message.writeln(
47-
'The specific widget that could not find a Material ancestor was:'
48-
);
49-
message.writeln(' ${context.widget}');
50-
final List<Widget> ancestors = <Widget>[];
51-
context.visitAncestorElements((Element element) {
52-
ancestors.add(element.widget);
53-
return true;
54-
});
55-
if (ancestors.isNotEmpty) {
56-
message.write('The ancestors of this widget were:');
57-
for (Widget ancestor in ancestors)
58-
message.write('\n $ancestor');
59-
} else {
60-
message.writeln(
61-
'This widget is the root of the tree, so it has no '
62-
'ancestors, let alone a "Material" ancestor.'
63-
);
64-
}
65-
throw FlutterError(message.toString());
6648
}
6749
return true;
6850
}());
@@ -87,42 +69,24 @@ bool debugCheckHasMaterial(BuildContext context) {
8769
bool debugCheckHasMaterialLocalizations(BuildContext context) {
8870
assert(() {
8971
if (Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) == null) {
90-
final StringBuffer message = StringBuffer();
91-
message.writeln('No MaterialLocalizations found.');
92-
message.writeln(
93-
'${context.widget.runtimeType} widgets require MaterialLocalizations '
94-
'to be provided by a Localizations widget ancestor.'
95-
);
96-
message.writeln(
97-
'Localizations are used to generate many different messages, labels,'
98-
'and abbreviations which are used by the material library. '
99-
);
100-
message.writeln(
101-
'To introduce a MaterialLocalizations, either use a '
102-
' MaterialApp at the root of your application to include them '
103-
'automatically, or add a Localization widget with a '
104-
'MaterialLocalizations delegate.'
105-
);
106-
message.writeln(
107-
'The specific widget that could not find a MaterialLocalizations ancestor was:'
108-
);
109-
message.writeln(' ${context.widget}');
110-
final List<Widget> ancestors = <Widget>[];
111-
context.visitAncestorElements((Element element) {
112-
ancestors.add(element.widget);
113-
return true;
114-
});
115-
if (ancestors.isNotEmpty) {
116-
message.write('The ancestors of this widget were:');
117-
for (Widget ancestor in ancestors)
118-
message.write('\n $ancestor');
119-
} else {
120-
message.writeln(
121-
'This widget is the root of the tree, so it has no '
122-
'ancestors, let alone a "Localizations" ancestor.'
123-
);
124-
}
125-
throw FlutterError(message.toString());
72+
throw FlutterError.fromParts(<DiagnosticsNode>[
73+
ErrorSummary('No MaterialLocalizations found.'),
74+
ErrorDescription(
75+
'${context.widget.runtimeType} widgets require MaterialLocalizations '
76+
'to be provided by a Localizations widget ancestor.'
77+
),
78+
ErrorDescription(
79+
'Localizations are used to generate many different messages, labels,'
80+
'and abbreviations which are used by the material library.'
81+
),
82+
ErrorHint(
83+
'To introduce a MaterialLocalizations, either use a '
84+
'MaterialApp at the root of your application to include them '
85+
'automatically, or add a Localization widget with a '
86+
'MaterialLocalizations delegate.'
87+
),
88+
...context.describeMissingAncestor(expectedAncestorType: MaterialLocalizations)
89+
]);
12690
}
12791
return true;
12892
}());
@@ -145,17 +109,15 @@ bool debugCheckHasMaterialLocalizations(BuildContext context) {
145109
bool debugCheckHasScaffold(BuildContext context) {
146110
assert(() {
147111
if (context.widget is! Scaffold && context.ancestorWidgetOfExactType(Scaffold) == null) {
148-
final Element element = context;
149-
throw FlutterError(
150-
'No Scaffold widget found.\n'
151-
'${context.widget.runtimeType} widgets require a Scaffold widget ancestor.\n'
152-
'The Specific widget that could not find a Scaffold ancestor was:\n'
153-
' ${context.widget}\n'
154-
'The ownership chain for the affected widget is:\n'
155-
' ${element.debugGetCreatorChain(10)}\n'
112+
throw FlutterError.fromParts(<DiagnosticsNode>[
113+
ErrorSummary('No Scaffold widget found.'),
114+
ErrorDescription('${context.widget.runtimeType} widgets require a Scaffold widget ancestor.'),
115+
...context.describeMissingAncestor(expectedAncestorType: Scaffold),
116+
ErrorHint(
156117
'Typically, the Scaffold widget is introduced by the MaterialApp or '
157118
'WidgetsApp widget at the top of your application widget tree.'
158-
);
119+
)
120+
]);
159121
}
160122
return true;
161123
}());

packages/flutter/lib/src/material/page.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,10 @@ class MaterialPageRoute<T> extends PageRoute<T> {
8787
final Widget result = builder(context);
8888
assert(() {
8989
if (result == null) {
90-
throw FlutterError(
91-
'The builder for route "${settings.name}" returned null.\n'
92-
'Route builders must never return null.'
93-
);
90+
throw FlutterError.fromParts(<DiagnosticsNode>[
91+
ErrorSummary('The builder for route "${settings.name}" returned null.'),
92+
ErrorDescription('Route builders must never return null.')
93+
]);
9494
}
9595
return true;
9696
}());

packages/flutter/lib/src/material/scaffold.dart

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,25 +1310,32 @@ class Scaffold extends StatefulWidget {
13101310
final ScaffoldState result = context.ancestorStateOfType(const TypeMatcher<ScaffoldState>());
13111311
if (nullOk || result != null)
13121312
return result;
1313-
throw FlutterError(
1314-
'Scaffold.of() called with a context that does not contain a Scaffold.\n'
1315-
'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). '
1316-
'This usually happens when the context provided is from the same StatefulWidget as that '
1317-
'whose build function actually creates the Scaffold widget being sought.\n'
1318-
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
1319-
'context that is "under" the Scaffold. For an example of this, please see the '
1320-
'documentation for Scaffold.of():\n'
1321-
' https://api.flutter.dev/flutter/material/Scaffold/of.html\n'
1322-
'A more efficient solution is to split your build function into several widgets. This '
1323-
'introduces a new context from which you can obtain the Scaffold. In this solution, '
1324-
'you would have an outer widget that creates the Scaffold populated by instances of '
1325-
'your new inner widgets, and then in these inner widgets you would use Scaffold.of().\n'
1326-
'A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, '
1327-
'then use the key.currentState property to obtain the ScaffoldState rather than '
1328-
'using the Scaffold.of() function.\n'
1329-
'The context used was:\n'
1330-
' $context'
1331-
);
1313+
throw FlutterError.fromParts(<DiagnosticsNode>[
1314+
ErrorSummary(
1315+
'Scaffold.of() called with a context that does not contain a Scaffold.'
1316+
),
1317+
ErrorDescription(
1318+
'No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). '
1319+
'This usually happens when the context provided is from the same StatefulWidget as that '
1320+
'whose build function actually creates the Scaffold widget being sought.'
1321+
),
1322+
ErrorHint(
1323+
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
1324+
'context that is "under" the Scaffold. For an example of this, please see the '
1325+
'documentation for Scaffold.of():\n'
1326+
' https://api.flutter.dev/flutter/material/Scaffold/of.html'
1327+
),
1328+
ErrorHint(
1329+
'A more efficient solution is to split your build function into several widgets. This '
1330+
'introduces a new context from which you can obtain the Scaffold. In this solution, '
1331+
'you would have an outer widget that creates the Scaffold populated by instances of '
1332+
'your new inner widgets, and then in these inner widgets you would use Scaffold.of().\n'
1333+
'A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, '
1334+
'then use the key.currentState property to obtain the ScaffoldState rather than '
1335+
'using the Scaffold.of() function.'
1336+
),
1337+
context.describeElement('The context used was')
1338+
]);
13321339
}
13331340

13341341
/// Returns a [ValueListenable] for the [ScaffoldGeometry] for the closest
@@ -1354,22 +1361,28 @@ class Scaffold extends StatefulWidget {
13541361
static ValueListenable<ScaffoldGeometry> geometryOf(BuildContext context) {
13551362
final _ScaffoldScope scaffoldScope = context.inheritFromWidgetOfExactType(_ScaffoldScope);
13561363
if (scaffoldScope == null)
1357-
throw FlutterError(
1358-
'Scaffold.geometryOf() called with a context that does not contain a Scaffold.\n'
1359-
'This usually happens when the context provided is from the same StatefulWidget as that '
1360-
'whose build function actually creates the Scaffold widget being sought.\n'
1361-
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
1362-
'context that is "under" the Scaffold. For an example of this, please see the '
1363-
'documentation for Scaffold.of():\n'
1364-
' https://api.flutter.dev/flutter/material/Scaffold/of.html\n'
1365-
'A more efficient solution is to split your build function into several widgets. This '
1366-
'introduces a new context from which you can obtain the Scaffold. In this solution, '
1367-
'you would have an outer widget that creates the Scaffold populated by instances of '
1368-
'your new inner widgets, and then in these inner widgets you would use Scaffold.geometryOf().\n'
1369-
'The context used was:\n'
1370-
' $context'
1371-
);
1372-
1364+
throw FlutterError.fromParts(<DiagnosticsNode>[
1365+
ErrorSummary(
1366+
'Scaffold.geometryOf() called with a context that does not contain a Scaffold.'
1367+
),
1368+
ErrorDescription(
1369+
'This usually happens when the context provided is from the same StatefulWidget as that '
1370+
'whose build function actually creates the Scaffold widget being sought.'
1371+
),
1372+
ErrorHint(
1373+
'There are several ways to avoid this problem. The simplest is to use a Builder to get a '
1374+
'context that is "under" the Scaffold. For an example of this, please see the '
1375+
'documentation for Scaffold.of():\n'
1376+
' https://api.flutter.dev/flutter/material/Scaffold/of.html'
1377+
),
1378+
ErrorHint(
1379+
'A more efficient solution is to split your build function into several widgets. This '
1380+
'introduces a new context from which you can obtain the Scaffold. In this solution, '
1381+
'you would have an outer widget that creates the Scaffold populated by instances of '
1382+
'your new inner widgets, and then in these inner widgets you would use Scaffold.geometryOf().',
1383+
),
1384+
context.describeElement('The context used was')
1385+
]);
13731386
return scaffoldScope.geometryNotifier;
13741387
}
13751388

@@ -1679,9 +1692,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
16791692
assert(() {
16801693
if (widget.bottomSheet != null && isPersistent && _currentBottomSheet != null) {
16811694
throw FlutterError(
1682-
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
1683-
'with showBottomSheet() is still visible.\n Rebuild the Scaffold with a null '
1684-
'bottomSheet before calling showBottomSheet().'
1695+
'Scaffold.bottomSheet cannot be specified while a bottom sheet'
1696+
'displayed with showBottomSheet() is still visible.\n'
1697+
'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().'
16851698
);
16861699
}
16871700
return true;
@@ -1818,9 +1831,9 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
18181831
assert(() {
18191832
if (widget.bottomSheet != null) {
18201833
throw FlutterError(
1821-
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
1822-
'with showBottomSheet() is still visible.\n Rebuild the Scaffold with a null '
1823-
'bottomSheet before calling showBottomSheet().'
1834+
'Scaffold.bottomSheet cannot be specified while a bottom sheet'
1835+
'displayed with showBottomSheet() is still visible.\n'
1836+
'Rebuild the Scaffold with a null bottomSheet before calling showBottomSheet().'
18241837
);
18251838
}
18261839
return true;
@@ -1952,12 +1965,17 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
19521965
if (widget.bottomSheet != oldWidget.bottomSheet) {
19531966
assert(() {
19541967
if (widget.bottomSheet != null && _currentBottomSheet?._isLocalHistoryEntry == true) {
1955-
throw FlutterError(
1956-
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
1957-
'with showBottomSheet() is still visible.\n Use the PersistentBottomSheetController '
1958-
'returned by showBottomSheet() to close the old bottom sheet before creating '
1959-
'a Scaffold with a (non null) bottomSheet.'
1960-
);
1968+
throw FlutterError.fromParts(<DiagnosticsNode>[
1969+
ErrorSummary(
1970+
'Scaffold.bottomSheet cannot be specified while a bottom sheet displayed '
1971+
'with showBottomSheet() is still visible.'
1972+
),
1973+
ErrorHint(
1974+
'Use the PersistentBottomSheetController '
1975+
'returned by showBottomSheet() to close the old bottom sheet before creating '
1976+
'a Scaffold with a (non null) bottomSheet.'
1977+
),
1978+
]);
19611979
}
19621980
return true;
19631981
}());

packages/flutter/lib/src/material/stepper.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -672,8 +672,9 @@ class _StepperState extends State<Stepper> with TickerProviderStateMixin {
672672
assert(() {
673673
if (context.ancestorWidgetOfExactType(Stepper) != null)
674674
throw FlutterError(
675-
'Steppers must not be nested. The material specification advises '
676-
'that one should avoid embedding steppers within steppers. '
675+
'Steppers must not be nested.\n'
676+
'The material specification advises that one should avoid embedding '
677+
'steppers within steppers. '
677678
'https://material.io/archive/guidelines/components/steppers.html#steppers-usage'
678679
);
679680
return true;

0 commit comments

Comments
 (0)