diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart index 02c164a95ff..44afd986d15 100644 --- a/packages/flutter/lib/src/foundation/assertions.dart +++ b/packages/flutter/lib/src/foundation/assertions.dart @@ -140,6 +140,38 @@ class FlutterErrorDetails { longMessage = ' '; return longMessage; } + + @override + String toString() { + final StringBuffer buffer = new StringBuffer(); + if ((library != null && library != '') || (context != null && context != '')) { + if (library != null && library != '') { + buffer.write('Error caught by $library'); + if (context != null && context != '') + buffer.write(', '); + } else { + buffer.writeln('Exception '); + } + if (context != null && context != '') + buffer.write('thrown $context'); + buffer.writeln('.'); + } else { + buffer.write('An error was caught.'); + } + buffer.writeln(exceptionAsString()); + if (informationCollector != null) + informationCollector(buffer); + if (stack != null) { + Iterable stackLines = stack.toString().trimRight().split('\n'); + if (stackFilter != null) { + stackLines = stackFilter(stackLines); + } else { + stackLines = FlutterError.defaultStackFilter(stackLines); + } + buffer.writeAll(stackLines, '\n'); + } + return buffer.toString().trimRight(); + } } /// Error class used to report Flutter-specific assertion failures and diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index be784140339..511e2bba848 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -14,6 +14,7 @@ import 'framework.dart'; export 'package:flutter/animation.dart'; export 'package:flutter/foundation.dart' show ChangeNotifier, + FlutterErrorDetails, Listenable, TargetPlatform, ValueNotifier; diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 435570d4669..3457dcf87fe 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -840,13 +840,14 @@ class RenderObjectToWidgetElement extends RootRenderObje _child = updateChild(_child, widget.child, _rootChildSlot); assert(_child != null); } catch (exception, stack) { - FlutterError.reportError(new FlutterErrorDetails( + final FlutterErrorDetails details = new FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets library', context: 'attaching to the render tree' - )); - final Widget error = new ErrorWidget(exception); + ); + FlutterError.reportError(details); + final Widget error = ErrorWidget.builder(details); _child = updateChild(null, error, _rootChildSlot); } } diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 8dae92fea38..70ada7b363e 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -3456,6 +3456,20 @@ abstract class Element extends DiagnosticableTree implements BuildContext { void performRebuild(); } +/// Signature for the constructor that is called when an error occurs while +/// building a widget. +/// +/// The argument provides information regarding the cause of the error. +/// +/// See also: +/// +/// * [ErrorWidget.builder], which can be set to override the default +/// [ErrorWidget] builder. +/// * [FlutterError.reportError], which is typically called with the same +/// [FlutterErrorDetails] object immediately prior to [ErrorWidget.builder] +/// being called. +typedef Widget ErrorWidgetBuilder(FlutterErrorDetails details); + /// A widget that renders an exception's message. /// /// This widget is used when a build method fails, to help with determining @@ -3467,6 +3481,32 @@ class ErrorWidget extends LeafRenderObjectWidget { ErrorWidget(Object exception) : message = _stringify(exception), super(key: new UniqueKey()); + /// The configurable factory for [ErrorWidget]. + /// + /// When an error occurs while building a widget, the broken widget is + /// replaced by the widget returned by this function. By default, an + /// [ErrorWidget] is returned. + /// + /// The system is typically in an unstable state when this function is called. + /// An exception has just been thrown in the middle of build (and possibly + /// layout), so surrounding widgets and render objects may be in a rather + /// fragile state. The framework itself (especially the [BuildOwner]) may also + /// be confused, and additional exceptions are quite likely to be thrown. + /// + /// Because of this, it is highly recommended that the widget returned from + /// this function perform the least amount of work possible. A + /// [LeafRenderObjectWidget] is the best choice, especially one that + /// corresponds to a [RenderBox] that can handle the most absurd of incoming + /// constraints. The default constructor maps to a [RenderErrorBox]. + /// + /// See also: + /// + /// * [FlutterError.onError], which is typically called with the same + /// [FlutterErrorDetails] object immediately prior to this callback being + /// invoked, and which can also be configured to control how errors are + /// reported. + static ErrorWidgetBuilder builder = (FlutterErrorDetails details) => new ErrorWidget(details.exception); + /// The message to display. final String message; @@ -3544,8 +3584,7 @@ abstract class ComponentElement extends Element { built = build(); debugWidgetBuilderValue(widget, built); } catch (e, stack) { - _debugReportException('building $this', e, stack); - built = new ErrorWidget(e); + built = ErrorWidget.builder(_debugReportException('building $this', e, stack)); } finally { // We delay marking the element as clean until after calling build() so // that attempts to markNeedsBuild() during build() will be ignored. @@ -3556,8 +3595,7 @@ abstract class ComponentElement extends Element { _child = updateChild(_child, built, slot); assert(_child != null); } catch (e, stack) { - _debugReportException('building $this', e, stack); - built = new ErrorWidget(e); + built = ErrorWidget.builder(_debugReportException('building $this', e, stack)); _child = updateChild(null, built, slot); } @@ -4656,14 +4694,19 @@ class _DebugCreator { String toString() => element.debugGetCreatorChain(12); } -void _debugReportException(String context, dynamic exception, StackTrace stack, { +FlutterErrorDetails _debugReportException( + String context, + dynamic exception, + StackTrace stack, { InformationCollector informationCollector }) { - FlutterError.reportError(new FlutterErrorDetails( + final FlutterErrorDetails details = new FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets library', context: context, informationCollector: informationCollector, - )); + ); + FlutterError.reportError(details); + return details; } diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart index a0afd25109b..6b6f321159b 100644 --- a/packages/flutter/lib/src/widgets/layout_builder.dart +++ b/packages/flutter/lib/src/widgets/layout_builder.dart @@ -111,16 +111,14 @@ class _LayoutBuilderElement extends RenderObjectElement { built = widget.builder(this, constraints); debugWidgetBuilderValue(widget, built); } catch (e, stack) { - _debugReportException('building $widget', e, stack); - built = new ErrorWidget(e); + built = ErrorWidget.builder(_debugReportException('building $widget', e, stack)); } } try { _child = updateChild(_child, built, null); assert(_child != null); } catch (e, stack) { - _debugReportException('building $widget', e, stack); - built = new ErrorWidget(e); + built = ErrorWidget.builder(_debugReportException('building $widget', e, stack)); _child = updateChild(null, built, slot); } }); @@ -225,11 +223,17 @@ class _RenderLayoutBuilder extends RenderBox with RenderObjectWithChildMixin log = captureOutput(() { final FlutterErrorDetails details = new FlutterErrorDetails( @@ -42,4 +41,55 @@ void main() { expect(joined, contains('\nExample information\n')); }); + test('FlutterErrorDetails.toString', () { + expect( + new FlutterErrorDetails( + exception: 'MESSAGE', + library: 'LIBRARY', + context: 'CONTEXTING', + informationCollector: (StringBuffer information) { + information.writeln('INFO'); + }, + ).toString(), + 'Error caught by LIBRARY, thrown CONTEXTING.\n' + 'MESSAGE\n' + 'INFO', + ); + expect( + new FlutterErrorDetails( + library: 'LIBRARY', + context: 'CONTEXTING', + informationCollector: (StringBuffer information) { + information.writeln('INFO'); + }, + ).toString(), + 'Error caught by LIBRARY, thrown CONTEXTING.\n' + ' null\n' + 'INFO', + ); + expect( + new FlutterErrorDetails( + exception: 'MESSAGE', + context: 'CONTEXTING', + informationCollector: (StringBuffer information) { + information.writeln('INFO'); + }, + ).toString(), + 'Error caught by Flutter framework, thrown CONTEXTING.\n' + 'MESSAGE\n' + 'INFO', + ); + expect( + const FlutterErrorDetails( + exception: 'MESSAGE', + ).toString(), + 'Error caught by Flutter framework.\n' + 'MESSAGE' + ); + expect( + const FlutterErrorDetails().toString(), + 'Error caught by Flutter framework.\n' + ' null' + ); + }); } diff --git a/packages/flutter/test/widgets/error_widget_builder_test.dart b/packages/flutter/test/widgets/error_widget_builder_test.dart new file mode 100644 index 00000000000..73cc9e775bd --- /dev/null +++ b/packages/flutter/test/widgets/error_widget_builder_test.dart @@ -0,0 +1,25 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets('ErrorWidget.builder', (WidgetTester tester) async { + ErrorWidget.builder = (FlutterErrorDetails details) { + return const Text('oopsie!', textDirection: TextDirection.ltr); + }; + await tester.pumpWidget( + new SizedBox( + child: new Builder( + builder: (BuildContext context) { + throw 'test'; + }, + ), + ), + ); + expect(tester.takeException().toString(), 'test'); + expect(find.text('oopsie!'), findsOneWidget); + }); +}