Unverified Commit 73b2895f authored by Dan Field's avatar Dan Field Committed by GitHub

Add errorBuilder to Image widget (#52481)

parent 1bf9d6f4
......@@ -221,6 +221,14 @@ typedef ImageLoadingBuilder = Widget Function(
ImageChunkEvent loadingProgress,
);
/// Signature used by [Image.errorBuilder] to create a replacement widget to
/// render instead of the image.
typedef ImageErrorWidgetBuilder = Widget Function(
BuildContext context,
Object error,
StackTrace stackTrace,
);
/// A widget that displays an image.
///
/// Several constructors are provided for the various ways that an image can be
......@@ -311,6 +319,7 @@ class Image extends StatefulWidget {
@required this.image,
this.frameBuilder,
this.loadingBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
......@@ -370,6 +379,7 @@ class Image extends StatefulWidget {
double scale = 1.0,
this.frameBuilder,
this.loadingBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
......@@ -423,6 +433,7 @@ class Image extends StatefulWidget {
Key key,
double scale = 1.0,
this.frameBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
......@@ -584,6 +595,7 @@ class Image extends StatefulWidget {
Key key,
AssetBundle bundle,
this.frameBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
double scale,
......@@ -643,6 +655,7 @@ class Image extends StatefulWidget {
Key key,
double scale = 1.0,
this.frameBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
......@@ -822,6 +835,43 @@ class Image extends StatefulWidget {
/// {@animation 400 400 https://flutter.github.io/assets-for-api-docs/assets/widgets/loading_progress_image.mp4}
final ImageLoadingBuilder loadingBuilder;
/// A builder function that is called if an error occurs during image loading.
///
/// If this builder is not provided, any exceptions will be reported to
/// [FlutterError.onError]. If it is provided, the caller should either handle
/// the exception by providing a replacement widget, or rethrow the exception.
///
/// {@tool dartpad --template=stateless_widget_material}
///
/// The following sample uses [errorBuilder] to show a '😢' in place of the
/// image that fails to load, and prints the error to the console.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return DecoratedBox(
/// decoration: BoxDecoration(
/// color: Colors.white,
/// border: Border.all(),
/// borderRadius: BorderRadius.circular(20),
/// ),
/// child: Image.network(
/// 'https://example.does.not.exist/image.jpg',
/// errorBuilder: (BuildContext context, Object exception, StackTrace stackTrace) {
/// // Appropriate logging or analytics, e.g.
/// // myAnalytics.recordError(
/// // 'An error occurred loading "https://example.does.not.exist/image.jpg"',
/// // exception,
/// // stackTrace,
/// // );
/// return Text('😢');
/// },
/// ),
/// );
/// }
/// ```
/// {@end-tool}
final ImageErrorWidgetBuilder errorBuilder;
/// If non-null, require the image to have this width.
///
/// If null, the image will pick a size that best preserves its intrinsic
......@@ -977,6 +1027,8 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
int _frameNumber;
bool _wasSynchronouslyLoaded;
DisposableBuildContext<State<Image>> _scrollAwareContext;
Object _lastException;
StackTrace _lastStack;
@override
void initState() {
......@@ -1054,9 +1106,19 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
_lastException = null;
_lastStack = null;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
onError: widget.errorBuilder != null
? (dynamic error, StackTrace stackTrace) {
setState(() {
_lastException = error;
_lastStack = stackTrace;
});
}
: null,
);
}
......@@ -1116,6 +1178,11 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
if (_lastException != null) {
assert(widget.errorBuilder != null);
return widget.errorBuilder(context, _lastException, _lastStack);
}
Widget result = RawImage(
image: _imageInfo?.image,
width: widget.width,
......
......@@ -1568,6 +1568,58 @@ void main() {
expect(imageCache.statusForKey(provider).live, false);
});
});
testWidgets('errorBuilder - fails on key', (WidgetTester tester) async {
final UniqueKey errorKey = UniqueKey();
Object caughtException;
await tester.pumpWidget(
Image(
image: const FailingImageProvider(failOnObtainKey: true, throws: 'threw'),
errorBuilder: (BuildContext context, Object error, StackTrace stackTrace) {
caughtException = error;
return SizedBox.expand(key: errorKey);
},
),
);
await tester.pump();
expect(find.byKey(errorKey), findsOneWidget);
expect(caughtException.toString(), 'threw');
expect(tester.takeException(), isNull);
});
testWidgets('errorBuilder - fails on load', (WidgetTester tester) async {
final UniqueKey errorKey = UniqueKey();
Object caughtException;
await tester.pumpWidget(
Image(
image: const FailingImageProvider(failOnLoad: true, throws: 'threw'),
errorBuilder: (BuildContext context, Object error, StackTrace stackTrace) {
caughtException = error;
return SizedBox.expand(key: errorKey);
},
),
);
await tester.pump();
expect(find.byKey(errorKey), findsOneWidget);
expect(caughtException.toString(), 'threw');
expect(tester.takeException(), isNull);
});
testWidgets('no errorBuilder - failure reported to FlutterError', (WidgetTester tester) async {
await tester.pumpWidget(
const Image(
image: FailingImageProvider(failOnLoad: true, throws: 'threw'),
),
);
await tester.pump();
expect(tester.takeException(), 'threw');
});
}
class ConfigurationAwareKey {
......@@ -1726,3 +1778,41 @@ class DebouncingImageProvider extends ImageProvider<Object> {
@override
ImageStreamCompleter load(Object key, DecoderCallback decode) => imageProvider.load(key, decode);
}
class FailingImageProvider extends ImageProvider<int> {
const FailingImageProvider({
this.failOnObtainKey = false,
this.failOnLoad = false,
@required this.throws,
}) : assert(failOnLoad != null),
assert(failOnObtainKey != null),
assert(failOnLoad == true || failOnObtainKey == true),
assert(throws != null);
final bool failOnObtainKey;
final bool failOnLoad;
final Object throws;
@override
Future<int> obtainKey(ImageConfiguration configuration) {
if (failOnObtainKey) {
throw throws;
}
return SynchronousFuture<int>(hashCode);
}
@override
ImageStreamCompleter load(int key, DecoderCallback decode) {
if (failOnLoad) {
throw throws;
}
return OneFrameImageStreamCompleter(
Future<ImageInfo>.value(
ImageInfo(
image: TestImage(),
scale: 0,
),
),
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment