Fix LRUness of ScrollAwareImageProvider (#50242)
diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart
index 8c0ac71..04ec59f 100644
--- a/packages/flutter/lib/src/painting/image_provider.dart
+++ b/packages/flutter/lib/src/painting/image_provider.dart
@@ -411,6 +411,18 @@
/// [ImageCache].
@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
+ // This is an unusual edge case where someone has told us that they found
+ // the image we want before getting to this method. We should avoid calling
+ // load again, but still update the image cache with LRU information.
+ if (stream.completer != null) {
+ final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
+ key,
+ () => stream.completer,
+ onError: handleError,
+ );
+ assert(identical(completer, stream.completer));
+ return;
+ }
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
diff --git a/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart b/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart
index 90f301a..851aa1e 100644
--- a/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart
+++ b/packages/flutter/lib/src/widgets/scroll_aware_image_provider.dart
@@ -74,12 +74,16 @@
T key,
ImageErrorListener handleError,
) {
- // Something managed to complete the stream. Nothing left to do.
- if (stream.completer != null) {
- return;
- }
- // Something else got this image into the cache. Return it.
- if (PaintingBinding.instance.imageCache.containsKey(key)) {
+ // Something managed to complete the stream, or it's already in the image
+ // cache. Notify the wrapped provider and expect it to behave by not
+ // reloading the image since it's already resolved.
+ // Do this even if the context has gone out of the tree, since it will
+ // update LRU information about the cache. Even though we never showed the
+ // image, it was still touched more recently.
+ // Do this before checking scrolling, so that if the bytes are available we
+ // render them even though we're scrolling fast - there's no additional
+ // allocations to do for texture memory, it's already there.
+ if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) {
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
return;
}
diff --git a/packages/flutter/test/painting/image_test_utils.dart b/packages/flutter/test/painting/image_test_utils.dart
index c690f76..4fbd7a2 100644
--- a/packages/flutter/test/painting/image_test_utils.dart
+++ b/packages/flutter/test/painting/image_test_utils.dart
@@ -18,6 +18,7 @@
final Completer<ImageInfo> _completer = Completer<ImageInfo>.sync();
ImageConfiguration configuration;
+ int loadCallCount = 0;
@override
Future<TestImageProvider> obtainKey(ImageConfiguration configuration) {
@@ -31,8 +32,10 @@
}
@override
- ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) =>
- OneFrameImageStreamCompleter(_completer.future);
+ ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
+ loadCallCount += 1;
+ return OneFrameImageStreamCompleter(_completer.future);
+ }
ImageInfo complete() {
final ImageInfo imageInfo = ImageInfo(image: testImage);
diff --git a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart
index e86179a..1f2b56c 100644
--- a/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart
+++ b/packages/flutter/test/widgets/scroll_aware_image_provider_test.dart
@@ -327,8 +327,74 @@
expect(imageCache.containsKey(testImageProvider), true);
expect(imageCache.currentSize, 1);
+ expect(testImageProvider.loadCallCount, 1);
expect(stream.completer, null);
});
+
+ testWidgets('ScrollAwareImageProvider does not block LRU updates to image cache', (WidgetTester tester) async {
+ final int oldSize = imageCache.maximumSize;
+ imageCache.maximumSize = 1;
+
+ final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
+ final ScrollController scrollController = ScrollController();
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: SingleChildScrollView(
+ physics: ControllablePhysics(),
+ controller: scrollController,
+ child: TestWidget(key),
+ ),
+ ));
+
+ final DisposableBuildContext context = DisposableBuildContext(key.currentState);
+ const TestImage testImage = TestImage(width: 10, height: 10);
+ final TestImageProvider testImageProvider = TestImageProvider(testImage);
+ final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
+ context: context,
+ imageProvider: testImageProvider,
+ );
+
+ expect(testImageProvider.configuration, null);
+ expect(imageCache.containsKey(testImageProvider), false);
+
+ final ControllablePhysics physics = _findPhysics<ControllablePhysics>(tester);
+ physics.recommendDeferredLoadingValue = true;
+
+ final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
+
+ expect(testImageProvider.configuration, null);
+ expect(stream.completer, null);
+ expect(imageCache.currentSize, 0);
+
+ // Occupy the only slot in the cache with another image.
+ final TestImageProvider testImageProvider2 = TestImageProvider(const TestImage());
+ testImageProvider2.complete();
+ await precacheImage(testImageProvider2, context.context);
+ expect(imageCache.containsKey(testImageProvider), false);
+ expect(imageCache.containsKey(testImageProvider2), true);
+ expect(imageCache.currentSize, 1);
+
+ // Complete the original image while we're still scrolling fast.
+ testImageProvider.complete();
+ stream.setCompleter(testImageProvider.load(testImageProvider, PaintingBinding.instance.instantiateImageCodec));
+
+ // Verify that this hasn't chagned the cache state yet
+ expect(imageCache.containsKey(testImageProvider), false);
+ expect(imageCache.containsKey(testImageProvider2), true);
+ expect(imageCache.currentSize, 1);
+ expect(testImageProvider.loadCallCount, 1);
+
+ await tester.pump();
+
+ // After pumping a frame, the original image should be in the cache because
+ // it took the LRU slot.
+ expect(imageCache.containsKey(testImageProvider), true);
+ expect(imageCache.containsKey(testImageProvider2), false);
+ expect(imageCache.currentSize, 1);
+ expect(testImageProvider.loadCallCount, 1);
+
+ imageCache.maximumSize = oldSize;
+ });
}
class TestWidget extends StatefulWidget {