fix(mobile): fix flutter cache eviction on thumbnails (#27663)

* fix: add markFinished parameter to loadRequest and loadCodecRequest methods

* update loadRequest and loadCodecRequest methods to use isFinal

* Apply suggestions from code review

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>

* remove redundant check

* fix: ensure isFinished is set correctly during cache eviction

* formatting

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
Luis Nachtigall 2026-04-10 17:28:55 +02:00 committed by GitHub
parent bc400d68ac
commit d39e7da10d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 26 additions and 35 deletions

View File

@ -51,7 +51,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
return null;
}
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* {
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {required bool isFinal}) async* {
if (isCancelled) {
this.request = null;
return;
@ -59,21 +59,18 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
try {
final image = await request.load(decode);
if (isCancelled) {
return;
}
if (image == null && evictOnError) {
PaintingBinding.instance.imageCache.evict(this);
return;
} else if (image == null) {
if (isCancelled || image == null) {
image?.dispose();
return;
}
isFinished = isFinal;
yield image;
} catch (e, stack) {
if (isCancelled) {
return;
}
if (evictOnError) {
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
@ -83,7 +80,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
}
}
Future<ui.Codec?> loadCodecRequest(ImageRequest request) async {
Future<ui.Codec?> loadCodecRequest(ImageRequest request, {required bool isFinal}) async {
if (isCancelled) {
this.request = null;
return null;
@ -91,20 +88,19 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
try {
final codec = await request.loadCodec();
if (isCancelled) {
if (isCancelled || codec == null) {
codec?.dispose();
return null;
}
if (codec == null) {
PaintingBinding.instance.imageCache.evict(this);
return null;
}
isFinished = isFinal;
return codec;
} catch (e) {
if (!isCancelled) {
if (isFinal) {
isFinished = true;
PaintingBinding.instance.imageCache.evict(this);
rethrow;
}
rethrow;
return null;
} finally {
this.request = null;
}

View File

@ -36,7 +36,7 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
Stream<ImageInfo> _codec(LocalThumbProvider key, ImageDecoderCallback decode) {
final request = this.request = LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType);
return loadRequest(request, decode);
return loadRequest(request, decode, isFinal: true);
}
@override
@ -103,16 +103,16 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
return;
}
final loadOriginal = Store.get(StoreKey.loadOriginal, false);
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
var request = this.request = LocalImageRequest(
localId: key.id,
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(request, decode);
yield* loadRequest(request, decode, isFinal: !loadOriginal);
if (!Store.get(StoreKey.loadOriginal, false)) {
isFinished = true;
if (!loadOriginal) {
return;
}
@ -122,8 +122,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
yield* loadRequest(request, decode);
isFinished = true;
yield* loadRequest(request, decode, isFinal: true);
}
Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
@ -139,7 +138,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
size: Size(size.width * devicePixelRatio, size.height * devicePixelRatio),
assetType: key.assetType,
);
yield* loadRequest(previewRequest, decode);
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) {
return;
@ -147,13 +146,12 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
// always try original for animated, since previews don't support animation
final originalRequest = request = LocalImageRequest(localId: key.id, size: Size.zero, assetType: key.assetType);
final codec = await loadCodecRequest(originalRequest);
final codec = await loadCodecRequest(originalRequest, isFinal: true);
if (codec == null) {
if (isCancelled) return;
throw StateError('Failed to load animated codec for local asset ${key.id}');
}
yield codec;
isFinished = true;
}
@override

View File

@ -38,7 +38,7 @@ class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
Stream<ImageInfo> _codec(RemoteImageProvider key, ImageDecoderCallback decode) {
final request = this.request = RemoteImageRequest(uri: key.url);
return loadRequest(request, decode);
return loadRequest(request, decode, isFinal: true);
}
@override
@ -112,10 +112,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
);
final loadOriginal = assetType == AssetType.image && AppSetting.get(Setting.loadOriginal);
yield* loadRequest(previewRequest, decode, evictOnError: !loadOriginal);
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (!loadOriginal) {
isFinished = true;
return;
}
@ -124,8 +123,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
}
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
yield* loadRequest(originalRequest, decode);
isFinished = true;
yield* loadRequest(originalRequest, decode, isFinal: true);
}
Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
@ -138,7 +136,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
final previewRequest = request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId, type: AssetMediaSize.preview, thumbhash: key.thumbhash),
);
yield* loadRequest(previewRequest, decode, evictOnError: false);
yield* loadRequest(previewRequest, decode, isFinal: false);
if (isCancelled) {
return;
@ -146,7 +144,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
// always try original for animated, since previews don't support animation
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
final codec = await loadCodecRequest(originalRequest);
final codec = await loadCodecRequest(originalRequest, isFinal: true);
if (codec == null) {
if (isCancelled) {
return;
@ -154,7 +152,6 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
throw StateError('Failed to load animated codec for asset ${key.assetId}');
}
yield codec;
isFinished = true;
}
@override

View File

@ -22,7 +22,7 @@ class ThumbHashProvider extends CancellableImageProvider<ThumbHashProvider>
Stream<ImageInfo> _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) {
final request = this.request = ThumbhashImageRequest(thumbhash: key.thumbHash);
return loadRequest(request, decode);
return loadRequest(request, decode, isFinal: true);
}
@override