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();
/// } /// }
/// ///
......
...@@ -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