Ensure all errors thrown by image providers can be caught by developers. (#25980)
* Ensure all errors thrown by image providers can be caught by developers. Add an `onError` parameter to the ImageCache.putIfAbsent method. In the event that an error is thrown when resolving an image, catch if this parameter is provided. Use the onError parameter to ensure that all errors thrown are forwarded to the ImageStream error channel instead of directly into the void.
This commit is contained in:
parent
919f457ae9
commit
d84879d910
@ -126,7 +126,12 @@ class ImageCache {
|
|||||||
/// key is moved to the "most recently used" position.
|
/// key is moved to the "most recently used" position.
|
||||||
///
|
///
|
||||||
/// The arguments must not be null. The `loader` cannot return null.
|
/// The arguments must not be null. The `loader` cannot return null.
|
||||||
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
|
///
|
||||||
|
/// In the event that the loader throws an exception, it will be caught only if
|
||||||
|
/// `onError` is also provided. When an exception is caught resolving an image,
|
||||||
|
/// no completers are cached and `null` is returned instead of a new
|
||||||
|
/// completer.
|
||||||
|
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
|
||||||
assert(key != null);
|
assert(key != null);
|
||||||
assert(loader != null);
|
assert(loader != null);
|
||||||
ImageStreamCompleter result = _pendingImages[key];
|
ImageStreamCompleter result = _pendingImages[key];
|
||||||
@ -140,7 +145,16 @@ class ImageCache {
|
|||||||
_cache[key] = image;
|
_cache[key] = image;
|
||||||
return image.completer;
|
return image.completer;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
result = loader();
|
result = loader();
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
if (onError != null) {
|
||||||
|
onError(error, stackTrace);
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
void listener(ImageInfo info, bool syncCall) {
|
void listener(ImageInfo info, bool syncCall) {
|
||||||
// Images that fail to load don't contribute to cache size.
|
// Images that fail to load don't contribute to cache size.
|
||||||
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
|
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
|
||||||
|
@ -262,27 +262,31 @@ abstract class ImageProvider<T> {
|
|||||||
assert(configuration != null);
|
assert(configuration != null);
|
||||||
final ImageStream stream = ImageStream();
|
final ImageStream stream = ImageStream();
|
||||||
T obtainedKey;
|
T obtainedKey;
|
||||||
obtainKey(configuration).then<void>((T key) {
|
Future<void> handleError(dynamic exception, StackTrace stack) async {
|
||||||
obtainedKey = key;
|
await null; // wait an event turn in case a listener has been added to the image stream.
|
||||||
stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
|
final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
|
||||||
}).catchError(
|
stream.setCompleter(imageCompleter);
|
||||||
(dynamic exception, StackTrace stack) async {
|
imageCompleter.setError(
|
||||||
FlutterError.reportError(FlutterErrorDetails(
|
|
||||||
exception: exception,
|
exception: exception,
|
||||||
stack: stack,
|
stack: stack,
|
||||||
library: 'services library',
|
|
||||||
context: 'while resolving an image',
|
context: 'while resolving an image',
|
||||||
silent: true, // could be a network error or whatnot
|
silent: true, // could be a network error or whatnot
|
||||||
informationCollector: (StringBuffer information) {
|
informationCollector: (StringBuffer information) {
|
||||||
information.writeln('Image provider: $this');
|
information.writeln('Image provider: $this');
|
||||||
information.writeln('Image configuration: $configuration');
|
information.writeln('Image configuration: $configuration');
|
||||||
if (obtainedKey != null)
|
if (obtainedKey != null) {
|
||||||
information.writeln('Image key: $obtainedKey');
|
information.writeln('Image key: $obtainedKey');
|
||||||
}
|
}
|
||||||
));
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
obtainKey(configuration).then<void>((T key) {
|
||||||
|
obtainedKey = key;
|
||||||
|
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key), onError: handleError);
|
||||||
|
if (completer != null) {
|
||||||
|
stream.setCompleter(completer);
|
||||||
|
}
|
||||||
|
}).catchError(handleError);
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,7 +499,7 @@ class NetworkImage extends ImageProvider<NetworkImage> {
|
|||||||
if (bytes.lengthInBytes == 0)
|
if (bytes.lengthInBytes == 0)
|
||||||
throw Exception('NetworkImage is an empty file: $resolved');
|
throw Exception('NetworkImage is an empty file: $resolved');
|
||||||
|
|
||||||
return await PaintingBinding.instance.instantiateImageCodec(bytes);
|
return PaintingBinding.instance.instantiateImageCodec(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -773,3 +777,24 @@ class ExactAssetImage extends AssetBundleImageProvider {
|
|||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType(name: "$keyName", scale: $scale, bundle: $bundle)';
|
String toString() => '$runtimeType(name: "$keyName", scale: $scale, bundle: $bundle)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A completer used when resolving an image fails sync.
|
||||||
|
class _ErrorImageCompleter extends ImageStreamCompleter {
|
||||||
|
_ErrorImageCompleter();
|
||||||
|
|
||||||
|
void setError({
|
||||||
|
String context,
|
||||||
|
dynamic exception,
|
||||||
|
StackTrace stack,
|
||||||
|
InformationCollector informationCollector,
|
||||||
|
bool silent = false,
|
||||||
|
}) {
|
||||||
|
reportError(
|
||||||
|
context: context,
|
||||||
|
exception: exception,
|
||||||
|
stack: stack,
|
||||||
|
informationCollector: informationCollector,
|
||||||
|
silent: silent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -128,5 +128,18 @@ void main() {
|
|||||||
expect(imageCache.currentSizeBytes, 256);
|
expect(imageCache.currentSizeBytes, 256);
|
||||||
expect(imageCache.maximumSizeBytes, 256 + 1000);
|
expect(imageCache.maximumSizeBytes, 256 + 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Returns null if an error is caught resolving an image', () {
|
||||||
|
final ErrorImageProvider errorImage = ErrorImageProvider();
|
||||||
|
expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage)), throwsA(isInstanceOf<Error>()));
|
||||||
|
bool caughtError = false;
|
||||||
|
final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage), onError: (dynamic error, StackTrace stackTrace) {
|
||||||
|
caughtError = true;
|
||||||
|
});
|
||||||
|
expect(result, null);
|
||||||
|
expect(caughtError, true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../rendering/rendering_tester.dart';
|
import '../rendering/rendering_tester.dart';
|
||||||
import 'image_data.dart';
|
import 'image_data.dart';
|
||||||
|
import 'mocks_for_image_cache.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestRenderingFlutterBinding(); // initializes the imageCache
|
TestRenderingFlutterBinding(); // initializes the imageCache
|
||||||
@ -53,5 +55,20 @@ void main() {
|
|||||||
expect(otherCache.currentSize, 0);
|
expect(otherCache.currentSize, 0);
|
||||||
expect(imageCache.currentSize, 1);
|
expect(imageCache.currentSize, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ImageProvider errors can always be caught', () async {
|
||||||
|
final ErrorImageProvider imageProvider = ErrorImageProvider();
|
||||||
|
final Completer<bool> caughtError = Completer<bool>();
|
||||||
|
FlutterError.onError = (FlutterErrorDetails details) {
|
||||||
|
caughtError.complete(false);
|
||||||
|
};
|
||||||
|
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
|
||||||
|
stream.addListener((ImageInfo info, bool syncCall) {
|
||||||
|
caughtError.complete(false);
|
||||||
|
}, onError: (dynamic error, StackTrace stackTrace) {
|
||||||
|
caughtError.complete(true);
|
||||||
|
});
|
||||||
|
expect(await caughtError.future, true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -72,3 +72,15 @@ class TestImage implements ui.Image {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ErrorImageProvider extends ImageProvider<ErrorImageProvider> {
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter load(ErrorImageProvider key) {
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ErrorImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture<ErrorImageProvider>(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user