Skip to content

Commit

Permalink
Unsubscribe from chunkEvents
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmartos96 committed Oct 22, 2022
1 parent ad4dea3 commit ce6d671
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class CachedNetworkImageProvider
ImageStreamCompleter load(
image_provider.CachedNetworkImageProvider key, DecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
final imageStreamCompleter = MultiImageStreamCompleter(
return MultiImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
Expand All @@ -89,10 +89,6 @@ class CachedNetworkImageProvider
);
},
);

_handleChunkEventsClose(imageStreamCompleter, chunkEvents);

return imageStreamCompleter;
}

@Deprecated(
Expand Down Expand Up @@ -122,7 +118,7 @@ class CachedNetworkImageProvider
ImageStreamCompleter loadBuffer(image_provider.CachedNetworkImageProvider key,
DecoderBufferCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
final imageStreamCompleter = MultiImageStreamCompleter(
return MultiImageStreamCompleter(
codec: _loadBufferAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
Expand All @@ -134,10 +130,6 @@ class CachedNetworkImageProvider
);
},
);

_handleChunkEventsClose(imageStreamCompleter, chunkEvents);

return imageStreamCompleter;
}

Stream<ui.Codec> _loadBufferAsync(
Expand Down Expand Up @@ -172,21 +164,6 @@ class CachedNetworkImageProvider
return false;
}

void _handleChunkEventsClose(
ImageStreamCompleter imageStreamCompleter,
StreamController<ImageChunkEvent> chunkEvents,
) {
// Make sure to close the chunk events controller after
// the image stream disposes. Otherwise reporting an image chunk
// event could fail beacause the ImageStream has been disposed.
// (https://github.com/Baseflow/flutter_cached_network_image/issues/785)
imageStreamCompleter.addOnLastListenerRemovedCallback(() {
if (!chunkEvents.isClosed) {
chunkEvents.close();
}
});
}

@override
int get hashCode => Object.hash(cacheKey ?? url, scale, maxHeight, maxWidth);

Expand Down
Original file line number Diff line number Diff line change
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 has 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
Original file line number Diff line number Diff line change
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 ce6d671

Please sign in to comment.