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
7 changes: 7 additions & 0 deletions cached_network_image_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [2.0.4] - 2026-10-23

* Report a failure to read or decode a cached file on the `HttpGet` render method to the image stream, evict the image and the broken cache entry, and close the streams, instead of leaving the image loading forever
* A stale cached file that fails to decode is only a failure if no fresh file follows, and a failure after a frame was shown keeps that frame
* Close the streams after a download error too
* Only the web loader removes the broken cache entry; the IO loader is unchanged

## [2.0.3] - 2026-09-29

* Bump `flutter_cache_manager` to `^3.4.5`, `material_ui` to `^1.3.0` and `web` to `^1.1.1`
Expand Down
112 changes: 78 additions & 34 deletions cached_network_image_web/lib/cached_network_image_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'src/create_image_codec_from_url_stub.dart'
if (dart.library.ui_web) 'src/create_image_codec_from_url_web.dart';

enum _State { open, waitingForData, closing }

/// ImageLoader class to load images on the web platform.
class ImageLoader implements platform.ImageLoader {
@Deprecated('Use loadImageAsync instead')
Expand Down Expand Up @@ -121,7 +119,74 @@ class ImageLoader implements platform.ImageLoader {
Map<String, String>? headers,
VoidCallback evictImage,
) {
var streamController = StreamController<ui.Codec>();
final streamController = StreamController<ui.Codec>();

var downloadDone = false;
var pendingDecodes = 0;
var finished = false;
var settling = false;
var decoded = false;
(Object, StackTrace)? decodeFailure;
(Object, StackTrace)? downloadFailure;

void finish() {
if (finished) return;
finished = true;
streamController.close();
chunkEvents.close();
}

void fail(Object e, StackTrace st) {
if (finished) return;
scheduleMicrotask(() {
evictImage();
});
streamController.addError(e, st);
finish();
}

// Runs once the download is over and every decode has finished, so a
// stale file that fails to decode is only fatal when no fresh file came.
Future<void> settle() async {
final failure = downloadFailure ?? (decoded ? null : decodeFailure);
if (decodeFailure != null && !decoded) {
try {
// Drop the broken entry so the next attempt downloads it again.
await cacheManager.removeFile(cacheKey ?? url);
} on Object catch (_) {
// Keep the original error visible.
}
}
if (failure == null) {
finish();
} else {
fail(failure.$1, failure.$2);
}
}

void closeIfComplete() {
if (settling || !downloadDone || pendingDecodes > 0) return;
settling = true;
unawaited(settle());
}

Future<void> decodeFile(FileInfo info) async {
try {
final bytes = await info.file.readAsBytes();
final codec = await decode(bytes);
if (finished) {
codec.dispose();
} else {
decoded = true;
streamController.add(codec);
Comment thread
rickdijk marked this conversation as resolved.
Resolved
}
} on Object catch (e, st) {
decodeFailure = (e, st);
} finally {
pendingDecodes--;
closeIfComplete();
}
}

try {
final stream = cacheManager.getFileStream(
Expand All @@ -131,8 +196,6 @@ class ImageLoader implements platform.ImageLoader {
key: cacheKey,
);

var state = _State.open;

stream.listen(
(event) {
if (event is DownloadProgress) {
Expand All @@ -144,42 +207,23 @@ class ImageLoader implements platform.ImageLoader {
);
}
if (event is FileInfo) {
if (state == _State.open) {
state = _State.waitingForData;
}

event.file.readAsBytes().then((value) => decode(value)).then((
data,
) {
streamController.add(data);
if (state == _State.closing) {
streamController.close();
chunkEvents.close();
}
});
pendingDecodes++;
unawaited(decodeFile(event));
}
},
onError: (e, st) {
scheduleMicrotask(() {
evictImage();
});
streamController.addError(e, st);
onError: (Object e, StackTrace st) {
downloadFailure = (e, st);
downloadDone = true;
closeIfComplete();
},
onDone: () async {
if (state == _State.open) {
streamController.close();
chunkEvents.close();
} else if (state == _State.waitingForData) {
state = _State.closing;
}
onDone: () {
downloadDone = true;
closeIfComplete();
},
cancelOnError: true,
);
} on Object catch (e, st) {
scheduleMicrotask(() {
evictImage();
});
streamController.addError(e, st);
fail(e, st);
}

return streamController.stream;
Expand Down
2 changes: 1 addition & 1 deletion cached_network_image_web/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: cached_network_image_web
description: Web implementation of CachedNetworkImage
version: 2.0.3
version: 2.0.4
homepage: https://github.com/Baseflow/flutter_cached_network_image

environment:
Expand Down
Loading
Loading