Skip to content

Commit

Permalink
Merge pull request #792 from davidmartos96/closeChunksStream
Browse files Browse the repository at this point in the history
Fix #785 Cancel chunk events stream subscription when imageStream is disposed
  • Loading branch information
renefloor committed Nov 25, 2022
2 parents 3fcd65a + ac60e35 commit c9865f7
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 1 deletion.
Expand Up @@ -38,7 +38,7 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
);
});
if (chunkEvents != null) {
chunkEvents.listen(
_chunkSubscription = chunkEvents.listen(
reportImageChunkEvent,
onError: (dynamic error, StackTrace stack) {
reportError(
Expand All @@ -65,10 +65,17 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
// How many frames have been emitted so far.
int _framesEmitted = 0;
Timer? _timer;
StreamSubscription<ImageChunkEvent>? _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;
Expand Down Expand Up @@ -159,6 +166,7 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {

@override
void addListener(ImageStreamListener listener) {
__hadAtLeastOneListener = true;
if (!hasListeners && _codec != null) _decodeNextFrameAndSchedule();
super.addListener(listener);
}
Expand All @@ -169,6 +177,56 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
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;
}
}
31 changes: 31 additions & 0 deletions cached_network_image/test/image_stream_completer_test.dart
Expand Up @@ -96,6 +96,37 @@ void main() {
expect(tester.takeException(), 'failure message');
});

test('Completer unsubscribes to chunk events when disposed', () async {
final codecStream = StreamController<Codec>();
final chunkStream = StreamController<ImageChunkEvent>();

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<Codec>();
Expand Down

0 comments on commit c9865f7

Please sign in to comment.