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( ...@@ -221,6 +221,14 @@ typedef ImageLoadingBuilder = Widget Function(
ImageChunkEvent loadingProgress, 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. /// A widget that displays an image.
/// ///
/// Several constructors are provided for the various ways that an image can be /// Several constructors are provided for the various ways that an image can be
...@@ -311,6 +319,7 @@ class Image extends StatefulWidget { ...@@ -311,6 +319,7 @@ class Image extends StatefulWidget {
@required this.image, @required this.image,
this.frameBuilder, this.frameBuilder,
this.loadingBuilder, this.loadingBuilder,
this.errorBuilder,
this.semanticLabel, this.semanticLabel,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.width, this.width,
...@@ -370,6 +379,7 @@ class Image extends StatefulWidget { ...@@ -370,6 +379,7 @@ class Image extends StatefulWidget {
double scale = 1.0, double scale = 1.0,
this.frameBuilder, this.frameBuilder,
this.loadingBuilder, this.loadingBuilder,
this.errorBuilder,
this.semanticLabel, this.semanticLabel,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.width, this.width,
...@@ -423,6 +433,7 @@ class Image extends StatefulWidget { ...@@ -423,6 +433,7 @@ class Image extends StatefulWidget {
Key key, Key key,
double scale = 1.0, double scale = 1.0,
this.frameBuilder, this.frameBuilder,
this.errorBuilder,
this.semanticLabel, this.semanticLabel,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.width, this.width,
...@@ -584,6 +595,7 @@ class Image extends StatefulWidget { ...@@ -584,6 +595,7 @@ class Image extends StatefulWidget {
Key key, Key key,
AssetBundle bundle, AssetBundle bundle,
this.frameBuilder, this.frameBuilder,
this.errorBuilder,
this.semanticLabel, this.semanticLabel,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
double scale, double scale,
...@@ -643,6 +655,7 @@ class Image extends StatefulWidget { ...@@ -643,6 +655,7 @@ class Image extends StatefulWidget {
Key key, Key key,
double scale = 1.0, double scale = 1.0,
this.frameBuilder, this.frameBuilder,
this.errorBuilder,
this.semanticLabel, this.semanticLabel,
this.excludeFromSemantics = false, this.excludeFromSemantics = false,
this.width, this.width,
...@@ -822,6 +835,43 @@ class Image extends StatefulWidget { ...@@ -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} /// {@animation 400 400 https://flutter.github.io/assets-for-api-docs/assets/widgets/loading_progress_image.mp4}
final ImageLoadingBuilder loadingBuilder; 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 non-null, require the image to have this width.
/// ///
/// If null, the image will pick a size that best preserves its intrinsic /// If null, the image will pick a size that best preserves its intrinsic
...@@ -977,6 +1027,8 @@ class _ImageState extends State<Image> with WidgetsBindingObserver { ...@@ -977,6 +1027,8 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
int _frameNumber; int _frameNumber;
bool _wasSynchronouslyLoaded; bool _wasSynchronouslyLoaded;
DisposableBuildContext<State<Image>> _scrollAwareContext; DisposableBuildContext<State<Image>> _scrollAwareContext;
Object _lastException;
StackTrace _lastStack;
@override @override
void initState() { void initState() {
...@@ -1054,9 +1106,19 @@ class _ImageState extends State<Image> with WidgetsBindingObserver { ...@@ -1054,9 +1106,19 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) { ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder; loadingBuilder ??= widget.loadingBuilder;
_lastException = null;
_lastStack = null;
return ImageStreamListener( return ImageStreamListener(
_handleImageFrame, _handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk, 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 { ...@@ -1116,6 +1178,11 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_lastException != null) {
assert(widget.errorBuilder != null);
return widget.errorBuilder(context, _lastException, _lastStack);
}
Widget result = RawImage( Widget result = RawImage(
image: _imageInfo?.image, image: _imageInfo?.image,
width: widget.width, width: widget.width,
......
...@@ -1568,6 +1568,58 @@ void main() { ...@@ -1568,6 +1568,58 @@ void main() {
expect(imageCache.statusForKey(provider).live, false); 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 { class ConfigurationAwareKey {
...@@ -1726,3 +1778,41 @@ class DebouncingImageProvider extends ImageProvider<Object> { ...@@ -1726,3 +1778,41 @@ class DebouncingImageProvider extends ImageProvider<Object> {
@override @override
ImageStreamCompleter load(Object key, DecoderCallback decode) => imageProvider.load(key, decode); 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