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> {
// http client.
final NetworkImage image = NetworkImage('https://github.com/flutter/flutter');
final ImageStream stream = image.resolve(ImageConfiguration.empty);
stream.addListener((ImageInfo info, bool syncCall) {}, onError: (dynamic error, StackTrace stackTrace) {
print('ERROR caught by framework');
});
ImageStreamListener listener;
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();
}
......
......@@ -238,9 +238,10 @@ class DecorationImagePainter {
final ImageStream newImageStream = _details.image.resolve(configuration);
if (newImageStream.key != _imageStream?.key) {
_imageStream?.removeListener(_imageListener);
final ImageStreamListener listener = ImageStreamListener(_handleImage);
_imageStream?.removeListener(listener);
_imageStream = newImageStream;
_imageStream.addListener(_imageListener);
_imageStream.addListener(listener);
}
if (_image == null)
return;
......@@ -268,7 +269,7 @@ class DecorationImagePainter {
canvas.restore();
}
void _imageListener(ImageInfo value, bool synchronousCall) {
void _handleImage(ImageInfo value, bool synchronousCall) {
if (_image == value)
return;
_image = value;
......@@ -284,7 +285,7 @@ class DecorationImagePainter {
/// After this method has been called, the object is no longer usable.
@mustCallSuper
void dispose() {
_imageStream?.removeListener(_imageListener);
_imageStream?.removeListener(ImageStreamListener(_handleImage));
}
@override
......
......@@ -183,8 +183,10 @@ class ImageCache {
_checkCacheSize();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, listener);
result.addListener(listener);
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
}
return result;
}
......@@ -215,7 +217,7 @@ class _PendingImage {
_PendingImage(this.completer, this.listener);
final ImageStreamCompleter completer;
final ImageListener listener;
final ImageStreamListener listener;
void removeListener() {
completer.removeListener(listener);
......
......@@ -218,8 +218,9 @@ class ImageConfiguration {
/// // 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
/// // to switch our listeners to the new image stream.
/// oldImageStream?.removeListener(_updateImage);
/// _imageStream.addListener(_updateImage);
/// final ImageStreamListener listener = ImageStreamListener(_updateImage);
/// oldImageStream?.removeListener(listener);
/// _imageStream.addListener(listener);
/// }
/// }
///
......@@ -232,7 +233,7 @@ class ImageConfiguration {
///
/// @override
/// void dispose() {
/// _imageStream.removeListener(_updateImage);
/// _imageStream.removeListener(ImageStreamListener(_updateImage));
/// super.dispose();
/// }
///
......
......@@ -374,8 +374,9 @@ class _ImageProviderResolver {
assert(_imageStream != null);
if (_imageStream.key != oldImageStream?.key) {
oldImageStream?.removeListener(_handleImageChanged);
_imageStream.addListener(_handleImageChanged);
final ImageStreamListener listener = ImageStreamListener(_handleImageChanged);
oldImageStream?.removeListener(listener);
_imageStream.addListener(listener);
}
}
......@@ -385,7 +386,7 @@ class _ImageProviderResolver {
}
void stopListening() {
_imageStream?.removeListener(_handleImageChanged);
_imageStream?.removeListener(ImageStreamListener(_handleImageChanged));
}
}
......
......@@ -82,26 +82,29 @@ Future<void> precacheImage(
final ImageConfiguration config = createLocalImageConfiguration(context, size: size);
final Completer<void> completer = Completer<void>();
final ImageStream stream = provider.resolve(config);
void listener(ImageInfo image, bool sync) {
completer.complete();
stream.removeListener(listener);
}
void errorListener(dynamic exception, StackTrace stackTrace) {
completer.complete();
stream.removeListener(listener);
if (onError != null) {
onError(exception, stackTrace);
} else {
FlutterError.reportError(FlutterErrorDetails(
context: ErrorDescription('image failed to precache'),
library: 'image resource service',
exception: exception,
stack: stackTrace,
silent: true,
));
}
}
stream.addListener(listener, onError: errorListener);
ImageStreamListener listener;
listener = ImageStreamListener(
(ImageInfo image, bool sync) {
completer.complete();
stream.removeListener(listener);
},
onError: (dynamic exception, StackTrace stackTrace) {
completer.complete();
stream.removeListener(listener);
if (onError != null) {
onError(exception, stackTrace);
} else {
FlutterError.reportError(FlutterErrorDetails(
context: ErrorDescription('image failed to precache'),
library: 'image resource service',
exception: exception,
stack: stackTrace,
silent: true,
));
}
},
);
stream.addListener(listener);
return completer.future;
}
......@@ -656,28 +659,30 @@ class _ImageState extends State<Image> {
if (_imageStream?.key == newStream?.key)
return;
final ImageStreamListener listener = ImageStreamListener(_handleImageChanged);
if (_isListeningToStream)
_imageStream.removeListener(_handleImageChanged);
_imageStream.removeListener(listener);
if (!widget.gaplessPlayback)
setState(() { _imageInfo = null; });
_imageStream = newStream;
if (_isListeningToStream)
_imageStream.addListener(_handleImageChanged);
_imageStream.addListener(listener);
}
void _listenToStream() {
if (_isListeningToStream)
return;
_imageStream.addListener(_handleImageChanged);
_imageStream.addListener(ImageStreamListener(_handleImageChanged));
_isListeningToStream = true;
}
void _stopListeningToStream() {
if (!_isListeningToStream)
return;
_imageStream.removeListener(_handleImageChanged);
_imageStream.removeListener(ImageStreamListener(_handleImageChanged));
_isListeningToStream = false;
}
......
......@@ -199,10 +199,15 @@ void main() {
test('failed image can successfully be removed from the cache\'s pending images', () async {
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) {
final bool evicationResult = imageCache.evict(1);
expect(evicationResult, isTrue);
});
const FailingTestImageProvider(1, 1, image: testImage)
.resolve(ImageConfiguration.empty)
.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() {
final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
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;
expect(imageCache.currentSize, 1);
......@@ -46,7 +46,7 @@ void main() {
otherCache.putIfAbsent(imageProvider, () => imageProvider.load(imageProvider));
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
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;
expect(otherCache.currentSize, 1);
......@@ -63,11 +63,11 @@ void main() {
caughtError.complete(false);
};
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener((ImageInfo info, bool syncCall) {
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
});
}));
expect(await caughtError.future, true);
});
});
......@@ -79,11 +79,11 @@ void main() {
caughtError.complete(false);
};
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener((ImageInfo info, bool syncCall) {
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
});
}));
expect(await caughtError.future, true);
});
......@@ -101,10 +101,10 @@ void main() {
throw Error();
};
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) {
caughtError.complete(true);
});
}));
expect(await caughtError.future, true);
});
expect(uncaught, false);
......@@ -124,10 +124,10 @@ void main() {
throw Error();
};
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) {
caughtError.complete(true);
});
}));
expect(await caughtError.future, true);
});
expect(uncaught, false);
......@@ -158,12 +158,12 @@ void main() {
Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final ImageStreamCompleter completer = networkImage.load(networkImage);
completer.addListener(
completer.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { },
onError: (dynamic error, StackTrace stackTrace) {
capturedErrors.add(error);
},
);
));
await Future<void>.value();
}
......@@ -187,10 +187,10 @@ void main() {
throw Error();
};
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) {
caughtError.complete(true);
});
}));
expect(await caughtError.future, true);
}, zoneSpecification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
......
......@@ -103,7 +103,7 @@ void main() {
expect(mockCodec.numFramesAsked, 0);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
await tester.idle();
expect(mockCodec.numFramesAsked, 1);
});
......@@ -118,7 +118,7 @@ void main() {
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
await tester.idle();
expect(mockCodec.numFramesAsked, 0);
......@@ -138,7 +138,7 @@ void main() {
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec);
// MultiFrameImageStreamCompleter only sets an error handler for the next
// frame future after the codec future has completed.
......@@ -163,9 +163,9 @@ void main() {
);
final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) {
imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
});
}));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -189,9 +189,9 @@ void main() {
);
final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) {
imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
});
}));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -237,9 +237,9 @@ void main() {
);
final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) {
imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
});
}));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -280,9 +280,9 @@ void main() {
);
final List<ImageInfo> emittedImages = <ImageInfo>[];
imageStream.addListener((ImageInfo image, bool synchronousCall) {
imageStream.addListener(ImageStreamListener((ImageInfo image, bool synchronousCall) {
emittedImages.add(image);
});
}));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -320,7 +320,7 @@ void main() {
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -332,7 +332,7 @@ void main() {
await tester.idle(); // let nextFrameFuture complete
await tester.pump(); // first animation frame shows on first app frame.
mockCodec.completeNextFrame(frame2);
imageStream.removeListener(listener);
imageStream.removeListener(ImageStreamListener(listener));
await tester.idle(); // let nextFrameFuture complete
await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
......@@ -340,7 +340,7 @@ void main() {
// listeners to the stream
expect(mockCodec.numFramesAsked, 2);
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
await tester.idle(); // let nextFrameFuture complete
expect(mockCodec.numFramesAsked, 3);
});
......@@ -364,8 +364,8 @@ void main() {
final ImageListener listener2 = (ImageInfo image, bool synchronousCall) {
emittedImages2.add(image);
};
imageStream.addListener(listener1);
imageStream.addListener(listener2);
imageStream.addListener(ImageStreamListener(listener1));
imageStream.addListener(ImageStreamListener(listener2));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -382,7 +382,7 @@ void main() {
mockCodec.completeNextFrame(frame2);
await tester.idle(); // let nextFrameFuture complete
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.
expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
......@@ -404,7 +404,7 @@ void main() {
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -420,7 +420,7 @@ void main() {
await tester.idle(); // let nextFrameFuture complete
await tester.pump();
imageStream.removeListener(listener);
imageStream.removeListener(ImageStreamListener(listener));
// The test framework will fail this if there are pending timers at this
// point.
});
......@@ -437,7 +437,7 @@ void main() {
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec);
await tester.idle();
......@@ -476,10 +476,10 @@ void main() {
capturedException = exception;
};
streamUnderTest.addListener(
streamUnderTest.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { },
onError: errorListener,
);
));
codecCompleter.complete(mockCodec);
// MultiFrameImageStreamCompleter only sets an error handler for the next
......@@ -508,14 +508,14 @@ void main() {
);
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(listener);
imageStream.addListener(ImageStreamListener(listener));
codecCompleter.complete(mockCodec);
await tester.idle(); // let nextFrameFuture complete
imageStream.removeListener(listener);
imageStream.addListener(listener);
imageStream.removeListener(ImageStreamListener(listener));
imageStream.addListener(ImageStreamListener(listener));
final FrameInfo frame1 = FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
......@@ -541,7 +541,7 @@ void main() {
// );
//
// final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
// imageStream.addListener(listener);
// imageStream.addListener(ImageLoadingListener(listener));
//
// codecCompleter.complete(mockCodec);
// await tester.idle();
......@@ -560,7 +560,7 @@ void main() {
// tester.flushTimers();
//
// imageStream.removeListener(listener);
// imageStream.addListener(listener);
// imageStream.addListener(ImageLoadingListener(listener));
//
// mockCodec.completeNextFrame(frame3);
// await tester.idle(); // let nextFrameFuture complete
......
......@@ -58,10 +58,11 @@ class FailingTestImageProvider extends TestImageProvider {
Future<ImageInfo> extractOneFrame(ImageStream stream) {
final Completer<ImageInfo> completer = Completer<ImageInfo>();
void listener(ImageInfo image, bool synchronousCall) {
ImageStreamListener listener;
listener = ImageStreamListener((ImageInfo image, bool synchronousCall) {
completer.complete(image);
stream.removeListener(listener);
}
});
stream.addListener(listener);
return completer.future;
}
......
......@@ -331,7 +331,7 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
final TestImageProvider imageProvider = TestImageProvider();
imageProvider._streamCompleter.addListener(listener, onError: errorListener);
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
ImageConfiguration configuration;
await tester.pumpWidget(
Builder(
......@@ -399,7 +399,7 @@ void main() {
expect(reportedException, testException);
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.
// The image stream error handler should have the original exception.
......@@ -422,9 +422,9 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
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.
imageProvider._streamCompleter.addListener(listener);
imageProvider._streamCompleter.addListener(ImageStreamListener(listener));
ImageConfiguration configuration;
await tester.pumpWidget(
Builder(
......@@ -466,9 +466,9 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
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.
imageProvider._streamCompleter.addListener(null, onError: errorListener);
imageProvider._streamCompleter.addListener(ImageStreamListener(listener, onError: errorListener));
ImageConfiguration configuration;
await tester.pumpWidget(
Builder(
......@@ -494,29 +494,27 @@ void main() {
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;
dynamic reportedException;
StackTrace reportedStackTrace;
ImageInfo capturedImage;
final ImageErrorListener errorListener = (dynamic exception, StackTrace stackTrace) {
errorListenerCalled = true;
reportedException = exception;
reportedStackTrace = stackTrace;
};
final ImageListener listener = (ImageInfo info, bool synchronous) {
capturedImage = info;
};
FlutterError.onError = (FlutterErrorDetails flutterError) {
reportedException = flutterError.exception;
reportedStackTrace = flutterError.stack;
};
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
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.
// Don't explicitly remove the error listener.
imageProvider._streamCompleter.removeListener(listener);
imageProvider._streamCompleter.removeListener(ImageStreamListener(listener));
ImageConfiguration configuration;
await tester.pumpWidget(
Builder(
......@@ -534,8 +532,7 @@ void main() {
await tester.idle(); // Let the failed completer's future hit the stream completer.
expect(tester.binding.microtaskCount, 0);
expect(errorListenerCalled, false);
// Since the error listener is removed, bubble up to FlutterError.
expect(errorListenerCalled, true);
expect(reportedException, testException);
expect(reportedStackTrace, testStack);
expect(capturedImage, isNull); // The image stream listeners should never be called.
......@@ -554,12 +551,12 @@ void main() {
final Exception testException = Exception('cannot resolve host');
final StackTrace testStack = StackTrace.current;
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.
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.
// Don't explicitly remove the error listener.
imageProvider._streamCompleter.removeListener(listener);
imageProvider._streamCompleter.removeListener(ImageStreamListener(listener, onError: errorListener));
ImageConfiguration configuration;
await tester.pumpWidget(
Builder(
......@@ -581,58 +578,6 @@ void main() {
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 {
await tester.pumpWidget(Image.memory(Uint8List.fromList(kTransparentImage), excludeFromSemantics: true,));
});
......@@ -669,7 +614,7 @@ void main() {
// Check that a second resolve of the same image is synchronous.
final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration);
bool isSync;
stream.addListener((ImageInfo image, bool sync) { isSync = sync; });
stream.addListener(ImageStreamListener((ImageInfo image, bool sync) { isSync = sync; }));
expect(isSync, isTrue);
});
......@@ -687,10 +632,10 @@ void main() {
);
expect(imageStreamCompleter.listeners.length, 2);
imageStreamCompleter.listeners.keys.toList()[1](null, null);
imageStreamCompleter.listeners.toList()[1].onImage(null, null);
expect(imageStreamCompleter.listeners.length, 1);
imageStreamCompleter.listeners.keys.toList()[0](null, null);
imageStreamCompleter.listeners.toList()[0].onImage(null, null);
expect(imageStreamCompleter.listeners.length, 0);
});
......@@ -902,15 +847,15 @@ class TestImageProvider extends ImageProvider<TestImageProvider> {
}
class TestImageStreamCompleter extends ImageStreamCompleter {
final Map<ImageListener, ImageErrorListener> listeners = <ImageListener, ImageErrorListener>{};
final Set<ImageStreamListener> listeners = <ImageStreamListener>{};
@override
void addListener(ImageListener listener, { ImageErrorListener onError }) {
listeners[listener] = onError;
void addListener(ImageStreamListener listener) {
listeners.add(listener);
}
@override
void removeListener(ImageListener listener) {
void removeListener(ImageStreamListener 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