diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index 9513fb80..1465bd94 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,3 +1,8 @@ +## [3.4.6] - 2026-10-13 + +* Uses the asynchronous `exists()` instead of `existsSync()` when removing a cached file, so cache cleanup, `removeFile` and `emptyCache` no longer block the calling isolate on file system I/O. On Android with Flutter 3.29 or later this blocked the platform thread and could cause ANRs. +* Ignores a failed delete of a cached file when the file no longer exists, because a concurrent removal can delete it after the asynchronous existence check. + ## [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_store.dart b/flutter_cache_manager/lib/src/cache_store.dart index 44ffbfeb..43337c82 100644 --- a/flutter_cache_manager/lib/src/cache_store.dart +++ b/flutter_cache_manager/lib/src/cache_store.dart @@ -197,12 +197,13 @@ class CacheStore { } final file = await fileSystem.createFile(cacheObject.relativePath); - if (file.existsSync()) { + if (await file.exists()) { try { await file.delete(); - // ignore: unused_catch_clause - } on PathNotFoundException catch (e) { - // File has already been deleted. Do nothing #184 + } on FileSystemException { + // A concurrent removal can delete the file after the existence check. + // Ignore the failure only when the file is gone (#184). + if (await file.exists()) rethrow; } } } 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/cache_store_test.dart b/flutter_cache_manager/test/cache_store_test.dart index 412a35f1..bf844673 100644 --- a/flutter_cache_manager/test/cache_store_test.dart +++ b/flutter_cache_manager/test/cache_store_test.dart @@ -2,12 +2,16 @@ import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_cache_manager/src/cache_store.dart'; +import 'package:flutter_cache_manager/src/config/config.dart'; import 'package:flutter_cache_manager/src/storage/cache_object.dart'; +import 'package:flutter_cache_manager/src/storage/file_system/file_system.dart' + as cache_fs; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'helpers/config_extensions.dart'; import 'helpers/mock_cache_info_repository.dart'; +import 'helpers/mock_file_service.dart'; import 'helpers/test_configuration.dart'; void main() { @@ -440,6 +444,34 @@ void main() { ).called(1); }); + test('Store should not check file existence synchronously when removing ' + 'a cached file', () async { + var fileSystem = _AsyncOnlyFileSystem(); + var config = Config( + 'test', + fileSystem: fileSystem, + repo: MockCacheInfoRepository(), + fileService: MockFileService(), + ); + var store = CacheStore(config); + + var file = await fileSystem.inner.createFile(fileName); + await file.writeAsBytes([1, 3]); + var cacheObject = CacheObject( + fileUrl, + relativePath: fileName, + id: 1, + validTill: validTill, + ); + + await store.removeCachedFile(cacheObject); + + expect(await file.exists(), isFalse); + verify( + config.mockRepo.deleteAll(argThat(contains(cacheObject.id))), + ).called(1); + }); + test('Store should delete file when remove cached file', () async { var config = createTestConfig(); var store = CacheStore(config); @@ -464,3 +496,31 @@ Future createDir() async { final fileSystem = MemoryFileSystem(); return fileSystem.systemTempDirectory.createTemp('test'); } + +/// Wraps the files of [TestFileSystem] so that a synchronous existence check +/// fails the test. +class _AsyncOnlyFileSystem extends cache_fs.FileSystem { + final inner = TestFileSystem(); + + @override + Future createFile(String name) async => + _AsyncOnlyFile(await inner.createFile(name)); +} + +class _AsyncOnlyFile extends Fake implements File { + _AsyncOnlyFile(this._delegate); + + final File _delegate; + + @override + bool existsSync() => throw StateError('existsSync() must not be called'); + + @override + Future exists() => _delegate.exists(); + + @override + Future delete({bool recursive = false}) async { + await _delegate.delete(recursive: recursive); + return this; + } +}