Skip to content
Open
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
4 changes: 4 additions & 0 deletions flutter_cache_manager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [3.5.1] - 2026-11-05

* Allows failed resized-image downloads to be retried by clearing completed resize streams ([#439](https://github.com/Baseflow/flutter_cache_manager/issues/439)).

## [3.5.0] - 2026-11-04

* **DEPRECATION** Deprecates `supportedFileNames`: whether an image is resized is now decided by the image engine from the file's content instead of from the file extension derived from the `Content-Type` header of the response ([#475](https://github.com/Baseflow/flutter_cache_manager/pull/475))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,21 @@ mixin ImageCacheManager on BaseCacheManager {
}
var runningResize = _runningResizes[resizedKey];
if (runningResize == null) {
runningResize = _fetchedResizedFile(
url,
key,
runningResize = _trackResizeStream(
resizedKey,
headers,
withProgress,
maxWidth: maxWidth,
maxHeight: maxHeight,
).asBroadcastStream();
_fetchedResizedFile(
url,
key,
resizedKey,
headers,
withProgress,
maxWidth: maxWidth,
maxHeight: maxHeight,
),
);
_runningResizes[resizedKey] = runningResize;
}
yield* runningResize;
_runningResizes.remove(resizedKey);
}

final Map<String, Stream<FileResponse>> _runningResizes = {};
Expand All @@ -89,6 +91,34 @@ mixin ImageCacheManager on BaseCacheManager {
/// when the manager instance is recreated.
final Set<String> _notResizableKeys = {};

Stream<FileResponse> _trackResizeStream(
String resizedKey,
Stream<FileResponse> stream,
) {
// Clear the in-flight entry on both success and error so failures can be
// retried by a later request.
late final Stream<FileResponse> trackedStream;
trackedStream = stream
.transform(
StreamTransformer<FileResponse, FileResponse>.fromHandlers(
handleError: (Object error, StackTrace stackTrace, sink) {
if (identical(_runningResizes[resizedKey], trackedStream)) {
_runningResizes.remove(resizedKey);
}
sink.addError(error, stackTrace);
},
handleDone: (sink) {
if (identical(_runningResizes[resizedKey], trackedStream)) {
_runningResizes.remove(resizedKey);
}
sink.close();
},
),
)
.asBroadcastStream();
return trackedStream;
}

Stream<FileResponse> _fetchedResizedFile(
String url,
String originalKey,
Expand Down
2 changes: 1 addition & 1 deletion flutter_cache_manager/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_cache_manager
description: Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
version: 3.5.0
version: 3.5.1
homepage: https://github.com/Baseflow/flutter_cache_manager
topics:
- cache
Expand Down
47 changes: 47 additions & 0 deletions flutter_cache_manager/test/image_cache_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';

import 'cache_manager_test.dart';
import 'helpers/config_extensions.dart';
import 'helpers/mock_file_fetcher_response.dart';
import 'helpers/test_configuration.dart';
import 'test_images.dart';

Expand Down Expand Up @@ -72,6 +74,51 @@ void main() {
);
});
group('Test resized image caching', () {
test('Failed resize download can be retried', () async {
final config = createTestConfig();
final cacheManager = TestCacheManager(config);
var downloadCount = 0;
when(
config.mockFileService.get(fileUrl, headers: anyNamed('headers')),
).thenAnswer((_) async {
downloadCount++;
if (downloadCount == 1) {
return MockFileFetcherResponse(
const Stream.empty(),
0,
null,
'',
404,
DateTime.now(),
);
}

final imageBytes = exampleImage;
return MockFileFetcherResponse(
Stream.value(imageBytes),
imageBytes.length,
'testv1',
'.png',
200,
DateTime.now().add(const Duration(days: 1)),
);
});

await expectLater(
cacheManager.getImageFile(fileUrl, maxHeight: 100).toList(),
throwsA(isA<HttpExceptionWithStatus>()),
);

final results = await cacheManager
.getImageFile(fileUrl, maxHeight: 100)
.timeout(const Duration(seconds: 1))
.toList();

expect(downloadCount, 2);
expect(results, hasLength(1));
expect(results.single, isA<FileInfo>());
});

test('Resized image should be fetched from cache', () async {
var config = await setupConfig(cacheKey: 'resized_w100_h80_$fileUrl');
var cacheManager = TestCacheManager(config);
Expand Down
Loading