Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cached_network_image/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## [4.0.5] - 2026-10-17

* Stop decoding a static image again each time a listener is re-added, which blanked images on web (Firefox and Safari) after navigating back to a route
* Resume an animated image from its waiting frame when a listener is re-added, instead of skipping that frame
* Reset the frame count when a new codec arrives, so an animation that follows a placeholder shows all of its frames
* Discard frames from a codec that has been replaced, whether still decoding or waiting to be shown, so a stale cached image is no longer shown over the refreshed one
* Dispose frames that finish decoding after the completer is disposed, instead of leaking them
* Ignore listener re-adds while a frame is already being decoded; a failed decode no longer blocks later decodes
* Add tests for listener re-add, failed decodes and codec switches during decoding

## [4.0.4] - 2026-09-30

* Bump `flutter_cache_manager` to `^3.4.5`, `material_ui` to `^1.3.0`, `octo_image` to `^2.1.1` and `mocktail` to `^1.0.5`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {

// How many frames have been emitted so far.
int _framesEmitted = 0;
bool _isDecoding = false;
Timer? _timer;
StreamSubscription<ImageChunkEvent>? _chunkSubscription;

Expand All @@ -84,14 +85,19 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
bool __disposed = false;

void _switchToNewCodec() {
_framesEmitted = 0;
_timer = null;
_handleCodecReady(_nextImageCodec!);
_nextImageCodec = null;
}

void _handleCodecReady(ui.Codec codec) {
Comment thread
shihabkandil marked this conversation as resolved.
_codec = codec;
_framesEmitted = 0;
_isDecoding = false;
// A frame still waiting from the previous codec must not be shown as part
// of this one.
_nextFrame?.image.dispose();
_nextFrame = null;

if (hasListeners) {
_decodeNextFrameAndSchedule();
Expand All @@ -100,7 +106,7 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {

void _handleAppFrame(Duration timestamp) {
_frameCallbackScheduled = false;
if (!hasListeners) return;
if (!hasListeners || _nextFrame == null) return;
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
_emitFrame(
ImageInfo(
Expand Down Expand Up @@ -136,9 +142,18 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
}

Future<void> _decodeNextFrameAndSchedule() async {
if (_isDecoding) {
return;
}
_isDecoding = true;
final codec = _codec!;
final ui.FrameInfo frame;
try {
_nextFrame = await _codec!.getNextFrame();
frame = await codec.getNextFrame();
} on Object catch (exception, stack) {
if (_codec == codec) {
Comment thread
rickdijk marked this conversation as resolved.
_isDecoding = false;
}
reportError(
context: ErrorDescription('resolving an image frame'),
exception: exception,
Expand All @@ -148,25 +163,26 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
);
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;
}

// A newer codec replaced this one while decoding; it resets _isDecoding.
if (_codec != codec) {
frame.image.dispose();
return;
}
_isDecoding = false;
if (__disposed) {
frame.image.dispose();
return;
}
if (codec.frameCount == 1) {
Comment thread
rickdijk marked this conversation as resolved.
// This is not an animated image, just return it and don't schedule more
// frames.
_emitFrame(
ImageInfo(
image: _nextFrame!.image,
scale: _scale,
debugLabel: debugLabel,
),
ImageInfo(image: frame.image, scale: _scale, debugLabel: debugLabel),
);
return;
}
_nextFrame?.image.dispose();
_nextFrame = frame;
_scheduleAppFrame();
}

Expand All @@ -186,7 +202,14 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
@override
void addListener(ImageStreamListener listener) {
__hadAtLeastOneListener = true;
if (!hasListeners && _codec != null) _decodeNextFrameAndSchedule();
if (!hasListeners && _codec != null) {
if (_nextFrame != null) {
// Show the frame decoded while nobody listened instead of skipping it.
_scheduleAppFrame();
} else if (_framesEmitted == 0 || _codec!.frameCount > 1) {
_decodeNextFrameAndSchedule();
}
}
super.addListener(listener);
}

Expand Down
2 changes: 1 addition & 1 deletion cached_network_image/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ topics:
- cache
- image
- network-image
version: 4.0.4
version: 4.0.5

environment:
sdk: ^3.12.0
Expand Down
Loading
Loading