Unverified Commit 592f81e7 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Add some sanity to the ImageStream listener API (#32936)

The current API was broken in that you registered multiple
callbacks at once, but when you removed listeners, only the
primary listener was used to determine what was removed.
This led to unintuitive cases where the caller could get
unexpected behavior.

This updates the API to add and remove listeners using
a newly introduced [ImageStreamListener] object, a value
object that has references to the individual callbacks
that may fire.

flutter/flutter#24722
flutter/flutter#32374
flutter/flutter#32935
parent d31ce31a
...@@ -44,9 +44,17 @@ class _ImageLoaderState extends State<ImageLoader> { ...@@ -44,9 +44,17 @@ class _ImageLoaderState extends State<ImageLoader> {
// http client. // http client.
final NetworkImage image = NetworkImage('https://github.com/flutter/flutter'); final NetworkImage image = NetworkImage('https://github.com/flutter/flutter');
final ImageStream stream = image.resolve(ImageConfiguration.empty); final ImageStream stream = image.resolve(ImageConfiguration.empty);
stream.addListener((ImageInfo info, bool syncCall) {}, onError: (dynamic error, StackTrace stackTrace) { ImageStreamListener listener;
print('ERROR caught by framework'); listener = ImageStreamListener(
}); (ImageInfo info, bool syncCall) {
stream.removeListener(listener);
},
onError: (dynamic error, StackTrace stackTrace) {
print('ERROR caught by framework');
stream.removeListener(listener);
},
);
stream.addListener(listener);
super.initState(); super.initState();
} }
......
...@@ -238,9 +238,10 @@ class DecorationImagePainter { ...@@ -238,9 +238,10 @@ class DecorationImagePainter {
final ImageStream newImageStream = _details.image.resolve(configuration); final ImageStream newImageStream = _details.image.resolve(configuration);
if (newImageStream.key != _imageStream?.key) { if (newImageStream.key != _imageStream?.key) {
_imageStream?.removeListener(_imageListener); final ImageStreamListener listener = ImageStreamListener(_handleImage);
_imageStream?.removeListener(listener);
_imageStream = newImageStream; _imageStream = newImageStream;
_imageStream.addListener(_imageListener); _imageStream.addListener(listener);
} }
if (_image == null) if (_image == null)
return; return;
...@@ -268,7 +269,7 @@ class DecorationImagePainter { ...@@ -268,7 +269,7 @@ class DecorationImagePainter {
canvas.restore(); canvas.restore();
} }
void _imageListener(ImageInfo value, bool synchronousCall) { void _handleImage(ImageInfo value, bool synchronousCall) {
if (_image == value) if (_image == value)
return; return;
_image = value; _image = value;
...@@ -284,7 +285,7 @@ class DecorationImagePainter { ...@@ -284,7 +285,7 @@ class DecorationImagePainter {
/// After this method has been called, the object is no longer usable. /// After this method has been called, the object is no longer usable.
@mustCallSuper @mustCallSuper
void dispose() { void dispose() {
_imageStream?.removeListener(_imageListener); _imageStream?.removeListener(ImageStreamListener(_handleImage));
} }
@override @override
......
...@@ -183,8 +183,10 @@ class ImageCache { ...@@ -183,8 +183,10 @@ class ImageCache {
_checkCacheSize(); _checkCacheSize();
} }
if (maximumSize > 0 && maximumSizeBytes > 0) { if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, listener); final ImageStreamListener streamListener = ImageStreamListener(listener);
result.addListener(listener); _pendingImages[key] = _PendingImage(result, streamListener);
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
} }
return result; return result;
} }
...@@ -215,7 +217,7 @@ class _PendingImage { ...@@ -215,7 +217,7 @@ class _PendingImage {
_PendingImage(this.completer, this.listener); _PendingImage(this.completer, this.listener);
final ImageStreamCompleter completer; final ImageStreamCompleter completer;
final ImageListener listener; final ImageStreamListener listener;
void removeListener() { void removeListener() {
completer.removeListener(listener); completer.removeListener(listener);
......
...@@ -218,8 +218,9 @@ class ImageConfiguration { ...@@ -218,8 +218,9 @@ class ImageConfiguration {
/// // If the keys are the same, then we got the same image back, and so we don't /// // If the keys are the same, then we got the same image back, and so we don't
/// // need to update the listeners. If the key changed, though, we must make sure /// // need to update the listeners. If the key changed, though, we must make sure
/// // to switch our listeners to the new image stream. /// // to switch our listeners to the new image stream.
/// oldImageStream?.removeListener(_updateImage); /// final ImageStreamListener listener = ImageStreamListener(_updateImage);
/// _imageStream.addListener(_updateImage); /// oldImageStream?.removeListener(listener);
/// _imageStream.addListener(listener);
/// } /// }
/// } /// }
/// ///
...@@ -232,7 +233,7 @@ class ImageConfiguration { ...@@ -232,7 +233,7 @@ class ImageConfiguration {
/// ///
/// @override /// @override
/// void dispose() { /// void dispose() {
/// _imageStream.removeListener(_updateImage); /// _imageStream.removeListener(ImageStreamListener(_updateImage));
/// super.dispose(); /// super.dispose();
/// } /// }
/// ///
......
...@@ -56,9 +56,64 @@ class ImageInfo { ...@@ -56,9 +56,64 @@ class ImageInfo {
} }
} }
/// Interface for receiving notifications about the loading of an image.
///
/// This class overrides `operator ==` and `hashCode` to compare the individual
/// callbacks in the listener, meaning that if you add an instance of this class
/// as a listener (e.g. via [ImageStream.addListener]), you can instantiate a
/// _different_ instance of this class when you remove the listener, and the
/// listener will be properly removed as long all associated callbacks are
/// equal.
///
/// Used by [ImageStream] and [ImageStreamCompleter].
@immutable
class ImageStreamListener {
/// Creates a new [ImageStreamListener].
///
/// The [onImage] parameter must not be null.
const ImageStreamListener(
this.onImage, {
this.onError,
}) : assert(onImage != null);
/// Callback for getting notified that an image is available.
///
/// This callback may fire multiple times (e.g. if the [ImageStreamCompleter]
/// that drives the notifications fires multiple times). An example of such a
/// case would be an image with multiple frames within it (such as an animated
/// GIF).
///
/// For more information on how to interpret the parameters to the callback,
/// see the documentation on [ImageListener].
///
/// See also:
///
/// * [onError], which will be called instead of [onImage] if an error occurs
/// during loading.
final ImageListener onImage;
/// Callback for getting notified when an error occurs while loading an image.
///
/// If an error occurs during loading, [onError] will be called instead of
/// [onImage].
final ImageErrorListener onError;
@override
int get hashCode => hashValues(onImage, onError);
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final ImageStreamListener typedOther = other;
return onImage == typedOther.onImage
&& onError == typedOther.onError;
}
}
/// Signature for callbacks reporting that an image is available. /// Signature for callbacks reporting that an image is available.
/// ///
/// Used by [ImageStream]. /// Used in [ImageStreamListener].
/// ///
/// The `synchronousCall` argument is true if the listener is being invoked /// The `synchronousCall` argument is true if the listener is being invoked
/// during the call to `addListener`. This can be useful if, for example, /// during the call to `addListener`. This can be useful if, for example,
...@@ -70,15 +125,10 @@ typedef ImageListener = void Function(ImageInfo image, bool synchronousCall); ...@@ -70,15 +125,10 @@ typedef ImageListener = void Function(ImageInfo image, bool synchronousCall);
/// Signature for reporting errors when resolving images. /// Signature for reporting errors when resolving images.
/// ///
/// Used by [ImageStream] and [precacheImage] to report errors. /// Used in [ImageStreamListener], as well as by [ImageCache.putIfAbsent] and
/// [precacheImage], to report errors.
typedef ImageErrorListener = void Function(dynamic exception, StackTrace stackTrace); typedef ImageErrorListener = void Function(dynamic exception, StackTrace stackTrace);
class _ImageListenerPair {
_ImageListenerPair(this.listener, this.errorListener);
final ImageListener listener;
final ImageErrorListener errorListener;
}
/// A handle to an image resource. /// A handle to an image resource.
/// ///
/// ImageStream represents a handle to a [dart:ui.Image] object and its scale /// ImageStream represents a handle to a [dart:ui.Image] object and its scale
...@@ -107,7 +157,7 @@ class ImageStream extends Diagnosticable { ...@@ -107,7 +157,7 @@ class ImageStream extends Diagnosticable {
ImageStreamCompleter get completer => _completer; ImageStreamCompleter get completer => _completer;
ImageStreamCompleter _completer; ImageStreamCompleter _completer;
List<_ImageListenerPair> _listeners; List<ImageStreamListener> _listeners;
/// Assigns a particular [ImageStreamCompleter] to this [ImageStream]. /// Assigns a particular [ImageStreamCompleter] to this [ImageStream].
/// ///
...@@ -121,14 +171,9 @@ class ImageStream extends Diagnosticable { ...@@ -121,14 +171,9 @@ class ImageStream extends Diagnosticable {
assert(_completer == null); assert(_completer == null);
_completer = value; _completer = value;
if (_listeners != null) { if (_listeners != null) {
final List<_ImageListenerPair> initialListeners = _listeners; final List<ImageStreamListener> initialListeners = _listeners;
_listeners = null; _listeners = null;
for (_ImageListenerPair listenerPair in initialListeners) { initialListeners.forEach(_completer.addListener);
_completer.addListener(
listenerPair.listener,
onError: listenerPair.errorListener,
);
}
} }
} }
...@@ -139,52 +184,34 @@ class ImageStream extends Diagnosticable { ...@@ -139,52 +184,34 @@ class ImageStream extends Diagnosticable {
/// If the assigned [completer] completes multiple images over its lifetime, /// If the assigned [completer] completes multiple images over its lifetime,
/// this listener will fire multiple times. /// this listener will fire multiple times.
/// ///
/// {@template flutter.painting.imageStream.addListener}
/// The listener will be passed a flag indicating whether a synchronous call /// The listener will be passed a flag indicating whether a synchronous call
/// occurred. If the listener is added within a render object paint function, /// occurred. If the listener is added within a render object paint function,
/// then use this flag to avoid calling [RenderObject.markNeedsPaint] during /// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
/// a paint. /// a paint.
/// ///
/// An [ImageErrorListener] can also optionally be added along with the /// If a duplicate `listener` is registered N times, then it will be called N
/// `listener`. If an error occurred, `onError` will be called instead of /// times when the image stream completes (whether because a new image is
/// `listener`. /// available or because an error occurs). Likewise, to remove all instances
/// /// of the listener, [removeListener] would need to called N times as well.
/// If a `listener` or `onError` handler is registered multiple times, then it /// {@endtemplate}
/// will be called multiple times when the image stream completes (whether void addListener(ImageStreamListener listener) {
/// because a new image is available or because an error occurs,
/// respectively). In general, registering a listener multiple times is
/// discouraged because [removeListener] will remove the first instance that
/// was added, even if it was added with a different `onError` than the
/// intended paired `addListener` call.
void addListener(ImageListener listener, { ImageErrorListener onError }) {
if (_completer != null) if (_completer != null)
return _completer.addListener(listener, onError: onError); return _completer.addListener(listener);
_listeners ??= <_ImageListenerPair>[]; _listeners ??= <ImageStreamListener>[];
_listeners.add(_ImageListenerPair(listener, onError)); _listeners.add(listener);
} }
/// Stop listening for new concrete [ImageInfo] objects and errors from /// Stops listening for events from this stream's [ImageStreamCompleter].
/// the `listener`'s associated [ImageErrorListener]. ///
/// /// If [listener] has been added multiple times, this removes the _first_
/// If `listener` has been added multiple times, this removes the first /// instance of the listener.
/// instance of the listener, along with the `onError` listener that was void removeListener(ImageStreamListener listener) {
/// registered with that first instance. This might not be the instance that
/// the `addListener` corresponding to this `removeListener` had added.
///
/// For example, if one widget calls [addListener] with a global static
/// function and a private error handler, and another widget calls
/// [addListener] with the same global static function but a different private
/// error handler, then the second widget is disposed and removes the image
/// listener (the aforementioned global static function), it will remove the
/// error handler from the first widget, not the second. If an error later
/// occurs, the first widget, which is still supposedly listening, will not
/// receive any messages, while the second, which is supposedly disposed, will
/// have its callback invoked.
void removeListener(ImageListener listener) {
if (_completer != null) if (_completer != null)
return _completer.removeListener(listener); return _completer.removeListener(listener);
assert(_listeners != null); assert(_listeners != null);
for (int i = 0; i < _listeners.length; i += 1) { for (int i = 0; i < _listeners.length; i += 1) {
if (_listeners[i].listener == listener) { if (_listeners[i] == listener) {
_listeners.removeAt(i); _listeners.removeAt(i);
break; break;
} }
...@@ -213,7 +240,7 @@ class ImageStream extends Diagnosticable { ...@@ -213,7 +240,7 @@ class ImageStream extends Diagnosticable {
ifPresent: _completer?.toStringShort(), ifPresent: _completer?.toStringShort(),
ifNull: 'unresolved', ifNull: 'unresolved',
)); ));
properties.add(ObjectFlagProperty<List<_ImageListenerPair>>( properties.add(ObjectFlagProperty<List<ImageStreamListener>>(
'listeners', 'listeners',
_listeners, _listeners,
ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }', ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
...@@ -231,7 +258,7 @@ class ImageStream extends Diagnosticable { ...@@ -231,7 +258,7 @@ class ImageStream extends Diagnosticable {
/// [ImageProvider] subclass will return an [ImageStream] and automatically /// [ImageProvider] subclass will return an [ImageStream] and automatically
/// configure it with the right [ImageStreamCompleter] when possible. /// configure it with the right [ImageStreamCompleter] when possible.
abstract class ImageStreamCompleter extends Diagnosticable { abstract class ImageStreamCompleter extends Diagnosticable {
final List<_ImageListenerPair> _listeners = <_ImageListenerPair>[]; final List<ImageStreamListener> _listeners = <ImageStreamListener>[];
ImageInfo _currentImage; ImageInfo _currentImage;
FlutterErrorDetails _currentError; FlutterErrorDetails _currentError;
...@@ -246,42 +273,27 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -246,42 +273,27 @@ abstract class ImageStreamCompleter extends Diagnosticable {
/// ///
/// Typically this is used by overriding [addListener], checking if /// Typically this is used by overriding [addListener], checking if
/// [hasListeners] is false before calling `super.addListener()`, and if so, /// [hasListeners] is false before calling `super.addListener()`, and if so,
/// starting whatever work is needed to determine when to call /// starting whatever work is needed to determine when to notify listeners;
/// [notifyListeners]; and similarly, by overriding [removeListener], checking /// and similarly, by overriding [removeListener], checking if [hasListeners]
/// if [hasListeners] is false after calling `super.removeListener()`, and if /// is false after calling `super.removeListener()`, and if so, stopping that
/// so, stopping that same work. /// same work.
@protected @protected
bool get hasListeners => _listeners.isNotEmpty; bool get hasListeners => _listeners.isNotEmpty;
/// Adds a listener callback that is called whenever a new concrete [ImageInfo] /// Adds a listener callback that is called whenever a new concrete [ImageInfo]
/// object is available or an error is reported. If a concrete image is /// object is available or an error is reported. If a concrete image is
/// already available, or if an error has been already reported, this object /// already available, or if an error has been already reported, this object
/// will call the listener or error listener synchronously. /// will notify the listener synchronously.
/// ///
/// If the [ImageStreamCompleter] completes multiple images over its lifetime, /// If the [ImageStreamCompleter] completes multiple images over its lifetime,
/// this listener will fire multiple times. /// this listener's [ImageStreamListener.onImage] will fire multiple times.
///
/// The listener will be passed a flag indicating whether a synchronous call
/// occurred. If the listener is added within a render object paint function,
/// then use this flag to avoid calling [RenderObject.markNeedsPaint] during
/// a paint.
/// ///
/// An [ImageErrorListener] can also optionally be added along with the /// {@macro flutter.painting.imageStream.addListener}
/// `listener`. If an error occurred, `onError` will be called instead of void addListener(ImageStreamListener listener) {
/// `listener`. _listeners.add(listener);
///
/// If a `listener` or `onError` handler is registered multiple times, then it
/// will be called multiple times when the image stream completes (whether
/// because a new image is available or because an error occurs,
/// respectively). In general, registering a listener multiple times is
/// discouraged because [removeListener] will remove the first instance that
/// was added, even if it was added with a different `onError` than the
/// intended paired `addListener` call.
void addListener(ImageListener listener, { ImageErrorListener onError }) {
_listeners.add(_ImageListenerPair(listener, onError));
if (_currentImage != null) { if (_currentImage != null) {
try { try {
listener(_currentImage, true); listener.onImage(_currentImage, true);
} catch (exception, stack) { } catch (exception, stack) {
reportError( reportError(
context: ErrorDescription('by a synchronously-called image listener'), context: ErrorDescription('by a synchronously-called image listener'),
...@@ -290,9 +302,9 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -290,9 +302,9 @@ abstract class ImageStreamCompleter extends Diagnosticable {
); );
} }
} }
if (_currentError != null && onError != null) { if (_currentError != null && listener.onError != null) {
try { try {
onError(_currentError.exception, _currentError.stack); listener.onError(_currentError.exception, _currentError.stack);
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError( FlutterError.reportError(
FlutterErrorDetails( FlutterErrorDetails(
...@@ -306,16 +318,13 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -306,16 +318,13 @@ abstract class ImageStreamCompleter extends Diagnosticable {
} }
} }
/// Stop listening for new concrete [ImageInfo] objects and errors from /// Stops the specified [listener] from receiving image stream events.
/// its associated [ImageErrorListener].
/// ///
/// If `listener` has been added multiple times, this removes the first /// If [listener] has been added multiple times, this removes the _first_
/// instance of the listener, along with the `onError` listener that was /// instance of the listener.
/// registered with that first instance. This might not be the instance that void removeListener(ImageStreamListener listener) {
/// the `addListener` corresponding to this `removeListener` had added.
void removeListener(ImageListener listener) {
for (int i = 0; i < _listeners.length; i += 1) { for (int i = 0; i < _listeners.length; i += 1) {
if (_listeners[i].listener == listener) { if (_listeners[i] == listener) {
_listeners.removeAt(i); _listeners.removeAt(i);
break; break;
} }
...@@ -328,12 +337,11 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -328,12 +337,11 @@ abstract class ImageStreamCompleter extends Diagnosticable {
_currentImage = image; _currentImage = image;
if (_listeners.isEmpty) if (_listeners.isEmpty)
return; return;
final List<ImageListener> localListeners = _listeners.map<ImageListener>( final List<ImageStreamListener> localListeners =
(_ImageListenerPair listenerPair) => listenerPair.listener List<ImageStreamListener>.from(_listeners);
).toList(); for (ImageStreamListener listener in localListeners) {
for (ImageListener listener in localListeners) {
try { try {
listener(image, false); listener.onImage(image, false);
} catch (exception, stack) { } catch (exception, stack) {
reportError( reportError(
context: ErrorDescription('by an image listener'), context: ErrorDescription('by an image listener'),
...@@ -347,8 +355,8 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -347,8 +355,8 @@ abstract class ImageStreamCompleter extends Diagnosticable {
/// Calls all the registered error listeners to notify them of an error that /// Calls all the registered error listeners to notify them of an error that
/// occurred while resolving the image. /// occurred while resolving the image.
/// ///
/// If no error listeners are attached, a [FlutterError] will be reported /// If no error listeners (listeners with an [ImageStreamListener.onError]
/// instead. /// specified) are attached, a [FlutterError] will be reported instead.
/// ///
/// The `context` should be a string describing where the error was caught, in /// The `context` should be a string describing where the error was caught, in
/// a form that will make sense in English when following the word "thrown", /// a form that will make sense in English when following the word "thrown",
...@@ -389,12 +397,10 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -389,12 +397,10 @@ abstract class ImageStreamCompleter extends Diagnosticable {
silent: silent, silent: silent,
); );
final List<ImageErrorListener> localErrorListeners = final List<ImageErrorListener> localErrorListeners = _listeners
_listeners.map<ImageErrorListener>( .map<ImageErrorListener>((ImageStreamListener listener) => listener.onError)
(_ImageListenerPair listenerPair) => listenerPair.errorListener .where((ImageErrorListener errorListener) => errorListener != null)
).where( .toList();
(ImageErrorListener errorListener) => errorListener != null
).toList();
if (localErrorListeners.isEmpty) { if (localErrorListeners.isEmpty) {
FlutterError.reportError(_currentError); FlutterError.reportError(_currentError);
...@@ -422,7 +428,7 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -422,7 +428,7 @@ abstract class ImageStreamCompleter extends Diagnosticable {
void debugFillProperties(DiagnosticPropertiesBuilder description) { void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description); super.debugFillProperties(description);
description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false)); description.add(DiagnosticsProperty<ImageInfo>('current', _currentImage, ifNull: 'unresolved', showName: false));
description.add(ObjectFlagProperty<List<_ImageListenerPair>>( description.add(ObjectFlagProperty<List<ImageStreamListener>>(
'listeners', 'listeners',
_listeners, _listeners,
ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }', ifPresent: '${_listeners?.length} listener${_listeners?.length == 1 ? "" : "s" }',
...@@ -610,14 +616,14 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter { ...@@ -610,14 +616,14 @@ class MultiFrameImageStreamCompleter extends ImageStreamCompleter {
} }
@override @override
void addListener(ImageListener listener, { ImageErrorListener onError }) { void addListener(ImageStreamListener listener) {
if (!hasListeners && _codec != null) if (!hasListeners && _codec != null)
_decodeNextFrameAndSchedule(); _decodeNextFrameAndSchedule();
super.addListener(listener, onError: onError); super.addListener(listener);
} }
@override @override
void removeListener(ImageListener listener) { void removeListener(ImageStreamListener listener) {
super.removeListener(listener); super.removeListener(listener);
if (!hasListeners) { if (!hasListeners) {
_timer?.cancel(); _timer?.cancel();
......
...@@ -374,8 +374,9 @@ class _ImageProviderResolver { ...@@ -374,8 +374,9 @@ class _ImageProviderResolver {
assert(_imageStream != null); assert(_imageStream != null);
if (_imageStream.key != oldImageStream?.key) { if (_imageStream.key != oldImageStream?.key) {
oldImageStream?.removeListener(_handleImageChanged); final ImageStreamListener listener = ImageStreamListener(_handleImageChanged);
_imageStream.addListener(_handleImageChanged); oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
} }
} }
...@@ -385,7 +386,7 @@ class _ImageProviderResolver { ...@@ -385,7 +386,7 @@ class _ImageProviderResolver {
} }
void stopListening() { void stopListening() {
_imageStream?.removeListener(_handleImageChanged); _imageStream?.removeListener(ImageStreamListener(_handleImageChanged));
} }
} }
......
...@@ -82,26 +82,29 @@ Future<void> precacheImage( ...@@ -82,26 +82,29 @@ Future<void> precacheImage(
final ImageConfiguration config = createLocalImageConfiguration(context, size: size); final ImageConfiguration config = createLocalImageConfiguration(context, size: size);
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
final ImageStream stream = provider.resolve(config); final ImageStream stream = provider.resolve(config);
void listener(ImageInfo image, bool sync) { ImageStreamListener listener;
completer.complete(); listener = ImageStreamListener(
stream.removeListener(listener); (ImageInfo image, bool sync) {
} completer.complete();
void errorListener(dynamic exception, StackTrace stackTrace) { stream.removeListener(listener);
completer.complete(); },
stream.removeListener(listener); onError: (dynamic exception, StackTrace stackTrace) {
if (onError != null) { completer.complete();
onError(exception, stackTrace); stream.removeListener(listener);
} else { if (onError != null) {
FlutterError.reportError(FlutterErrorDetails( onError(exception, stackTrace);
context: ErrorDescription('image failed to precache'), } else {
library: 'image resource service', FlutterError.reportError(FlutterErrorDetails(
exception: exception, context: ErrorDescription('image failed to precache'),
stack: stackTrace, library: 'image resource service',
silent: true, exception: exception,
)); stack: stackTrace,
} silent: true,
} ));
stream.addListener(listener, onError: errorListener); }
},
);
stream.addListener(listener);
return completer.future; return completer.future;
} }
...@@ -656,28 +659,30 @@ class _ImageState extends State<Image> { ...@@ -656,28 +659,30 @@ class _ImageState extends State<Image> {
if (_imageStream?.key == newStream?.key) if (_imageStream?.key == newStream?.key)
return; return;
final ImageStreamListener listener = ImageStreamListener(_handleImageChanged);
if (_isListeningToStream) if (_isListeningToStream)
_imageStream.removeListener(_handleImageChanged); _imageStream.removeListener(listener);
if (!widget.gaplessPlayback) if (!widget.gaplessPlayback)
setState(() { _imageInfo = null; }); setState(() { _imageInfo = null; });
_imageStream = newStream; _imageStream = newStream;
if (_isListeningToStream) if (_isListeningToStream)
_imageStream.addListener(_handleImageChanged); _imageStream.addListener(listener);
} }
void _listenToStream() { void _listenToStream() {
if (_isListeningToStream) if (_isListeningToStream)
return; return;
_imageStream.addListener(_handleImageChanged); _imageStream.addListener(ImageStreamListener(_handleImageChanged));
_isListeningToStream = true; _isListeningToStream = true;
} }
void _stopListeningToStream() { void _stopListeningToStream() {
if (!_isListeningToStream) if (!_isListeningToStream)
return; return;
_imageStream.removeListener(_handleImageChanged); _imageStream.removeListener(ImageStreamListener(_handleImageChanged));
_isListeningToStream = false; _isListeningToStream = false;
} }
......
...@@ -199,10 +199,15 @@ void main() { ...@@ -199,10 +199,15 @@ void main() {
test('failed image can successfully be removed from the cache\'s pending images', () async { test('failed image can successfully be removed from the cache\'s pending images', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
const FailingTestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty).addListener((ImageInfo image, bool synchronousCall) { }, onError: (dynamic exception, StackTrace stackTrace) { const FailingTestImageProvider(1, 1, image: testImage)
final bool evicationResult = imageCache.evict(1); .resolve(ImageConfiguration.empty)
expect(evicationResult, isTrue); .addListener(ImageStreamListener(
}); (ImageInfo image, bool synchronousCall) { },
onError: (dynamic exception, StackTrace stackTrace) {
final bool evicationResult = imageCache.evict(1);
expect(evicationResult, isTrue);
},
));
}); });
}); });
} }
......
...@@ -31,7 +31,7 @@ void main() { ...@@ -31,7 +31,7 @@ void main() {
final MemoryImage imageProvider = MemoryImage(bytes); final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
stream.addListener((ImageInfo info, bool syncCall) => completer.complete()); stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) => completer.complete()));
await completer.future; await completer.future;
expect(imageCache.currentSize, 1); expect(imageCache.currentSize, 1);
...@@ -46,7 +46,7 @@ void main() { ...@@ -46,7 +46,7 @@ void main() {
otherCache.putIfAbsent(imageProvider, () => imageProvider.load(imageProvider)); otherCache.putIfAbsent(imageProvider, () => imageProvider.load(imageProvider));
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
stream.addListener((ImageInfo info, bool syncCall) => completer.complete()); stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) => completer.complete()));
await completer.future; await completer.future;
expect(otherCache.currentSize, 1); expect(otherCache.currentSize, 1);
...@@ -63,11 +63,11 @@ void main() { ...@@ -63,11 +63,11 @@ void main() {
caughtError.complete(false); caughtError.complete(false);
}; };
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener((ImageInfo info, bool syncCall) { stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false); caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true); caughtError.complete(true);
}); }));
expect(await caughtError.future, true); expect(await caughtError.future, true);
}); });
}); });
...@@ -79,11 +79,11 @@ void main() { ...@@ -79,11 +79,11 @@ void main() {
caughtError.complete(false); caughtError.complete(false);
}; };
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener((ImageInfo info, bool syncCall) { stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false); caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true); caughtError.complete(true);
}); }));
expect(await caughtError.future, true); expect(await caughtError.future, true);
}); });
...@@ -101,10 +101,10 @@ void main() { ...@@ -101,10 +101,10 @@ void main() {
throw Error(); throw Error();
}; };
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty); final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((ImageInfo info, bool syncCall) { result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true); caughtError.complete(true);
}); }));
expect(await caughtError.future, true); expect(await caughtError.future, true);
}); });
expect(uncaught, false); expect(uncaught, false);
...@@ -124,10 +124,10 @@ void main() { ...@@ -124,10 +124,10 @@ void main() {
throw Error(); throw Error();
}; };
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty); final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((ImageInfo info, bool syncCall) { result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true); caughtError.complete(true);
}); }));
expect(await caughtError.future, true); expect(await caughtError.future, true);
}); });
expect(uncaught, false); expect(uncaught, false);
...@@ -158,12 +158,12 @@ void main() { ...@@ -158,12 +158,12 @@ void main() {
Future<void> loadNetworkImage() async { Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo')); final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final ImageStreamCompleter completer = networkImage.load(networkImage); final ImageStreamCompleter completer = networkImage.load(networkImage);
completer.addListener( completer.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { }, (ImageInfo image, bool synchronousCall) { },
onError: (dynamic error, StackTrace stackTrace) { onError: (dynamic error, StackTrace stackTrace) {
capturedErrors.add(error); capturedErrors.add(error);
}, },
); ));
await Future<void>.value(); await Future<void>.value();
} }
...@@ -187,10 +187,10 @@ void main() { ...@@ -187,10 +187,10 @@ void main() {
throw Error(); throw Error();
}; };
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty); final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((ImageInfo info, bool syncCall) { result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true); caughtError.complete(true);
}); }));
expect(await caughtError.future, true); expect(await caughtError.future, true);
}, zoneSpecification: ZoneSpecification( }, zoneSpecification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) { handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
......
...@@ -103,7 +103,7 @@ void main() { ...@@ -103,7 +103,7 @@ void main() {
expect(mockCodec.numFramesAsked, 0); expect(mockCodec.numFramesAsked, 0);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
await tester.idle(); await tester.idle();
expect(mockCodec.numFramesAsked, 1); expect(mockCodec.numFramesAsked, 1);
}); });
...@@ -118,7 +118,7 @@ void main() { ...@@ -118,7 +118,7 @@ void main() {
); );
final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
await tester.idle(); await tester.idle();
expect(mockCodec.numFramesAsked, 0); expect(mockCodec.numFramesAsked, 0);
...@@ -138,7 +138,7 @@ void main() { ...@@ -138,7 +138,7 @@ void main() {
); );
final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
// MultiFrameImageStreamCompleter only sets an error handler for the next // MultiFrameImageStreamCompleter only sets an error handler for the next
// frame future after the codec future has completed. // frame future after the codec future has completed.
...@@ -163,9 +163,9 @@ void main() { ...@@ -163,9 +163,9 @@ void main() {
); );
final List<ImageInfo> emittedImages = <ImageInfo>[]; final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) { imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image); emittedImages.add(image);
}); }));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -189,9 +189,9 @@ void main() { ...@@ -189,9 +189,9 @@ void main() {
); );
final List<ImageInfo> emittedImages = <ImageInfo>[]; final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) { imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image); emittedImages.add(image);
}); }));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -237,9 +237,9 @@ void main() { ...@@ -237,9 +237,9 @@ void main() {
); );
final List<ImageInfo> emittedImages = <ImageInfo>[]; final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) { imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image); emittedImages.add(image);
}); }));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -280,9 +280,9 @@ void main() { ...@@ -280,9 +280,9 @@ void main() {
); );
final List<ImageInfo> emittedImages = <ImageInfo>[]; final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) { imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image); emittedImages.add(image);
}); }));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -320,7 +320,7 @@ void main() { ...@@ -320,7 +320,7 @@ void main() {
); );
final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -332,7 +332,7 @@ void main() { ...@@ -332,7 +332,7 @@ void main() {
await tester.idle(); // let nextFrameFuture complete await tester.idle(); // let nextFrameFuture complete
await tester.pump(); // first animation frame shows on first app frame. await tester.pump(); // first animation frame shows on first app frame.
mockCodec.completeNextFrame(frame2); mockCodec.completeNextFrame(frame2);
imageStream.removeListener(listener); imageStream.removeListener(ImageStreamListener(listener));
await tester.idle(); // let nextFrameFuture complete await tester.idle(); // let nextFrameFuture complete
await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame. await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
...@@ -340,7 +340,7 @@ void main() { ...@@ -340,7 +340,7 @@ void main() {
// listeners to the stream // listeners to the stream
expect(mockCodec.numFramesAsked, 2); expect(mockCodec.numFramesAsked, 2);
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
await tester.idle(); // let nextFrameFuture complete await tester.idle(); // let nextFrameFuture complete
expect(mockCodec.numFramesAsked, 3); expect(mockCodec.numFramesAsked, 3);
}); });
...@@ -364,8 +364,8 @@ void main() { ...@@ -364,8 +364,8 @@ void main() {
final ImageListener listener2 = (ImageInfo image, bool synchronousCall) { final ImageListener listener2 = (ImageInfo image, bool synchronousCall) {
emittedImages2.add(image); emittedImages2.add(image);
}; };
imageStream.addListener(listener1); imageStream.addListener(ImageStreamListener(listener1));
imageStream.addListener(listener2); imageStream.addListener(ImageStreamListener(listener2));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -382,7 +382,7 @@ void main() { ...@@ -382,7 +382,7 @@ void main() {
mockCodec.completeNextFrame(frame2); mockCodec.completeNextFrame(frame2);
await tester.idle(); // let nextFrameFuture complete await tester.idle(); // let nextFrameFuture complete
await tester.pump(); // next app frame will schedule a timer. await tester.pump(); // next app frame will schedule a timer.
imageStream.removeListener(listener1); imageStream.removeListener(ImageStreamListener(listener1));
await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame. await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)])); expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
...@@ -404,7 +404,7 @@ void main() { ...@@ -404,7 +404,7 @@ void main() {
); );
final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -420,7 +420,7 @@ void main() { ...@@ -420,7 +420,7 @@ void main() {
await tester.idle(); // let nextFrameFuture complete await tester.idle(); // let nextFrameFuture complete
await tester.pump(); await tester.pump();
imageStream.removeListener(listener); imageStream.removeListener(ImageStreamListener(listener));
// The test framework will fail this if there are pending timers at this // The test framework will fail this if there are pending timers at this
// point. // point.
}); });
...@@ -437,7 +437,7 @@ void main() { ...@@ -437,7 +437,7 @@ void main() {
); );
final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); await tester.idle();
...@@ -476,10 +476,10 @@ void main() { ...@@ -476,10 +476,10 @@ void main() {
capturedException = exception; capturedException = exception;
}; };
streamUnderTest.addListener( streamUnderTest.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { }, (ImageInfo image, bool synchronousCall) { },
onError: errorListener, onError: errorListener,
); ));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
// MultiFrameImageStreamCompleter only sets an error handler for the next // MultiFrameImageStreamCompleter only sets an error handler for the next
...@@ -508,14 +508,14 @@ void main() { ...@@ -508,14 +508,14 @@ void main() {
); );
final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec); codecCompleter.complete(mockCodec);
await tester.idle(); // let nextFrameFuture complete await tester.idle(); // let nextFrameFuture complete
imageStream.removeListener(listener); imageStream.removeListener(ImageStreamListener(listener));
imageStream.addListener(listener); imageStream.addListener(ImageStreamListener(listener));
final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200)); final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
...@@ -541,7 +541,7 @@ void main() { ...@@ -541,7 +541,7 @@ void main() {
// ); // );
// //
// final ImageListener listener = (ImageInfo image, bool synchronousCall) { }; // final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
// imageStream.addListener(listener); // imageStream.addListener(ImageLoadingListener(listener));
// //
// codecCompleter.complete(mockCodec); // codecCompleter.complete(mockCodec);
// await tester.idle(); // await tester.idle();
...@@ -560,7 +560,7 @@ void main() { ...@@ -560,7 +560,7 @@ void main() {
// tester.flushTimers(); // tester.flushTimers();
// //
// imageStream.removeListener(listener); // imageStream.removeListener(listener);
// imageStream.addListener(listener); // imageStream.addListener(ImageLoadingListener(listener));
// //
// mockCodec.completeNextFrame(frame3); // mockCodec.completeNextFrame(frame3);
// await tester.idle(); // let nextFrameFuture complete // await tester.idle(); // let nextFrameFuture complete
......
...@@ -58,10 +58,11 @@ class FailingTestImageProvider extends TestImageProvider { ...@@ -58,10 +58,11 @@ class FailingTestImageProvider extends TestImageProvider {
Future<ImageInfo> extractOneFrame(ImageStream stream) { Future<ImageInfo> extractOneFrame(ImageStream stream) {
final Completer<ImageInfo> completer = Completer<ImageInfo>(); final Completer<ImageInfo> completer = Completer<ImageInfo>();
void listener(ImageInfo image, bool synchronousCall) { ImageStreamListener listener;
listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
completer.complete(image); completer.complete(image);
stream.removeListener(listener); stream.removeListener(listener);
} });
stream.addListener(listener); stream.addListener(listener);
return completer.future; return completer.future;
} }
......
...@@ -331,7 +331,7 @@ void main() { ...@@ -331,7 +331,7 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final TestImageProvider imageProvider = TestImageProvider();
imageProvider._streamCompleter.addListener(listener, onError: errorListener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
ImageConfiguration configuration; ImageConfiguration configuration;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -399,7 +399,7 @@ void main() { ...@@ -399,7 +399,7 @@ void main() {
expect(reportedException, testException); expect(reportedException, testException);
expect(reportedStackTrace, testStack); expect(reportedStackTrace, testStack);
streamUnderTest.addListener(listener, onError: errorListener); streamUnderTest.addListener(ImageStreamListener(listener, onError: errorListener));
expect(capturedImage, isNull); // The image stream listeners should never be called. expect(capturedImage, isNull); // The image stream listeners should never be called.
// The image stream error handler should have the original exception. // The image stream error handler should have the original exception.
...@@ -422,9 +422,9 @@ void main() { ...@@ -422,9 +422,9 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final TestImageProvider imageProvider = TestImageProvider();
imageProvider._streamCompleter.addListener(listener, onError: errorListener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Add the exact same listener a second time without the errorListener. // Add the exact same listener a second time without the errorListener.
imageProvider._streamCompleter.addListener(listener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener));
ImageConfiguration configuration; ImageConfiguration configuration;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -466,9 +466,9 @@ void main() { ...@@ -466,9 +466,9 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final TestImageProvider imageProvider = TestImageProvider();
imageProvider._streamCompleter.addListener(listener, onError: errorListener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Add the exact same errorListener a second time. // Add the exact same errorListener a second time.
imageProvider._streamCompleter.addListener(null, onError: errorListener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
ImageConfiguration configuration; ImageConfiguration configuration;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -494,29 +494,27 @@ void main() { ...@@ -494,29 +494,27 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('Error listeners are removed along with listeners', (WidgetTester tester) async { testWidgets('Listeners are only removed if callback tuple matches', (WidgetTester tester) async {
bool errorListenerCalled = false; bool errorListenerCalled = false;
dynamic reportedException; dynamic reportedException;
StackTrace reportedStackTrace; StackTrace reportedStackTrace;
ImageInfo capturedImage; ImageInfo capturedImage;
final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) { final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
errorListenerCalled = true; errorListenerCalled = true;
reportedException = exception;
reportedStackTrace = stackTrace;
}; };
final ImageListener listener = (ImageInfo info, bool synchronous) { final ImageListener listener = (ImageInfo info, bool synchronous) {
capturedImage = info; capturedImage = info;
}; };
FlutterError.onError = (FlutterErrorDetails flutterError) {
reportedException = flutterError.exception;
reportedStackTrace = flutterError.stack;
};
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final TestImageProvider imageProvider = TestImageProvider();
imageProvider._streamCompleter.addListener(listener, onError: errorListener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Now remove the listener the error listener is attached to. // Now remove the listener the error listener is attached to.
// Don't explicitly remove the error listener. // Don't explicitly remove the error listener.
imageProvider._streamCompleter.removeListener(listener); imageProvider._streamCompleter.removeListener(ImageStreamListener(listener));
ImageConfiguration configuration; ImageConfiguration configuration;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -534,8 +532,7 @@ void main() { ...@@ -534,8 +532,7 @@ void main() {
await tester.idle(); // Let the failed completer's future hit the stream completer. await tester.idle(); // Let the failed completer's future hit the stream completer.
expect(tester.binding.microtaskCount, 0); expect(tester.binding.microtaskCount, 0);
expect(errorListenerCalled, false); expect(errorListenerCalled, true);
// Since the error listener is removed, bubble up to FlutterError.
expect(reportedException, testException); expect(reportedException, testException);
expect(reportedStackTrace, testStack); expect(reportedStackTrace, testStack);
expect(capturedImage, isNull); // The image stream listeners should never be called. expect(capturedImage, isNull); // The image stream listeners should never be called.
...@@ -554,12 +551,12 @@ void main() { ...@@ -554,12 +551,12 @@ void main() {
final Exception testException = Exception('cannot resolve host'); final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current; final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider(); final TestImageProvider imageProvider = TestImageProvider();
imageProvider._streamCompleter.addListener(listener, onError: errorListener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Duplicates the same set of listener and errorListener. // Duplicates the same set of listener and errorListener.
imageProvider._streamCompleter.addListener(listener, onError: errorListener); imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
// Now remove one entry of the specified listener and associated error listener. // Now remove one entry of the specified listener and associated error listener.
// Don't explicitly remove the error listener. // Don't explicitly remove the error listener.
imageProvider._streamCompleter.removeListener(listener); imageProvider._streamCompleter.removeListener(ImageStreamListener(listener, onError: errorListener));
ImageConfiguration configuration; ImageConfiguration configuration;
await tester.pumpWidget( await tester.pumpWidget(
Builder( Builder(
...@@ -581,58 +578,6 @@ void main() { ...@@ -581,58 +578,6 @@ void main() {
expect(capturedImage, isNull); // The image stream listeners should never be called. expect(capturedImage, isNull); // The image stream listeners should never be called.
}); });
testWidgets('Removing listener FIFO removes exactly one listener and error listener', (WidgetTester tester) async {
// To make sure that a single listener removal doesn't only happen
// accidentally as described in https://github.com/flutter/flutter/pull/25865#discussion_r244851565.
int errorListener1Called = 0;
int errorListener2Called = 0;
int errorListener3Called = 0;
ImageInfo capturedImage;
final ImageErrorListener errorListener1 = (dynamic exception, StackTrace stackTrace) {
errorListener1Called++;
};
final ImageErrorListener errorListener2 = (dynamic exception, StackTrace stackTrace) {
errorListener2Called++;
};
final ImageErrorListener errorListener3 = (dynamic exception, StackTrace stackTrace) {
errorListener3Called++;
};
final ImageListener listener = (ImageInfo info, bool synchronous) {
capturedImage = info;
};
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
imageProvider._streamCompleter.addListener(listener, onError: errorListener1);
imageProvider._streamCompleter.addListener(listener, onError: errorListener2);
imageProvider._streamCompleter.addListener(listener, onError: errorListener3);
// Remove listener. It should remove exactly the first one and the associated
// errorListener1.
imageProvider._streamCompleter.removeListener(listener);
ImageConfiguration configuration;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
configuration = createLocalImageConfiguration(context);
return Container();
},
),
);
imageProvider.resolve(configuration);
imageProvider.fail(testException, testStack);
expect(tester.binding.microtaskCount, 1);
await tester.idle(); // Let the failed completer's future hit the stream completer.
expect(tester.binding.microtaskCount, 0);
expect(errorListener1Called, 0);
expect(errorListener2Called, 1);
expect(errorListener3Called, 1);
expect(capturedImage, isNull); // The image stream listeners should never be called.
});
testWidgets('Image.memory control test', (WidgetTester tester) async { testWidgets('Image.memory control test', (WidgetTester tester) async {
await tester.pumpWidget(Image.memory(Uint8List.fromList(kTransparentImage), excludeFromSemantics: true,)); await tester.pumpWidget(Image.memory(Uint8List.fromList(kTransparentImage), excludeFromSemantics: true,));
}); });
...@@ -669,7 +614,7 @@ void main() { ...@@ -669,7 +614,7 @@ void main() {
// Check that a second resolve of the same image is synchronous. // Check that a second resolve of the same image is synchronous.
final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration); final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration);
bool isSync; bool isSync;
stream.addListener((ImageInfo image, bool sync) { isSync = sync; }); stream.addListener(ImageStreamListener((ImageInfo image, bool sync) { isSync = sync; }));
expect(isSync, isTrue); expect(isSync, isTrue);
}); });
...@@ -687,10 +632,10 @@ void main() { ...@@ -687,10 +632,10 @@ void main() {
); );
expect(imageStreamCompleter.listeners.length, 2); expect(imageStreamCompleter.listeners.length, 2);
imageStreamCompleter.listeners.keys.toList()[1](null, null); imageStreamCompleter.listeners.toList()[1].onImage(null, null);
expect(imageStreamCompleter.listeners.length, 1); expect(imageStreamCompleter.listeners.length, 1);
imageStreamCompleter.listeners.keys.toList()[0](null, null); imageStreamCompleter.listeners.toList()[0].onImage(null, null);
expect(imageStreamCompleter.listeners.length, 0); expect(imageStreamCompleter.listeners.length, 0);
}); });
...@@ -902,15 +847,15 @@ class TestImageProvider extends ImageProvider<TestImageProvider> { ...@@ -902,15 +847,15 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
} }
class TestImageStreamCompleter extends ImageStreamCompleter { class TestImageStreamCompleter extends ImageStreamCompleter {
final Map<ImageListener, ImageErrorListener> listeners = <ImageListener, ImageErrorListener>{}; final Set<ImageStreamListener> listeners = <ImageStreamListener>{};
@override @override
void addListener(ImageListener listener, { ImageErrorListener onError }) { void addListener(ImageStreamListener listener) {
listeners[listener] = onError; listeners.add(listener);
} }
@override @override
void removeListener(ImageListener listener) { void removeListener(ImageStreamListener listener) {
listeners.remove(listener); listeners.remove(listener);
} }
} }
......
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