Try a mildly prettier FlutterError and make it less drastic in release mode. (#44967)
This commit is contained in:
parent
664350d2a8
commit
b75abd9f7e
@ -479,6 +479,11 @@ class FlutterErrorDetails extends Diagnosticable {
|
|||||||
|
|
||||||
/// Error class used to report Flutter-specific assertion failures and
|
/// Error class used to report Flutter-specific assertion failures and
|
||||||
/// contract violations.
|
/// contract violations.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * <https://flutter.dev/docs/testing/errors>, more information about error
|
||||||
|
/// handling in Flutter.
|
||||||
class FlutterError extends Error with DiagnosticableTreeMixin implements AssertionError {
|
class FlutterError extends Error with DiagnosticableTreeMixin implements AssertionError {
|
||||||
/// Create an error message from a string.
|
/// Create an error message from a string.
|
||||||
///
|
///
|
||||||
|
@ -10,9 +10,6 @@ import 'object.dart';
|
|||||||
const double _kMaxWidth = 100000.0;
|
const double _kMaxWidth = 100000.0;
|
||||||
const double _kMaxHeight = 100000.0;
|
const double _kMaxHeight = 100000.0;
|
||||||
|
|
||||||
// Line length to fit small phones without dynamically checking size.
|
|
||||||
const String _kLine = '\n\n────────────────────\n\n';
|
|
||||||
|
|
||||||
/// A render object used as a placeholder when an error occurs.
|
/// A render object used as a placeholder when an error occurs.
|
||||||
///
|
///
|
||||||
/// The box will be painted in the color given by the
|
/// The box will be painted in the color given by the
|
||||||
@ -25,8 +22,9 @@ const String _kLine = '\n\n─────────────────
|
|||||||
/// [RenderErrorBox.textStyle] and [RenderErrorBox.paragraphStyle] static
|
/// [RenderErrorBox.textStyle] and [RenderErrorBox.paragraphStyle] static
|
||||||
/// properties.
|
/// properties.
|
||||||
///
|
///
|
||||||
/// Again to help simplify the class, this box tries to be 100000.0 pixels wide
|
/// Again to help simplify the class, if the parent has left the constraints
|
||||||
/// and high, to approximate being infinitely high but without using infinities.
|
/// unbounded, this box tries to be 100000.0 pixels wide and high, to
|
||||||
|
/// approximate being infinitely high but without using infinities.
|
||||||
class RenderErrorBox extends RenderBox {
|
class RenderErrorBox extends RenderBox {
|
||||||
/// Creates a RenderErrorBox render object.
|
/// Creates a RenderErrorBox render object.
|
||||||
///
|
///
|
||||||
@ -45,13 +43,10 @@ class RenderErrorBox extends RenderBox {
|
|||||||
// see the paragraph.dart file and the RenderParagraph class.
|
// see the paragraph.dart file and the RenderParagraph class.
|
||||||
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(paragraphStyle);
|
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(paragraphStyle);
|
||||||
builder.pushStyle(textStyle);
|
builder.pushStyle(textStyle);
|
||||||
builder.addText(
|
builder.addText(message);
|
||||||
'$message$_kLine$message$_kLine$message$_kLine$message$_kLine$message$_kLine$message$_kLine'
|
|
||||||
'$message$_kLine$message$_kLine$message$_kLine$message$_kLine$message$_kLine$message'
|
|
||||||
);
|
|
||||||
_paragraph = builder.build();
|
_paragraph = builder.build();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
// Intentionally left empty.
|
// Intentionally left empty.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,39 +77,87 @@ class RenderErrorBox extends RenderBox {
|
|||||||
size = constraints.constrain(const Size(_kMaxWidth, _kMaxHeight));
|
size = constraints.constrain(const Size(_kMaxWidth, _kMaxHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The distance to place around the text.
|
||||||
|
///
|
||||||
|
/// This is intended to ensure that if the [RenderErrorBox] is placed at the top left
|
||||||
|
/// of the screen, under the system's status bar, the error text is still visible in
|
||||||
|
/// the area below the status bar.
|
||||||
|
///
|
||||||
|
/// The padding is ignored if the error box is smaller than the padding.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [minimumWidth], which controls how wide the box must be before the
|
||||||
|
// horizontal padding is applied.
|
||||||
|
static EdgeInsets padding = const EdgeInsets.fromLTRB(64.0, 96.0, 64.0, 12.0);
|
||||||
|
|
||||||
|
/// The width below which the horizontal padding is not applied.
|
||||||
|
///
|
||||||
|
/// If the left and right padding would reduce the available width to less than
|
||||||
|
/// this value, then the text is rendered flush with the left edge.
|
||||||
|
static double minimumWidth = 200.0;
|
||||||
|
|
||||||
/// The color to use when painting the background of [RenderErrorBox] objects.
|
/// The color to use when painting the background of [RenderErrorBox] objects.
|
||||||
static Color backgroundColor = const Color(0xF0900000);
|
///
|
||||||
|
/// Defaults to red in debug mode, a light gray otherwise.
|
||||||
|
static Color backgroundColor = _initBackgroundColor();
|
||||||
|
|
||||||
|
static Color _initBackgroundColor() {
|
||||||
|
Color result = const Color(0xF0C0C0C0);
|
||||||
|
assert(() {
|
||||||
|
result = const Color(0xF0900000);
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/// The text style to use when painting [RenderErrorBox] objects.
|
/// The text style to use when painting [RenderErrorBox] objects.
|
||||||
static ui.TextStyle textStyle = ui.TextStyle(
|
///
|
||||||
color: const Color(0xFFFFFF66),
|
/// Defaults to a yellow monospace font in debug mode, and a dark gray
|
||||||
fontFamily: 'monospace',
|
/// sans-serif font otherwise.
|
||||||
fontSize: 14.0,
|
static ui.TextStyle textStyle = _initTextStyle();
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
);
|
static ui.TextStyle _initTextStyle() {
|
||||||
|
ui.TextStyle result = ui.TextStyle(
|
||||||
|
color: const Color(0xFF303030),
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontSize: 18.0,
|
||||||
|
);
|
||||||
|
assert(() {
|
||||||
|
result = ui.TextStyle(
|
||||||
|
color: const Color(0xFFFFFF66),
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/// The paragraph style to use when painting [RenderErrorBox] objects.
|
/// The paragraph style to use when painting [RenderErrorBox] objects.
|
||||||
static ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle(
|
static ui.ParagraphStyle paragraphStyle = ui.ParagraphStyle(
|
||||||
height: 1.0,
|
textDirection: TextDirection.ltr,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
try {
|
try {
|
||||||
context.canvas.drawRect(offset & size, Paint() .. color = backgroundColor);
|
context.canvas.drawRect(offset & size, Paint() .. color = backgroundColor);
|
||||||
double width;
|
|
||||||
if (_paragraph != null) {
|
if (_paragraph != null) {
|
||||||
// See the comment in the RenderErrorBox constructor. This is not the
|
double width = size.width;
|
||||||
// code you want to be copying and pasting. :-)
|
double left = 0.0;
|
||||||
if (parent is RenderBox) {
|
double top = 0.0;
|
||||||
final RenderBox parentBox = parent;
|
if (width > padding.left + minimumWidth + padding.right) {
|
||||||
width = parentBox.size.width;
|
width -= padding.left + padding.right;
|
||||||
} else {
|
left += padding.left;
|
||||||
width = size.width;
|
|
||||||
}
|
}
|
||||||
_paragraph.layout(ui.ParagraphConstraints(width: width));
|
_paragraph.layout(ui.ParagraphConstraints(width: width));
|
||||||
|
if (size.height > padding.top + _paragraph.height + padding.bottom) {
|
||||||
context.canvas.drawParagraph(_paragraph, offset);
|
top += padding.top;
|
||||||
|
}
|
||||||
|
context.canvas.drawParagraph(_paragraph, offset + Offset(left, top));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Intentionally left empty.
|
// Intentionally left empty.
|
||||||
|
@ -3810,13 +3810,71 @@ typedef ErrorWidgetBuilder = Widget Function(FlutterErrorDetails details);
|
|||||||
/// where the problem lies. Exceptions are also logged to the console, which you
|
/// where the problem lies. Exceptions are also logged to the console, which you
|
||||||
/// can read using `flutter logs`. The console will also include additional
|
/// can read using `flutter logs`. The console will also include additional
|
||||||
/// information such as the stack trace for the exception.
|
/// information such as the stack trace for the exception.
|
||||||
|
///
|
||||||
|
/// It is possible to override this widget.
|
||||||
|
///
|
||||||
|
/// {@tool snippet --template=freeform}
|
||||||
|
/// ```dart
|
||||||
|
/// import 'package:flutter/material.dart';
|
||||||
|
///
|
||||||
|
/// void main() {
|
||||||
|
/// ErrorWidget.builder = (FlutterErrorDetails details) {
|
||||||
|
/// bool inDebug = false;
|
||||||
|
/// assert(() { inDebug = true; return true; }());
|
||||||
|
/// // In debug mode, use the normal error widget which shows
|
||||||
|
/// // the error message:
|
||||||
|
/// if (inDebug)
|
||||||
|
/// return ErrorWidget(details.exception);
|
||||||
|
/// // In release builds, show a yellow-on-blue message instead:
|
||||||
|
/// return Container(
|
||||||
|
/// alignment: Alignment.center,
|
||||||
|
/// child: Text(
|
||||||
|
/// 'Error!',
|
||||||
|
/// style: TextStyle(color: Colors.yellow),
|
||||||
|
/// textDirection: TextDirection.ltr,
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// };
|
||||||
|
/// // Here we would normally runApp() the root widget, but to demonstrate
|
||||||
|
/// // the error handling we artificially fail:
|
||||||
|
/// return runApp(Builder(
|
||||||
|
/// builder: (BuildContext context) {
|
||||||
|
/// throw 'oh no, an error';
|
||||||
|
/// },
|
||||||
|
/// ));
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [FlutterError.onError], which can be set to a method that exits the
|
||||||
|
/// application if that is preferable to showing an error message.
|
||||||
|
/// * <https://flutter.dev/docs/testing/errors>, more information about error
|
||||||
|
/// handling in Flutter.
|
||||||
class ErrorWidget extends LeafRenderObjectWidget {
|
class ErrorWidget extends LeafRenderObjectWidget {
|
||||||
/// Creates a widget that displays the given error message.
|
/// Creates a widget that displays the given exception.
|
||||||
|
///
|
||||||
|
/// The message will be the stringification of the given exception, unless
|
||||||
|
/// computing that value itself throws an exception, in which case it will
|
||||||
|
/// be the string "Error".
|
||||||
|
///
|
||||||
|
/// If this object is inspected from an IDE or the devtools, and the original
|
||||||
|
/// exception is a [FlutterError] object, the original exception itself will
|
||||||
|
/// be shown in the inspection output.
|
||||||
ErrorWidget(Object exception)
|
ErrorWidget(Object exception)
|
||||||
: message = _stringify(exception),
|
: message = _stringify(exception),
|
||||||
_flutterError = exception is FlutterError ? exception : null,
|
_flutterError = exception is FlutterError ? exception : null,
|
||||||
super(key: UniqueKey());
|
super(key: UniqueKey());
|
||||||
|
|
||||||
|
/// Creates a widget that displays the given error message.
|
||||||
|
///
|
||||||
|
/// An explicit [FlutterError] can be provided to be reported to inspection
|
||||||
|
/// tools. It need not match the message.
|
||||||
|
ErrorWidget.withDetails({ this.message = '', FlutterError error })
|
||||||
|
: _flutterError = error,
|
||||||
|
super(key: UniqueKey());
|
||||||
|
|
||||||
/// The configurable factory for [ErrorWidget].
|
/// The configurable factory for [ErrorWidget].
|
||||||
///
|
///
|
||||||
/// When an error occurs while building a widget, the broken widget is
|
/// When an error occurs while building a widget, the broken widget is
|
||||||
@ -3835,17 +3893,28 @@ class ErrorWidget extends LeafRenderObjectWidget {
|
|||||||
/// corresponds to a [RenderBox] that can handle the most absurd of incoming
|
/// corresponds to a [RenderBox] that can handle the most absurd of incoming
|
||||||
/// constraints. The default constructor maps to a [RenderErrorBox].
|
/// constraints. The default constructor maps to a [RenderErrorBox].
|
||||||
///
|
///
|
||||||
|
/// The default behavior is to show the exception's message in debug mode,
|
||||||
|
/// and to show nothing but a gray background in release builds.
|
||||||
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [FlutterError.onError], which is typically called with the same
|
/// * [FlutterError.onError], which is typically called with the same
|
||||||
/// [FlutterErrorDetails] object immediately prior to this callback being
|
/// [FlutterErrorDetails] object immediately prior to this callback being
|
||||||
/// invoked, and which can also be configured to control how errors are
|
/// invoked, and which can also be configured to control how errors are
|
||||||
/// reported.
|
/// reported.
|
||||||
static ErrorWidgetBuilder builder = (FlutterErrorDetails details) => ErrorWidget(details.exception);
|
/// * <https://flutter.dev/docs/testing/errors>, more information about error
|
||||||
|
/// handling in Flutter.
|
||||||
|
static ErrorWidgetBuilder builder = _defaultErrorWidgetBuilder;
|
||||||
|
|
||||||
/// The message to display.
|
static Widget _defaultErrorWidgetBuilder(FlutterErrorDetails details) {
|
||||||
final String message;
|
String message = '';
|
||||||
final FlutterError _flutterError;
|
assert(() {
|
||||||
|
message = _stringify(details.exception) + '\nSee also: https://flutter.dev/docs/testing/errors';
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
final Object exception = details.exception;
|
||||||
|
return ErrorWidget.withDetails(message: message, error: exception is FlutterError ? exception : null);
|
||||||
|
}
|
||||||
|
|
||||||
static String _stringify(Object exception) {
|
static String _stringify(Object exception) {
|
||||||
try {
|
try {
|
||||||
@ -3856,6 +3925,10 @@ class ErrorWidget extends LeafRenderObjectWidget {
|
|||||||
return 'Error';
|
return 'Error';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The message to display.
|
||||||
|
final String message;
|
||||||
|
final FlutterError _flutterError;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderBox createRenderObject(BuildContext context) => RenderErrorBox(message);
|
RenderBox createRenderObject(BuildContext context) => RenderErrorBox(message);
|
||||||
|
|
||||||
|
@ -14,9 +14,45 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('test draw error paragraph', (WidgetTester tester) async {
|
testWidgets('test draw error paragraph', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(ErrorWidget(Exception(errorMessage)));
|
await tester.pumpWidget(ErrorWidget(Exception(errorMessage)));
|
||||||
|
|
||||||
expect(find.byType(ErrorWidget), paints
|
expect(find.byType(ErrorWidget), paints
|
||||||
..rect(rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0))
|
..rect(rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0))
|
||||||
..paragraph(offset: Offset.zero));
|
..paragraph(offset: const Offset(64.0, 96.0)));
|
||||||
|
|
||||||
|
final Widget _error = Builder(builder: (BuildContext context) => throw 'pillow');
|
||||||
|
|
||||||
|
await tester.pumpWidget(Center(child: SizedBox(width: 100.0, child: _error)));
|
||||||
|
expect(tester.takeException(), 'pillow');
|
||||||
|
expect(find.byType(ErrorWidget), paints
|
||||||
|
..rect(rect: const Rect.fromLTWH(0.0, 0.0, 100.0, 600.0))
|
||||||
|
..paragraph(offset: const Offset(0.0, 96.0)));
|
||||||
|
|
||||||
|
await tester.pumpWidget(Center(child: SizedBox(height: 100.0, child: _error)));
|
||||||
|
expect(tester.takeException(), null);
|
||||||
|
|
||||||
|
await tester.pumpWidget(Center(child: SizedBox(key: UniqueKey(), height: 100.0, child: _error)));
|
||||||
|
expect(tester.takeException(), 'pillow');
|
||||||
|
expect(find.byType(ErrorWidget), paints
|
||||||
|
..rect(rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 100.0))
|
||||||
|
..paragraph(offset: const Offset(64.0, 0.0)));
|
||||||
|
|
||||||
|
RenderErrorBox.minimumWidth = 800.0;
|
||||||
|
await tester.pumpWidget(Center(child: _error));
|
||||||
|
expect(tester.takeException(), 'pillow');
|
||||||
|
expect(find.byType(ErrorWidget), paints
|
||||||
|
..rect(rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0))
|
||||||
|
..paragraph(offset: const Offset(0.0, 96.0)));
|
||||||
|
|
||||||
|
await tester.pumpWidget(Center(child: _error));
|
||||||
|
expect(tester.takeException(), null);
|
||||||
|
expect(find.byType(ErrorWidget), paints
|
||||||
|
..rect(color: const Color(0xF0900000))
|
||||||
|
..paragraph());
|
||||||
|
|
||||||
|
RenderErrorBox.backgroundColor = const Color(0xFF112233);
|
||||||
|
await tester.pumpWidget(Center(child: _error));
|
||||||
|
expect(tester.takeException(), null);
|
||||||
|
expect(find.byType(ErrorWidget), paints
|
||||||
|
..rect(color: const Color(0xFF112233))
|
||||||
|
..paragraph());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../rendering/mock_canvas.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('ErrorWidget.builder', (WidgetTester tester) async {
|
testWidgets('ErrorWidget.builder', (WidgetTester tester) async {
|
||||||
final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder;
|
final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder;
|
||||||
@ -24,4 +26,23 @@ void main() {
|
|||||||
expect(find.text('oopsie!'), findsOneWidget);
|
expect(find.text('oopsie!'), findsOneWidget);
|
||||||
ErrorWidget.builder = oldBuilder;
|
ErrorWidget.builder = oldBuilder;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('ErrorWidget.builder', (WidgetTester tester) async {
|
||||||
|
final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder;
|
||||||
|
ErrorWidget.builder = (FlutterErrorDetails details) {
|
||||||
|
return ErrorWidget('');
|
||||||
|
};
|
||||||
|
await tester.pumpWidget(
|
||||||
|
SizedBox(
|
||||||
|
child: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
throw 'test';
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(tester.takeException().toString(), 'test');
|
||||||
|
expect(find.byType(ErrorWidget), isNot(paints..paragraph()));
|
||||||
|
ErrorWidget.builder = oldBuilder;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user