Skip to content

Commit c816613

Browse files
author
Claude
committed
chore(sync): merge upstream v4.0.5
Picks up the one upstream commit since this branch's previous merge: e0ca258 'fix: Avoid decoding static images again when a listener is re-added (Baseflow#1062)', released as v4.0.5.
2 parents f3b96a5 + e0ca258 commit c816613

4 files changed

Lines changed: 610 additions & 19 deletions

File tree

‎cached_network_image/CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## [4.0.5] - 2026-10-17
2+
3+
* 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
4+
* Resume an animated image from its waiting frame when a listener is re-added, instead of skipping that frame
5+
* Reset the frame count when a new codec arrives, so an animation that follows a placeholder shows all of its frames
6+
* 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
7+
* Dispose frames that finish decoding after the completer is disposed, instead of leaking them
8+
* Ignore listener re-adds while a frame is already being decoded; a failed decode no longer blocks later decodes
9+
* Add tests for listener re-add, failed decodes and codec switches during decoding
10+
111
## [4.0.4] - 2026-09-30
212

313
* 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`

‎cached_network_image/lib/src/image_provider/multi_image_stream_completer.dart‎

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
7171

7272
// How many frames have been emitted so far.
7373
int _framesEmitted = 0;
74+
bool _isDecoding = false;
7475
Timer? _timer;
7576
StreamSubscription<ImageChunkEvent>? _chunkSubscription;
7677

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

8687
void _switchToNewCodec() {
87-
_framesEmitted = 0;
8888
_timer = null;
8989
_handleCodecReady(_nextImageCodec!);
9090
_nextImageCodec = null;
9191
}
9292

9393
void _handleCodecReady(ui.Codec codec) {
9494
_codec = codec;
95+
_framesEmitted = 0;
96+
_isDecoding = false;
97+
// A frame still waiting from the previous codec must not be shown as part
98+
// of this one.
99+
_nextFrame?.image.dispose();
100+
_nextFrame = null;
95101

96102
if (hasListeners) {
97103
_decodeNextFrameAndSchedule();
@@ -100,7 +106,7 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
100106

101107
void _handleAppFrame(Duration timestamp) {
102108
_frameCallbackScheduled = false;
103-
if (!hasListeners) return;
109+
if (!hasListeners || _nextFrame == null) return;
104110
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
105111
_emitFrame(
106112
ImageInfo(
@@ -136,9 +142,18 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
136142
}
137143

138144
Future<void> _decodeNextFrameAndSchedule() async {
145+
if (_isDecoding) {
146+
return;
147+
}
148+
_isDecoding = true;
149+
final codec = _codec!;
150+
final ui.FrameInfo frame;
139151
try {
140-
_nextFrame = await _codec!.getNextFrame();
152+
frame = await codec.getNextFrame();
141153
} on Object catch (exception, stack) {
154+
if (_codec == codec) {
155+
_isDecoding = false;
156+
}
142157
reportError(
143158
context: ErrorDescription('resolving an image frame'),
144159
exception: exception,
@@ -148,25 +163,26 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
148163
);
149164
return;
150165
}
151-
if (_codec!.frameCount == 1) {
152-
// ImageStreamCompleter listeners removed while waiting for next frame to
153-
// be decoded.
154-
// There's no reason to emit the frame without active listeners.
155-
if (!hasListeners) {
156-
return;
157-
}
158-
166+
// A newer codec replaced this one while decoding; it resets _isDecoding.
167+
if (_codec != codec) {
168+
frame.image.dispose();
169+
return;
170+
}
171+
_isDecoding = false;
172+
if (__disposed) {
173+
frame.image.dispose();
174+
return;
175+
}
176+
if (codec.frameCount == 1) {
159177
// This is not an animated image, just return it and don't schedule more
160178
// frames.
161179
_emitFrame(
162-
ImageInfo(
163-
image: _nextFrame!.image,
164-
scale: _scale,
165-
debugLabel: debugLabel,
166-
),
180+
ImageInfo(image: frame.image, scale: _scale, debugLabel: debugLabel),
167181
);
168182
return;
169183
}
184+
_nextFrame?.image.dispose();
185+
_nextFrame = frame;
170186
_scheduleAppFrame();
171187
}
172188

@@ -186,7 +202,14 @@ class MultiImageStreamCompleter extends ImageStreamCompleter {
186202
@override
187203
void addListener(ImageStreamListener listener) {
188204
__hadAtLeastOneListener = true;
189-
if (!hasListeners && _codec != null) _decodeNextFrameAndSchedule();
205+
if (!hasListeners && _codec != null) {
206+
if (_nextFrame != null) {
207+
// Show the frame decoded while nobody listened instead of skipping it.
208+
_scheduleAppFrame();
209+
} else if (_framesEmitted == 0 || _codec!.frameCount > 1) {
210+
_decodeNextFrameAndSchedule();
211+
}
212+
}
190213
super.addListener(listener);
191214
}
192215

‎cached_network_image/pubspec.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ topics:
66
- cache
77
- image
88
- network-image
9-
version: 4.0.4
9+
version: 4.0.5
1010

1111
environment:
1212
sdk: ^3.12.0

0 commit comments

Comments
 (0)