From 6a855b271e74793045c06f0307ac46b22758b8ee Mon Sep 17 00:00:00 2001 From: wen Date: Wed, 29 May 2024 01:26:48 +0800 Subject: [PATCH] Replace Stream with Future --- .../lib/cached_network_image.dart | 1 - .../lib/src/image_provider/_image_loader.dart | 1 - .../cached_network_image_provider.dart | 9 +- .../multi_image_stream_completer.dart | 238 ----- .../test/image_stream_completer_test.dart | 914 ------------------ 5 files changed, 4 insertions(+), 1159 deletions(-) delete mode 100644 cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart delete mode 100644 cached_network_image/test/image_stream_completer_test.dart diff --git a/cached_network_image/lib/cached_network_image.dart b/cached_network_image/lib/cached_network_image.dart index 99375aaa..317d8c87 100644 --- a/cached_network_image/lib/cached_network_image.dart +++ b/cached_network_image/lib/cached_network_image.dart @@ -7,4 +7,3 @@ export 'package:flutter_cache_manager/flutter_cache_manager.dart' export 'src/cached_image_widget.dart'; export 'src/image_provider/cached_network_image_provider.dart'; -export 'src/image_provider/multi_image_stream_completer.dart'; diff --git a/cached_network_image/lib/src/image_provider/_image_loader.dart b/cached_network_image/lib/src/image_provider/_image_loader.dart index 01f11ef9..e1dddbd9 100644 --- a/cached_network_image/lib/src/image_provider/_image_loader.dart +++ b/cached_network_image/lib/src/image_provider/_image_loader.dart @@ -108,7 +108,6 @@ class ImageLoader implements platform.ImageLoader { headers: headers, key: cacheKey, ); - await for (final result in stream) { if (result is DownloadProgress) { chunkEvents.add( diff --git a/cached_network_image/lib/src/image_provider/cached_network_image_provider.dart b/cached_network_image/lib/src/image_provider/cached_network_image_provider.dart index 5dc3b908..e93c9ba6 100644 --- a/cached_network_image/lib/src/image_provider/cached_network_image_provider.dart +++ b/cached_network_image/lib/src/image_provider/cached_network_image_provider.dart @@ -1,7 +1,6 @@ import 'dart:async' show Future, StreamController; import 'dart:ui' as ui show Codec; -import 'package:cached_network_image/src/image_provider/multi_image_stream_completer.dart'; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart' show ErrorListener, ImageRenderMethodForWeb; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart' @@ -77,8 +76,8 @@ class CachedNetworkImageProvider DecoderBufferCallback decode, ) { final chunkEvents = StreamController(); - final imageStreamCompleter = MultiImageStreamCompleter( - codec: _loadBufferAsync(key, chunkEvents, decode), + final imageStreamCompleter = MultiFrameImageStreamCompleter( + codec: _loadBufferAsync(key, chunkEvents, decode).first, chunkEvents: chunkEvents.stream, scale: key.scale, informationCollector: () => [ @@ -128,8 +127,8 @@ class CachedNetworkImageProvider ImageDecoderCallback decode, ) { final chunkEvents = StreamController(); - final imageStreamCompleter = MultiImageStreamCompleter( - codec: _loadImageAsync(key, chunkEvents, decode), + final imageStreamCompleter = MultiFrameImageStreamCompleter( + codec: _loadImageAsync(key, chunkEvents, decode).first, chunkEvents: chunkEvents.stream, scale: key.scale, informationCollector: () => [ diff --git a/cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart b/cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart deleted file mode 100644 index bc81795d..00000000 --- a/cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui show Codec, FrameInfo; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -import 'package:flutter/scheduler.dart'; - -/// Slows down animations by this factor to help in development. -double get timeDilation => _timeDilation; -double _timeDilation = 1; - -/// An ImageStreamCompleter with support for loading multiple images. -class MultiImageStreamCompleter extends ImageStreamCompleter { - /// The constructor to create an MultiImageStreamCompleter. The [codec] - /// should be a stream with the images that should be shown. The - /// [chunkEvents] should indicate the [ImageChunkEvent]s of the first image - /// to show. - MultiImageStreamCompleter({ - required Stream codec, - required double scale, - Stream? chunkEvents, - InformationCollector? informationCollector, - }) : _informationCollector = informationCollector, - _scale = scale { - codec.listen( - (event) { - if (_timer != null) { - _nextImageCodec = event; - } else { - _handleCodecReady(event); - } - }, - onError: (Object error, StackTrace stack) { - reportError( - context: ErrorDescription('resolving an image codec'), - exception: error, - stack: stack, - informationCollector: informationCollector, - silent: true, - ); - }, - ); - if (chunkEvents != null) { - _chunkSubscription = chunkEvents.listen( - reportImageChunkEvent, - onError: (Object error, StackTrace stack) { - reportError( - context: ErrorDescription('loading an image'), - exception: error, - stack: stack, - informationCollector: informationCollector, - silent: true, - ); - }, - ); - } - } - - ui.Codec? _codec; - ui.Codec? _nextImageCodec; - final double _scale; - final InformationCollector? _informationCollector; - ui.FrameInfo? _nextFrame; - - // When the current was first shown. - Duration? _shownTimestamp; - - // The requested duration for the current frame; - Duration? _frameDuration; - - // How many frames have been emitted so far. - int _framesEmitted = 0; - Timer? _timer; - StreamSubscription? _chunkSubscription; - - // Used to guard against registering multiple _handleAppFrame callbacks for the same frame. - bool _frameCallbackScheduled = false; - - /// We must avoid disposing a completer if it never had a listener, even - /// if all [keepAlive] handles get disposed. - bool __hadAtLeastOneListener = false; - - bool __disposed = false; - - void _switchToNewCodec() { - _framesEmitted = 0; - _timer = null; - _handleCodecReady(_nextImageCodec!); - _nextImageCodec = null; - } - - void _handleCodecReady(ui.Codec codec) { - _codec = codec; - - if (hasListeners) { - _decodeNextFrameAndSchedule(); - } - } - - void _handleAppFrame(Duration timestamp) { - _frameCallbackScheduled = false; - if (!hasListeners) return; - if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) { - _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale)); - _shownTimestamp = timestamp; - _frameDuration = _nextFrame!.duration; - _nextFrame = null; - if (_framesEmitted % _codec!.frameCount == 0 && _nextImageCodec != null) { - _switchToNewCodec(); - } else { - final completedCycles = _framesEmitted ~/ _codec!.frameCount; - if (_codec!.repetitionCount == -1 || - completedCycles <= _codec!.repetitionCount) { - _decodeNextFrameAndSchedule(); - } - } - return; - } - final delay = _frameDuration! - (timestamp - _shownTimestamp!); - _timer = Timer(delay * timeDilation, _scheduleAppFrame); - } - - bool _isFirstFrame() { - return _frameDuration == null; - } - - bool _hasFrameDurationPassed(Duration timestamp) { - return timestamp - _shownTimestamp! >= _frameDuration!; - } - - Future _decodeNextFrameAndSchedule() async { - try { - _nextFrame = await _codec!.getNextFrame(); - } on Object catch (exception, stack) { - reportError( - context: ErrorDescription('resolving an image frame'), - exception: exception, - stack: stack, - informationCollector: _informationCollector, - silent: true, - ); - return; - } - if (_codec!.frameCount == 1) { - // ImageStreamCompleter listeners removed while waiting for next frame to - // be decoded. - // There's no reason to emit the frame without active listeners. - if (!hasListeners) { - return; - } - - // This is not an animated image, just return it and don't schedule more - // frames. - _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale)); - return; - } - _scheduleAppFrame(); - } - - void _scheduleAppFrame() { - if (_frameCallbackScheduled) { - return; - } - _frameCallbackScheduled = true; - SchedulerBinding.instance.scheduleFrameCallback(_handleAppFrame); - } - - void _emitFrame(ImageInfo imageInfo) { - setImage(imageInfo); - _framesEmitted += 1; - } - - @override - void addListener(ImageStreamListener listener) { - __hadAtLeastOneListener = true; - if (!hasListeners && _codec != null) _decodeNextFrameAndSchedule(); - super.addListener(listener); - } - - @override - void removeListener(ImageStreamListener listener) { - super.removeListener(listener); - if (!hasListeners) { - _timer?.cancel(); - _timer = null; - __maybeDispose(); - } - } - - int __keepAliveHandles = 0; - - @override - ImageStreamCompleterHandle keepAlive() { - final delegateHandle = super.keepAlive(); - return _MultiImageStreamCompleterHandle(this, delegateHandle); - } - - void __maybeDispose() { - if (!__hadAtLeastOneListener || - __disposed || - hasListeners || - __keepAliveHandles != 0) { - return; - } - - __disposed = true; - - _chunkSubscription?.onData(null); - _chunkSubscription?.cancel(); - _chunkSubscription = null; - } -} - -class _MultiImageStreamCompleterHandle implements ImageStreamCompleterHandle { - _MultiImageStreamCompleterHandle(this._completer, this._delegateHandle) { - _completer!.__keepAliveHandles += 1; - } - - MultiImageStreamCompleter? _completer; - final ImageStreamCompleterHandle _delegateHandle; - - /// Call this method to signal the [ImageStreamCompleter] that it can now be - /// disposed when its last listener drops. - /// - /// This method must only be called once per object. - @override - void dispose() { - assert(_completer != null); - assert(_completer!.__keepAliveHandles > 0); - assert(!_completer!.__disposed); - - _delegateHandle.dispose(); - - _completer!.__keepAliveHandles -= 1; - _completer!.__maybeDispose(); - _completer = null; - } -} diff --git a/cached_network_image/test/image_stream_completer_test.dart b/cached_network_image/test/image_stream_completer_test.dart deleted file mode 100644 index 33e1b361..00000000 --- a/cached_network_image/test/image_stream_completer_test.dart +++ /dev/null @@ -1,914 +0,0 @@ -// Copyright 2020 Rene Floor. All rights reserved. -// Use of this source code is governed by a MIT-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:ui'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/painting.dart'; -import 'package:flutter/scheduler.dart' show SchedulerBinding; -import 'package:flutter_test/flutter_test.dart'; - -class FakeFrameInfo implements FrameInfo { - const FakeFrameInfo(this._duration, this._image); - - final Duration _duration; - final Image _image; - - @override - Duration get duration => _duration; - - @override - Image get image => _image; - - int get imageHandleCount => image.debugGetOpenHandleStackTraces()!.length; - - FakeFrameInfo clone() { - return FakeFrameInfo( - _duration, - _image.clone(), - ); - } -} - -class MockCodec implements Codec { - @override - int frameCount = 1; - - @override - int repetitionCount = 1; - - int numFramesAsked = 0; - - Completer _nextFrameCompleter = Completer(); - - @override - Future getNextFrame() { - numFramesAsked += 1; - return _nextFrameCompleter.future; - } - - void completeNextFrame(FrameInfo frameInfo) { - _nextFrameCompleter.complete(frameInfo); - _nextFrameCompleter = Completer(); - } - - void failNextFrame(String err) { - _nextFrameCompleter.completeError(err); - } - - @override - void dispose() {} -} - -class FakeEventReportingImageStreamCompleter extends ImageStreamCompleter { - FakeEventReportingImageStreamCompleter({ - Stream? chunkEvents, - }) { - if (chunkEvents != null) { - chunkEvents.listen( - (ImageChunkEvent event) { - reportImageChunkEvent(event); - }, - ); - } - } -} - -void main() { - late Image image20x10; - late Image image200x100; - late Image image300x100; - setUp(() async { - image20x10 = await createTestImage(width: 20, height: 10); - image200x100 = await createTestImage(width: 200, height: 100); - image300x100 = await createTestImage(width: 300, height: 100); - }); - - testWidgets('Codec future fails', (WidgetTester tester) async { - final codecStream = StreamController(); - MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - codecStream.addError('failure message'); - await tester.idle(); - expect(tester.takeException(), 'failure message'); - }); - - test('Completer unsubscribes to chunk events when disposed', () async { - final codecStream = StreamController(); - final chunkStream = StreamController(); - - final MultiImageStreamCompleter completer = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - chunkEvents: chunkStream.stream, - ); - - expect(chunkStream.hasListener, true); - - chunkStream.add( - const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3), - ); - - final ImageStreamListener listener = - ImageStreamListener((ImageInfo info, bool syncCall) {}); - // Cause the completer to dispose. - completer.addListener(listener); - completer.removeListener(listener); - - expect(chunkStream.hasListener, false); - - // The above expectation should cover this, but the point of this test is to - // make sure the completer does not assert that it's disposed and still - // receiving chunk events. Streams from the network can keep sending data - // even after evicting an image from the cache, for example. - chunkStream.add( - const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3), - ); - }); - - testWidgets('Decoding starts when a listener is added after codec is ready', - (WidgetTester tester) async { - final codecStream = StreamController(); - final mockCodec = MockCodec(); - mockCodec.frameCount = 1; - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - codecStream.add(mockCodec); - await tester.idle(); - expect(mockCodec.numFramesAsked, 0); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - await tester.idle(); - expect(mockCodec.numFramesAsked, 1); - }); - - testWidgets('Decoding starts when a codec is ready after a listener is added', - (WidgetTester tester) async { - final codecStream = StreamController(); - final mockCodec = MockCodec(); - mockCodec.frameCount = 1; - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - await tester.idle(); - expect(mockCodec.numFramesAsked, 0); - - codecStream.add(mockCodec); - await tester.idle(); - expect(mockCodec.numFramesAsked, 1); - }); - - testWidgets('Adding a second codec triggers start decoding', - (WidgetTester tester) async { - final codecStream = StreamController(); - final firstCodec = MockCodec(); - final secondCodec = MockCodec(); - firstCodec.frameCount = 1; - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - await tester.idle(); - expect(firstCodec.numFramesAsked, 0); - - codecStream.add(firstCodec); - await tester.idle(); - expect(firstCodec.numFramesAsked, 1); - - expect(secondCodec.numFramesAsked, 0); - - codecStream.add(secondCodec); - await tester.idle(); - expect(secondCodec.numFramesAsked, 1); - }); - - testWidgets('Decoding does not crash when disposed', - (WidgetTester tester) async { - final codecStream = StreamController(); - final mockCodec = MockCodec(); - mockCodec.frameCount = 1; - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - codecStream.add(mockCodec); - await tester.idle(); - expect(mockCodec.numFramesAsked, 0); - - listener(ImageInfo image, bool synchronousCall) {} - final streamListener = ImageStreamListener(listener); - imageStream.addListener(streamListener); - await tester.idle(); - expect(mockCodec.numFramesAsked, 1); - - final FrameInfo frame = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - mockCodec.completeNextFrame(frame); - imageStream.removeListener(streamListener); - await tester.idle(); - }); - - testWidgets('Chunk events of base ImageStreamCompleter are delivered', - (WidgetTester tester) async { - final chunkEvents = []; - final streamController = StreamController(); - final ImageStreamCompleter imageStream = - FakeEventReportingImageStreamCompleter( - chunkEvents: streamController.stream, - ); - - imageStream.addListener( - ImageStreamListener( - (ImageInfo image, bool synchronousCall) {}, - onChunk: (ImageChunkEvent event) { - chunkEvents.add(event); - }, - ), - ); - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3), - ); - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3), - ); - await tester.idle(); - - expect(chunkEvents.length, 2); - expect(chunkEvents[0].cumulativeBytesLoaded, 1); - expect(chunkEvents[0].expectedTotalBytes, 3); - expect(chunkEvents[1].cumulativeBytesLoaded, 2); - expect(chunkEvents[1].expectedTotalBytes, 3); - }); - - testWidgets( - 'Chunk events of base ImageStreamCompleter are not buffered before listener registration', - (WidgetTester tester) async { - final chunkEvents = []; - final streamController = StreamController(); - final ImageStreamCompleter imageStream = - FakeEventReportingImageStreamCompleter( - chunkEvents: streamController.stream, - ); - - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3), - ); - await tester.idle(); - imageStream.addListener( - ImageStreamListener( - (ImageInfo image, bool synchronousCall) {}, - onChunk: (ImageChunkEvent event) { - chunkEvents.add(event); - }, - ), - ); - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3), - ); - await tester.idle(); - - expect(chunkEvents.length, 1); - expect(chunkEvents[0].cumulativeBytesLoaded, 2); - expect(chunkEvents[0].expectedTotalBytes, 3); - }); - - testWidgets('Chunk events of MultiImageStreamCompleter are delivered', - (WidgetTester tester) async { - final chunkEvents = []; - final codecStream = StreamController(); - final streamController = StreamController(); - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - chunkEvents: streamController.stream, - scale: 1.0, - ); - - imageStream.addListener( - ImageStreamListener( - (ImageInfo image, bool synchronousCall) {}, - onChunk: (ImageChunkEvent event) { - chunkEvents.add(event); - }, - ), - ); - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3), - ); - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3), - ); - await tester.idle(); - - expect(chunkEvents.length, 2); - expect(chunkEvents[0].cumulativeBytesLoaded, 1); - expect(chunkEvents[0].expectedTotalBytes, 3); - expect(chunkEvents[1].cumulativeBytesLoaded, 2); - expect(chunkEvents[1].expectedTotalBytes, 3); - }); - - testWidgets( - 'Chunk events of MultiImageStreamCompleter are not buffered before listener registration', - (WidgetTester tester) async { - final chunkEvents = []; - final codecStream = StreamController(); - final streamController = StreamController(); - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - chunkEvents: streamController.stream, - scale: 1.0, - ); - - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 1, expectedTotalBytes: 3), - ); - await tester.idle(); - imageStream.addListener( - ImageStreamListener( - (ImageInfo image, bool synchronousCall) {}, - onChunk: (ImageChunkEvent event) { - chunkEvents.add(event); - }, - ), - ); - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3), - ); - await tester.idle(); - - expect(chunkEvents.length, 1); - expect(chunkEvents[0].cumulativeBytesLoaded, 2); - expect(chunkEvents[0].expectedTotalBytes, 3); - }); - - testWidgets('Chunk errors are reported', (WidgetTester tester) async { - final chunkEvents = []; - final codecStream = StreamController(); - final streamController = StreamController(); - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - chunkEvents: streamController.stream, - scale: 1.0, - ); - - imageStream.addListener( - ImageStreamListener( - (ImageInfo image, bool synchronousCall) {}, - onChunk: (ImageChunkEvent event) { - chunkEvents.add(event); - }, - ), - ); - streamController.addError(Error()); - streamController.add( - const ImageChunkEvent(cumulativeBytesLoaded: 2, expectedTotalBytes: 3), - ); - await tester.idle(); - - expect(tester.takeException(), isNotNull); - expect(chunkEvents.length, 1); - expect(chunkEvents[0].cumulativeBytesLoaded, 2); - expect(chunkEvents[0].expectedTotalBytes, 3); - }); - - testWidgets('getNextFrame future fails', (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - codecStream.add(mockCodec); - // MultiImageStreamCompleter only sets an error handler for the next - // frame future after the codec future has completed. - // Idling here lets the MultiImageStreamCompleter advance and set the - // error handler for the nextFrame future. - await tester.idle(); - - mockCodec.failNextFrame('frame completion error'); - await tester.idle(); - - expect(tester.takeException(), 'frame completion error'); - }); - - testWidgets('ImageStream emits frame (static image)', - (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - final emittedImages = []; - imageStream.addListener( - ImageStreamListener((ImageInfo image, bool synchronousCall) { - emittedImages.add(image); - }), - ); - - codecStream.add(mockCodec); - await tester.idle(); - - final FrameInfo frame = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - mockCodec.completeNextFrame(frame); - await tester.idle(); - - expect( - emittedImages - .every((ImageInfo info) => info.image.isCloneOf(frame.image)), - true, - ); - }); - - testWidgets('ImageStream emits frames (animated images)', - (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 2; - mockCodec.repetitionCount = -1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - final emittedImages = []; - imageStream.addListener( - ImageStreamListener((ImageInfo image, bool synchronousCall) { - emittedImages.add(image); - }), - ); - - codecStream.add(mockCodec); - await tester.idle(); - - final FrameInfo frame1 = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - mockCodec.completeNextFrame(frame1); - await tester.idle(); - // We are waiting for the next animation tick, so at this point no frames - // should have been emitted. - expect(emittedImages.length, 0); - - await tester.pump(); - expect(emittedImages.single.image.isCloneOf(frame1.image), true); - - final FrameInfo frame2 = - FakeFrameInfo(const Duration(milliseconds: 400), image200x100); - mockCodec.completeNextFrame(frame2); - - await tester.pump(const Duration(milliseconds: 100)); - // The duration for the current frame was 200ms, so we don't emit the next - // frame yet even though it is ready. - expect(emittedImages.length, 1); - - await tester.pump(const Duration(milliseconds: 100)); - expect(emittedImages[0].image.isCloneOf(frame1.image), true); - expect(emittedImages[1].image.isCloneOf(frame2.image), true); - - // Let the pending timer for the next frame to complete so we can cleanly - // quit the test without pending timers. - await tester.pump(const Duration(milliseconds: 400)); - }); - - testWidgets('animation wraps back', (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 2; - mockCodec.repetitionCount = -1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - final emittedImages = []; - imageStream.addListener( - ImageStreamListener((ImageInfo image, bool synchronousCall) { - emittedImages.add(image); - }), - ); - - codecStream.add(mockCodec); - await tester.idle(); - - final frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - final frame2 = - FakeFrameInfo(const Duration(milliseconds: 400), image200x100); - - mockCodec.completeNextFrame(frame1.clone()); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // first animation frame shows on first app frame. - mockCodec.completeNextFrame(frame2.clone()); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame. - mockCodec.completeNextFrame(frame1.clone()); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(const Duration(milliseconds: 400)); // emit 3rd frame - - expect(emittedImages[0].image.isCloneOf(frame1.image), true); - expect(emittedImages[1].image.isCloneOf(frame2.image), true); - expect(emittedImages[2].image.isCloneOf(frame1.image), true); - - // Let the pending timer for the next frame to complete so we can cleanly - // quit the test without pending timers. - await tester.pump(const Duration(milliseconds: 200)); - }); - - testWidgets('animation doesnt repeat more than specified', - (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 2; - mockCodec.repetitionCount = 0; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - final emittedImages = []; - imageStream.addListener( - ImageStreamListener((ImageInfo image, bool synchronousCall) { - emittedImages.add(image); - }), - ); - - codecStream.add(mockCodec); - await tester.idle(); - - final FrameInfo frame1 = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - final FrameInfo frame2 = - FakeFrameInfo(const Duration(milliseconds: 400), image200x100); - - mockCodec.completeNextFrame(frame1); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // first animation frame shows on first app frame. - mockCodec.completeNextFrame(frame2); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame. - mockCodec.completeNextFrame(frame1); - // allow another frame to complete (but we shouldn't be asking for it as - // this animation should not repeat. - await tester.idle(); - await tester.pump(const Duration(milliseconds: 400)); - - expect(emittedImages[0].image.isCloneOf(frame1.image), true); - expect(emittedImages[1].image.isCloneOf(frame2.image), true); - }); - - testWidgets('frames are only decoded when there are listeners', - (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 2; - mockCodec.repetitionCount = -1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - final handle = imageStream.keepAlive(); - - codecStream.add(mockCodec); - await tester.idle(); - - final FrameInfo frame1 = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - final FrameInfo frame2 = - FakeFrameInfo(const Duration(milliseconds: 400), image200x100); - - mockCodec.completeNextFrame(frame1); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // first animation frame shows on first app frame. - mockCodec.completeNextFrame(frame2); - imageStream.removeListener(ImageStreamListener(listener)); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame. - - // Decoding of the 3rd frame should not start as there are no registered - // listeners to the stream - expect(mockCodec.numFramesAsked, 2); - - imageStream.addListener(ImageStreamListener(listener)); - await tester.idle(); // let nextFrameFuture complete - expect(mockCodec.numFramesAsked, 3); - - handle.dispose(); - }); - - testWidgets('multiple stream listeners', (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 2; - mockCodec.repetitionCount = -1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - final emittedImages1 = []; - listener1(ImageInfo image, bool synchronousCall) { - emittedImages1.add(image); - } - - final emittedImages2 = []; - listener2(ImageInfo image, bool synchronousCall) { - emittedImages2.add(image); - } - - imageStream.addListener(ImageStreamListener(listener1)); - imageStream.addListener(ImageStreamListener(listener2)); - - codecStream.add(mockCodec); - await tester.idle(); - - final FrameInfo frame1 = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - final FrameInfo frame2 = - FakeFrameInfo(const Duration(milliseconds: 400), image200x100); - - mockCodec.completeNextFrame(frame1); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // first animation frame shows on first app frame. - - expect(emittedImages1.single.image.isCloneOf(frame1.image), true); - expect(emittedImages2.single.image.isCloneOf(frame1.image), true); - - mockCodec.completeNextFrame(frame2); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // next app frame will schedule a timer. - imageStream.removeListener(ImageStreamListener(listener1)); - - await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame. - expect(emittedImages1.single.image.isCloneOf(frame1.image), true); - expect(emittedImages2[0].image.isCloneOf(frame1.image), true); - expect(emittedImages2[1].image.isCloneOf(frame2.image), true); - }); - - testWidgets('timer is canceled when listeners are removed', - (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 2; - mockCodec.repetitionCount = -1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - - codecStream.add(mockCodec); - await tester.idle(); - - final FrameInfo frame1 = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - final FrameInfo frame2 = - FakeFrameInfo(const Duration(milliseconds: 400), image200x100); - - mockCodec.completeNextFrame(frame1); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // first animation frame shows on first app frame. - - mockCodec.completeNextFrame(frame2); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); - - imageStream.removeListener(ImageStreamListener(listener)); - // The test framework will fail this if there are pending timers at this - // point. - }); - - testWidgets('error handlers can intercept errors', - (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 1; - final codecStream = StreamController(); - - final ImageStreamCompleter streamUnderTest = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - Object? capturedException; - errorListener(Object exception, StackTrace? stackTrace) { - capturedException = exception; - } - - streamUnderTest.addListener( - ImageStreamListener( - (ImageInfo image, bool synchronousCall) {}, - onError: errorListener, - ), - ); - - codecStream.add(mockCodec); - // MultiImageStreamCompleter only sets an error handler for the next - // frame future after the codec future has completed. - // Idling here lets the MultiImageStreamCompleter advance and set the - // error handler for the nextFrame future. - await tester.idle(); - - mockCodec.failNextFrame('frame completion error'); - await tester.idle(); - - // No exception is passed up. - expect(tester.takeException(), isNull); - expect(capturedException, 'frame completion error'); - }); - - testWidgets('remove and add listener ', (WidgetTester tester) async { - final mockCodec = MockCodec(); - mockCodec.frameCount = 3; - mockCodec.repetitionCount = 0; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - - codecStream.add(mockCodec); - - await tester.idle(); // let nextFrameFuture complete - - imageStream.addListener(ImageStreamListener(listener)); - imageStream.removeListener(ImageStreamListener(listener)); - - final FrameInfo frame1 = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - - mockCodec.completeNextFrame(frame1); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // first animation frame shows on first app frame. - - await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame. - }); - - testWidgets( - 'Keep alive handles do not drive frames or prevent last listener callbacks', - (WidgetTester tester) async { - final image10x10 = - (await tester.runAsync(() => createTestImage(width: 10, height: 10))); - final mockCodec = MockCodec(); - mockCodec.frameCount = 2; - mockCodec.repetitionCount = -1; - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - var onImageCount = 0; - activeListener(ImageInfo image, bool synchronousCall) { - onImageCount += 1; - } - - var lastListenerDropped = false; - imageStream.addOnLastListenerRemovedCallback(() { - lastListenerDropped = true; - }); - - expect(lastListenerDropped, false); - final handle = imageStream.keepAlive(); - expect(lastListenerDropped, false); - SchedulerBinding.instance - .debugAssertNoTransientCallbacks('Only passive listeners'); - - codecStream.add(mockCodec); - await tester.idle(); - - expect(onImageCount, 0); - - final frame1 = FakeFrameInfo(Duration.zero, image20x10); - mockCodec.completeNextFrame(frame1); - await tester.idle(); - SchedulerBinding.instance - .debugAssertNoTransientCallbacks('Only passive listeners'); - await tester.pump(); - expect(onImageCount, 0); - - imageStream.addListener(ImageStreamListener(activeListener)); - - final frame2 = FakeFrameInfo(Duration.zero, image10x10!); - mockCodec.completeNextFrame(frame2); - await tester.idle(); - expect(SchedulerBinding.instance.transientCallbackCount, 1); - await tester.pump(); - - expect(onImageCount, 1); - - imageStream.removeListener(ImageStreamListener(activeListener)); - expect(lastListenerDropped, true); - - mockCodec.completeNextFrame(frame1); - await tester.idle(); - expect(SchedulerBinding.instance.transientCallbackCount, 1); - await tester.pump(); - - expect(onImageCount, 1); - - SchedulerBinding.instance - .debugAssertNoTransientCallbacks('Only passive listeners'); - - mockCodec.completeNextFrame(frame2); - await tester.idle(); - SchedulerBinding.instance - .debugAssertNoTransientCallbacks('Only passive listeners'); - await tester.pump(); - - expect(onImageCount, 1); - - handle.dispose(); - }); - - testWidgets('Multi-frame image is completed before next image is shown', - (WidgetTester tester) async { - final firstCodec = MockCodec(); - firstCodec.frameCount = 3; - firstCodec.repetitionCount = -1; - final secondCodec = MockCodec(); - - final codecStream = StreamController(); - - final ImageStreamCompleter imageStream = MultiImageStreamCompleter( - codec: codecStream.stream, - scale: 1.0, - ); - - listener(ImageInfo image, bool synchronousCall) {} - imageStream.addListener(ImageStreamListener(listener)); - - codecStream.add(firstCodec); - await tester.idle(); - - final FrameInfo frame1 = - FakeFrameInfo(const Duration(milliseconds: 200), image20x10); - final FrameInfo frame2 = - FakeFrameInfo(const Duration(milliseconds: 400), image200x100); - final FrameInfo frame3 = - FakeFrameInfo(const Duration(milliseconds: 200), image300x100); - - firstCodec.completeNextFrame(frame1); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(); // first animation frame shows on first app frame. - - firstCodec.completeNextFrame(frame2); - await tester.idle(); // let nextFrameFuture complete - await tester.pump(const Duration(milliseconds: 100)); // second frame is - // not yet shown, but ready. - - codecStream.add(secondCodec); - await tester.idle(); // let nextFrameFuture complete - - await tester.pump(const Duration(milliseconds: 300)); // emit 2nd frame. - firstCodec.completeNextFrame(frame3); - await tester.idle(); // let nextFrameFuture complete - expect(secondCodec.numFramesAsked, 0); - await tester.pump(const Duration(milliseconds: 200)); // emit 3rd frame. - await tester.idle(); - - // emit 1st frame 2nd image - await tester.pump(const Duration(milliseconds: 200)); - - // Decoding of the 3rd frame should not start as we switched images - expect(firstCodec.numFramesAsked, 3); - expect(secondCodec.numFramesAsked, 1); - }); -}