fix(mobile): improve image load cancellation handling (#27624)

fix(image): improve image load cancellation handling
This commit is contained in:
Luis Nachtigall 2026-04-08 23:23:42 +02:00 committed by GitHub
parent 55ab8c65b6
commit 2b0f6c9202
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 31 additions and 13 deletions

View file

@ -19,6 +19,7 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
static final _log = Logger('CancellableImageProviderMixin'); static final _log = Logger('CancellableImageProviderMixin');
bool isCancelled = false; bool isCancelled = false;
bool isFinished = false;
ImageRequest? request; ImageRequest? request;
CancelableOperation<ImageInfo?>? cachedOperation; CancelableOperation<ImageInfo?>? cachedOperation;
@ -53,13 +54,15 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* { Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* {
if (isCancelled) { if (isCancelled) {
this.request = null; this.request = null;
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
try { try {
final image = await request.load(decode); final image = await request.load(decode);
if ((image == null && evictOnError) || isCancelled) { if (isCancelled) {
return;
}
if (image == null && evictOnError) {
PaintingBinding.instance.imageCache.evict(this); PaintingBinding.instance.imageCache.evict(this);
return; return;
} else if (image == null) { } else if (image == null) {
@ -67,6 +70,9 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
} }
yield image; yield image;
} catch (e, stack) { } catch (e, stack) {
if (isCancelled) {
return;
}
if (evictOnError) { if (evictOnError) {
PaintingBinding.instance.imageCache.evict(this); PaintingBinding.instance.imageCache.evict(this);
rethrow; rethrow;
@ -80,20 +86,24 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
Future<ui.Codec?> loadCodecRequest(ImageRequest request) async { Future<ui.Codec?> loadCodecRequest(ImageRequest request) async {
if (isCancelled) { if (isCancelled) {
this.request = null; this.request = null;
PaintingBinding.instance.imageCache.evict(this);
return null; return null;
} }
try { try {
final codec = await request.loadCodec(); final codec = await request.loadCodec();
if (codec == null || isCancelled) { if (isCancelled) {
codec?.dispose(); codec?.dispose();
return null;
}
if (codec == null) {
PaintingBinding.instance.imageCache.evict(this); PaintingBinding.instance.imageCache.evict(this);
return null; return null;
} }
return codec; return codec;
} catch (e) { } catch (e) {
if (!isCancelled) {
PaintingBinding.instance.imageCache.evict(this); PaintingBinding.instance.imageCache.evict(this);
}
rethrow; rethrow;
} finally { } finally {
this.request = null; this.request = null;
@ -121,6 +131,8 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
@override @override
void cancel() { void cancel() {
isCancelled = true; isCancelled = true;
final hasActiveWork = !isFinished;
final request = this.request; final request = this.request;
if (request != null) { if (request != null) {
this.request = null; this.request = null;
@ -132,6 +144,10 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
cachedOperation = null; cachedOperation = null;
operation.cancel(); operation.cancel();
} }
if (hasActiveWork) {
PaintingBinding.instance.imageCache.evict(this);
}
} }
} }

View file

@ -100,7 +100,6 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
@ -113,24 +112,24 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
yield* loadRequest(request, decode); yield* loadRequest(request, decode);
if (!Store.get(StoreKey.loadOriginal, false)) { if (!Store.get(StoreKey.loadOriginal, false)) {
isFinished = true;
return; return;
} }
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero); request = this.request = LocalImageRequest(localId: key.id, assetType: key.assetType, size: Size.zero);
yield* loadRequest(request, decode); yield* loadRequest(request, decode);
isFinished = true;
} }
Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* { Stream<Object> _animatedCodec(LocalFullImageProvider key, ImageDecoderCallback decode) async* {
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
@ -143,7 +142,6 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
yield* loadRequest(previewRequest, decode); yield* loadRequest(previewRequest, decode);
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
@ -151,9 +149,11 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
final originalRequest = request = LocalImageRequest(localId: key.id, size: Size.zero, assetType: key.assetType); final originalRequest = request = LocalImageRequest(localId: key.id, size: Size.zero, assetType: key.assetType);
final codec = await loadCodecRequest(originalRequest); final codec = await loadCodecRequest(originalRequest);
if (codec == null) { if (codec == null) {
if (isCancelled) return;
throw StateError('Failed to load animated codec for local asset ${key.id}'); throw StateError('Failed to load animated codec for local asset ${key.id}');
} }
yield codec; yield codec;
isFinished = true;
} }
@override @override

View file

@ -105,7 +105,6 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
@ -116,23 +115,23 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
yield* loadRequest(previewRequest, decode, evictOnError: !loadOriginal); yield* loadRequest(previewRequest, decode, evictOnError: !loadOriginal);
if (!loadOriginal) { if (!loadOriginal) {
isFinished = true;
return; return;
} }
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId)); final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
yield* loadRequest(originalRequest, decode); yield* loadRequest(originalRequest, decode);
isFinished = true;
} }
Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* { Stream<Object> _animatedCodec(RemoteFullImageProvider key, ImageDecoderCallback decode) async* {
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
@ -142,7 +141,6 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
yield* loadRequest(previewRequest, decode, evictOnError: false); yield* loadRequest(previewRequest, decode, evictOnError: false);
if (isCancelled) { if (isCancelled) {
PaintingBinding.instance.imageCache.evict(this);
return; return;
} }
@ -150,9 +148,13 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId)); final originalRequest = request = RemoteImageRequest(uri: getOriginalUrlForRemoteId(key.assetId));
final codec = await loadCodecRequest(originalRequest); final codec = await loadCodecRequest(originalRequest);
if (codec == null) { if (codec == null) {
if (isCancelled) {
return;
}
throw StateError('Failed to load animated codec for asset ${key.assetId}'); throw StateError('Failed to load animated codec for asset ${key.assetId}');
} }
yield codec; yield codec;
isFinished = true;
} }
@override @override