diff --git a/packages/flutter/lib/src/painting/_network_image_io.dart b/packages/flutter/lib/src/painting/_network_image_io.dart index 8eed2296c5..12c1aa698e 100644 --- a/packages/flutter/lib/src/painting/_network_image_io.dart +++ b/packages/flutter/lib/src/painting/_network_image_io.dart @@ -9,6 +9,7 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; +import 'binding.dart'; import 'debug.dart'; import 'image_provider.dart' as image_provider; import 'image_stream.dart'; @@ -86,8 +87,13 @@ class NetworkImage extends image_provider.ImageProvider _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async { - final ByteData data = await key.bundle.load(key.name); - if (data == null) - throw 'Unable to read data'; + ByteData data; + // Hot reload/restart could change whether an asset bundle or key in a + // bundle are available, or if it is a network backed bundle. + try { + data = await key.bundle.load(key.name); + } on FlutterError { + PaintingBinding.instance.imageCache.evict(key); + rethrow; + } + if (data == null) { + PaintingBinding.instance.imageCache.evict(key); + throw StateError('Unable to read data'); + } return await decode(data.buffer.asUint8List()); } } @@ -827,8 +837,12 @@ class FileImage extends ImageProvider { assert(key == this); final Uint8List bytes = await file.readAsBytes(); - if (bytes.lengthInBytes == 0) + + if (bytes.lengthInBytes == 0) { + // The file may become available later. + PaintingBinding.instance.imageCache.evict(key); throw StateError('$file is empty and cannot be loaded as an image.'); + } return await decode(bytes); } diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index 510131c5d4..47371be5e8 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart @@ -10,6 +10,7 @@ import 'dart:typed_data'; import 'package:file/memory.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -34,6 +35,8 @@ void main() { tearDown(() { FlutterError.onError = oldError; + PaintingBinding.instance.imageCache.clear(); + PaintingBinding.instance.imageCache.clearLiveImages(); }); group('ImageProvider', () { @@ -42,6 +45,50 @@ void main() { imageCache.clear(); }); + test('AssetImageProvider - evicts on failure to load', () async { + final Completer error = Completer(); + FlutterError.onError = (FlutterErrorDetails details) { + error.complete(details.exception as FlutterError); + }; + + const ImageProvider provider = ExactAssetImage('does-not-exist'); + final Object key = await provider.obtainKey(ImageConfiguration.empty); + expect(imageCache.statusForKey(provider).untracked, true); + expect(imageCache.pendingImageCount, 0); + + provider.resolve(ImageConfiguration.empty); + + expect(imageCache.statusForKey(key).pending, true); + expect(imageCache.pendingImageCount, 1); + + await error.future; + + expect(imageCache.statusForKey(provider).untracked, true); + expect(imageCache.pendingImageCount, 0); + }, skip: isBrowser); + + test('AssetImageProvider - evicts on null load', () async { + final Completer error = Completer(); + FlutterError.onError = (FlutterErrorDetails details) { + error.complete(details.exception as StateError); + }; + + final ImageProvider provider = ExactAssetImage('does-not-exist', bundle: TestAssetBundle()); + final Object key = await provider.obtainKey(ImageConfiguration.empty); + expect(imageCache.statusForKey(provider).untracked, true); + expect(imageCache.pendingImageCount, 0); + + provider.resolve(ImageConfiguration.empty); + + expect(imageCache.statusForKey(key).pending, true); + expect(imageCache.pendingImageCount, 1); + + await error.future; + + expect(imageCache.statusForKey(provider).untracked, true); + expect(imageCache.pendingImageCount, 0); + }, skip: isBrowser); + test('ImageProvider can evict images', () async { final Uint8List bytes = Uint8List.fromList(kTransparentImage); final MemoryImage imageProvider = MemoryImage(bytes); @@ -168,7 +215,7 @@ void main() { expect(uncaught, false); }); - test('File image with empty file throws expected error - (image cache)', () async { + test('File image with empty file throws expected error and evicts from cache', () async { final Completer error = Completer(); FlutterError.onError = (FlutterErrorDetails details) { error.complete(details.exception as StateError); @@ -177,9 +224,17 @@ void main() { final File file = fs.file('/empty.png')..createSync(recursive: true); final FileImage provider = FileImage(file); + expect(imageCache.statusForKey(provider).untracked, true); + expect(imageCache.pendingImageCount, 0); + provider.resolve(ImageConfiguration.empty); + expect(imageCache.statusForKey(provider).pending, true); + expect(imageCache.pendingImageCount, 1); + expect(await error.future, isStateError); + expect(imageCache.statusForKey(provider).untracked, true); + expect(imageCache.pendingImageCount, 0); }); group('NetworkImage', () { @@ -194,7 +249,7 @@ void main() { debugNetworkImageHttpClientProvider = null; }); - test('Expect thrown exception with statusCode', () async { + test('Expect thrown exception with statusCode - evicts from cache', () async { final int errorStatusCode = HttpStatus.notFound; const String requestUrl = 'foo-url'; @@ -207,13 +262,24 @@ void main() { final Completer caughtError = Completer(); final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl)); + 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() @@ -465,3 +531,10 @@ class AsyncKeyMemoryImage extends MemoryImage { class MockHttpClient extends Mock implements HttpClient {} class MockHttpClientRequest extends Mock implements HttpClientRequest {} class MockHttpClientResponse extends Mock implements HttpClientResponse {} + +class TestAssetBundle extends CachingAssetBundle { + @override + Future load(String key) async { + return null; + } +}