diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index 9513fb80..94e86419 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## [3.4.6] - 2026-09-28 + +* Allows failed resized-image downloads to be retried by clearing completed resize streams ([#439](https://github.com/Baseflow/flutter_cache_manager/issues/439)). + ## [3.4.5] - 2026-09-18 * Prevents image resize decoding from completing more than once and propagates decoding errors ([#400](https://github.com/Baseflow/flutter_cache_manager/issues/400)). diff --git a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart index 5851a7ab..8dc2084d 100644 --- a/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart +++ b/flutter_cache_manager/lib/src/cache_managers/image_cache_manager.dart @@ -50,23 +50,53 @@ 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> _runningResizes = {}; + Stream _trackResizeStream( + String resizedKey, + Stream stream, + ) { + // Clear the in-flight entry on both success and error so failures can be + // retried by a later request. + late final Stream trackedStream; + trackedStream = stream + .transform( + StreamTransformer.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; + } + Future _resizeImageFile( FileInfo originalFile, String key, diff --git a/flutter_cache_manager/pubspec.yaml b/flutter_cache_manager/pubspec.yaml index 3f0c9ce3..8456c4b5 100644 --- a/flutter_cache_manager/pubspec.yaml +++ b/flutter_cache_manager/pubspec.yaml @@ -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.4.5 +version: 3.4.6 homepage: https://github.com/Baseflow/flutter_cache_manager topics: - cache diff --git a/flutter_cache_manager/test/image_cache_manager_test.dart b/flutter_cache_manager/test/image_cache_manager_test.dart index ce8ec599..0ea54dec 100644 --- a/flutter_cache_manager/test/image_cache_manager_test.dart +++ b/flutter_cache_manager/test/image_cache_manager_test.dart @@ -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'; const fileName = 'test.jpg'; @@ -71,6 +73,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 = await getExampleImage(); + 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()), + ); + + 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()); + }); + test('Resized image should be fetched from cache', () async { var config = await setupConfig(cacheKey: 'resized_w100_h80_$fileUrl'); var cacheManager = TestCacheManager(config);