-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Some possible approaches
Approach 1: Add ImageCache.evictWhere((key) => ...) in addition to evict(...).
Approach 2: Let ImageCache automatically evict failed caches.
Approach 3: Add a cache key in addition to the current key.
Is there an existing issue for this?
- I have searched the existing issues
- I have read the guide to filing a bug
Steps to reproduce
(I will PR for this bug)
Hi thanks for the Flutter framework! When resized network image has error, all future unrelated images using the same url will fail, even if the network becomes OK.
After digging into source code, the cause is as follows: Even if NetworkImage does call imageCache.evict(), it is using the wrong key when it is wrapped with a ResizedImage. In other words, it is imageCache.evict(instanceOfNetworkImage), while the real key is an instance of the ResizedImage.
The following reproducible sample, though seemingly quite lengthy, is indeed very simple: Copy-and-paste (and slightly simplify) the https://github.com/flutter/flutter/blob/master/packages/flutter/test/painting/image_provider_network_image_test.dart. Then, just modify one line:
// final ImageProvider imageProvider = NetworkImage(nonconst('testing.url'));
final ImageProvider imageProvider = ResizeImage(NetworkImage(nonconst('testing.url')), width: 5, height: 5);i.e. Add a ResizeImage. Then the tests fail.
Run test.
Expected results
no error.
Actual results
has error.
Code sample
Code sample
import 'dart:async';
import 'dart:io';
import 'dart:ui' show Codec, FrameInfo;
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late _FakeHttpClient httpClient;
setUp(() {
httpClient = _FakeHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
});
tearDown(() {
debugNetworkImageHttpClientProvider = null;
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
});
test('Expect thrown exception with statusCode - evicts from cache and drains', () async {
const int errorStatusCode = HttpStatus.notFound;
const String requestUrl = 'foo-url';
httpClient.request.response.statusCode = errorStatusCode;
final Completer<dynamic> caughtError = Completer<dynamic>();
// final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
final ImageProvider imageProvider = ResizeImage(NetworkImage(nonconst(requestUrl)), width: 5, height: 5);
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1);
expect(imageCache.statusForKey(imageProvider).pending, true);
result.addListener(
ImageStreamListener((ImageInfo info, bool syncCall) {}, onError: (dynamic error, StackTrace? stackTrace) {
caughtError.complete(error);
}));
final dynamic err = await caughtError.future;
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect(
err,
isA<NetworkImageLoadException>()
.having((NetworkImageLoadException e) => e.statusCode, 'statusCode', errorStatusCode)
.having((NetworkImageLoadException e) => e.uri, 'uri', Uri.base.resolve(requestUrl)),
);
expect(httpClient.request.response.drained, true);
}, skip: isBrowser); // [intended] Browser implementation does not use HTTP client but an <img> tag.
test('NetworkImage is evicted from cache on SocketException', () async {
final _FakeHttpClient mockHttpClient = _FakeHttpClient();
mockHttpClient.thrownError = const SocketException('test exception');
debugNetworkImageHttpClientProvider = () => mockHttpClient;
// final ImageProvider imageProvider = NetworkImage(nonconst('testing.url'));
final ImageProvider imageProvider = ResizeImage(NetworkImage(nonconst('testing.url')), width: 5, height: 5);
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1);
expect(imageCache.statusForKey(imageProvider).pending, true);
final Completer<dynamic> caughtError = Completer<dynamic>();
result.addListener(ImageStreamListener(
(ImageInfo info, bool syncCall) {},
onError: (dynamic error, StackTrace? stackTrace) {
caughtError.complete(error);
},
));
final dynamic err = await caughtError.future;
expect(err, isA<SocketException>());
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect(imageCache.containsKey(result), isFalse);
debugNetworkImageHttpClientProvider = null;
}, skip: isBrowser); // [intended] Browser does not resolve images this way.
}
class _FakeHttpClient extends Fake implements HttpClient {
final _FakeHttpClientRequest request = _FakeHttpClientRequest();
Object? thrownError;
@override
Future<HttpClientRequest> getUrl(Uri url) async {
if (thrownError != null) {
throw thrownError!;
}
return request;
}
}
class _FakeHttpClientRequest extends Fake implements HttpClientRequest {
final _FakeHttpClientResponse response = _FakeHttpClientResponse();
@override
Future<HttpClientResponse> close() async {
return response;
}
}
class _FakeHttpClientResponse extends Fake implements HttpClientResponse {
bool drained = false;
@override
int statusCode = HttpStatus.ok;
@override
int contentLength = 0;
@override
HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;
late List<List<int>> content;
@override
StreamSubscription<List<int>> listen(void Function(List<int> event)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
return Stream<List<int>>.fromIterable(content).listen(
onData,
onDone: onDone,
onError: onError,
cancelOnError: cancelOnError,
);
}
@override
Future<E> drain<E>([E? futureValue]) async {
drained = true;
return futureValue ?? futureValue as E; // Mirrors the implementation in Stream.
}
}
class FakeCodec implements Codec {
@override
void dispose() {}
@override
int get frameCount => throw UnimplementedError();
@override
Future<FrameInfo> getNextFrame() {
throw UnimplementedError();
}
@override
int get repetitionCount => throw UnimplementedError();
}
Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[Paste your output here]