Expose API for custom image decode and cache sizes (#41415)


diff --git a/packages/flutter/lib/src/painting/_network_image_io.dart b/packages/flutter/lib/src/painting/_network_image_io.dart
index 2b2c4a7..d976b93 100644
--- a/packages/flutter/lib/src/painting/_network_image_io.dart
+++ b/packages/flutter/lib/src/painting/_network_image_io.dart
@@ -9,7 +9,6 @@
 
 import 'package:flutter/foundation.dart';
 
-import 'binding.dart';
 import 'debug.dart';
 import 'image_provider.dart' as image_provider;
 import 'image_stream.dart';
@@ -38,14 +37,14 @@
   }
 
   @override
-  ImageStreamCompleter load(image_provider.NetworkImage key) {
+  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
     // Ownership of this controller is handed off to [_loadAsync]; it is that
     // method's responsibility to close the controller's stream when the image
     // has been loaded or an error is thrown.
     final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
 
     return MultiFrameImageStreamCompleter(
-      codec: _loadAsync(key, chunkEvents),
+      codec: _loadAsync(key, chunkEvents, decode),
       chunkEvents: chunkEvents.stream,
       scale: key.scale,
       informationCollector: () {
@@ -76,6 +75,7 @@
   Future<ui.Codec> _loadAsync(
     NetworkImage key,
     StreamController<ImageChunkEvent> chunkEvents,
+    image_provider.DecoderCallback decode,
   ) async {
     try {
       assert(key == this);
@@ -101,7 +101,7 @@
       if (bytes.lengthInBytes == 0)
         throw Exception('NetworkImage is an empty file: $resolved');
 
-      return PaintingBinding.instance.instantiateImageCodec(bytes);
+      return decode(bytes);
     } finally {
       chunkEvents.close();
     }
diff --git a/packages/flutter/lib/src/painting/_network_image_web.dart b/packages/flutter/lib/src/painting/_network_image_web.dart
index 6b566de..ba28c8a 100644
--- a/packages/flutter/lib/src/painting/_network_image_web.dart
+++ b/packages/flutter/lib/src/painting/_network_image_web.dart
@@ -11,6 +11,8 @@
 import 'image_stream.dart';
 
 /// The dart:html implemenation of [image_provider.NetworkImage].
+///
+/// NetworkImage on the web does not support decoding to a specified size.
 class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
   /// Creates an object that fetches the image at the given URL.
   ///
@@ -34,9 +36,9 @@
   }
 
   @override
-  ImageStreamCompleter load(image_provider.NetworkImage key) {
+  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
     return MultiFrameImageStreamCompleter(
-      codec: _loadAsync(key),
+      codec: _loadAsync(key, decode),
       scale: key.scale,
       informationCollector: () {
         return <DiagnosticsNode>[
@@ -47,7 +49,10 @@
     );
   }
 
-  Future<ui.Codec> _loadAsync(NetworkImage key) async {
+  // Web does not support decoding network images to a specified size. The decode parameter
+  // here is ignored and the web-only `ui.webOnlyInstantiateImageCodecFromUrl` will be used
+  // directly in place of the typical `instantiateImageCodec` method.
+  Future<ui.Codec> _loadAsync(NetworkImage key, image_provider.DecoderCallback decode) async {
     assert(key == this);
 
     final Uri resolved = Uri.base.resolve(key.url);
diff --git a/packages/flutter/lib/src/painting/binding.dart b/packages/flutter/lib/src/painting/binding.dart
index 4eb915d..c022373 100644
--- a/packages/flutter/lib/src/painting/binding.dart
+++ b/packages/flutter/lib/src/painting/binding.dart
@@ -70,8 +70,26 @@
   ImageCache createImageCache() => ImageCache();
 
   /// Calls through to [dart:ui] with [decodedCacheRatioCap] from [ImageCache].
-  Future<ui.Codec> instantiateImageCodec(Uint8List list) {
-    return ui.instantiateImageCodec(list);
+  ///
+  /// The [cacheWidth] and [cacheHeight] parameters, when specified, indicate the
+  /// size to decode the image to.
+  ///
+  /// Both [cacheWidth] and [cacheHeight] must be positive values greater than or
+  /// equal to 1 or null. It is valid to specify only one of [cacheWidth] and
+  /// [cacheHeight] with the other remaining null, in which case the omitted
+  /// dimension will decode to its original size. When both are null or omitted,
+  /// the image will be decoded at its native resolution.
+  Future<ui.Codec> instantiateImageCodec(Uint8List bytes, {
+    int cacheWidth,
+    int cacheHeight,
+  }) {
+    assert(cacheWidth == null || cacheWidth > 0);
+    assert(cacheHeight == null || cacheHeight > 0);
+    return ui.instantiateImageCodec(
+      bytes,
+      targetWidth: cacheWidth,
+      targetHeight: cacheHeight,
+    );
   }
 
   @override
diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart
index a08e4c9..69790b2 100644
--- a/packages/flutter/lib/src/painting/image_provider.dart
+++ b/packages/flutter/lib/src/painting/image_provider.dart
@@ -152,6 +152,16 @@
   }
 }
 
+/// Performs the decode process for use in [ImageProvider.load].
+///
+/// This callback allows decoupling of the `cacheWidth` and `cacheHeight`
+/// parameters from implementations of [ImageProvider] that do not use them.
+///
+/// See also:
+///
+///  * [ResizeImage], which uses this to override the `cacheWidth` and `cacheHeight` parameters.
+typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int cacheWidth, int cacheHeight});
+
 /// Identifies an image without committing to the precise final asset. This
 /// allows a set of images to be identified and for the precise image to later
 /// be resolved based on the environment, e.g. the device pixel ratio.
@@ -312,8 +322,11 @@
       }
       key.then<void>((T key) {
         obtainedKey = key;
-        final ImageStreamCompleter completer = PaintingBinding.instance
-            .imageCache.putIfAbsent(key, () => load(key), onError: handleError);
+        final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
+          key,
+          () => load(key, PaintingBinding.instance.instantiateImageCodec),
+          onError: handleError,
+        );
         if (completer != null) {
           stream.setCompleter(completer);
         }
@@ -379,8 +392,15 @@
 
   /// Converts a key into an [ImageStreamCompleter], and begins fetching the
   /// image.
+  ///
+  /// The [decode] callback provides the logic to obtain the codec for the
+  /// image.
+  ///
+  /// See also:
+  ///
+  ///   * [ResizeImage], for modifying the key to account for cache dimensions.
   @protected
-  ImageStreamCompleter load(T key);
+  ImageStreamCompleter load(T key, DecoderCallback decode);
 
   @override
   String toString() => '$runtimeType()';
@@ -444,9 +464,9 @@
   /// Converts a key into an [ImageStreamCompleter], and begins fetching the
   /// image using [loadAsync].
   @override
-  ImageStreamCompleter load(AssetBundleImageKey key) {
+  ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
     return MultiFrameImageStreamCompleter(
-      codec: _loadAsync(key),
+      codec: _loadAsync(key, decode),
       scale: key.scale,
       informationCollector: () sync* {
         yield DiagnosticsProperty<ImageProvider>('Image provider', this);
@@ -460,11 +480,82 @@
   ///
   /// This function is used by [load].
   @protected
-  Future<ui.Codec> _loadAsync(AssetBundleImageKey key) async {
+  Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async {
     final ByteData data = await key.bundle.load(key.name);
     if (data == null)
       throw 'Unable to read data';
-    return await PaintingBinding.instance.instantiateImageCodec(data.buffer.asUint8List());
+    return await decode(data.buffer.asUint8List());
+  }
+}
+
+class _SizeAwareCacheKey {
+  const _SizeAwareCacheKey(this.providerCacheKey, this.width, this.height);
+
+  final Object providerCacheKey;
+
+  final int width;
+
+  final int height;
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType)
+      return false;
+    final _SizeAwareCacheKey typedOther = other;
+    return providerCacheKey == typedOther.providerCacheKey
+        && width == typedOther.width
+        && height == typedOther.height;
+  }
+
+  @override
+  int get hashCode => hashValues(providerCacheKey, width, height);
+}
+
+/// Instructs Flutter to decode the image at the specified dimensions
+/// instead of at its native size.
+///
+/// This allows finer control of the size of the image in [ImageCache] and is
+/// generally used to reduce the memory footprint of [ImageCache].
+///
+/// The decoded image may still be displayed at sizes other than the
+/// cached size provided here.
+class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
+  /// Creates an ImageProvider that decodes the image to the specified size.
+  ///
+  /// The cached image will be directly decoded and stored at the resolution
+  /// defined by `width` and `height`. The image will lose detail and
+  /// use less memory if resized to a size smaller than the native size.
+  const ResizeImage(
+    this.imageProvider, {
+    this.width,
+    this.height,
+  }) : assert(width != null || height != null);
+
+  /// The [ImageProvider] that this class wraps.
+  final ImageProvider imageProvider;
+
+  /// The width the image should decode to and cache.
+  final int width;
+
+  /// The height the image should decode to and cache.
+  final int height;
+
+  @override
+  ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) {
+    final DecoderCallback decodeResize = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
+      assert(
+        cacheWidth == null && cacheHeight == null,
+        'ResizeImage cannot be composed with another ImageProvider that applies cacheWidth or cacheHeight.'
+      );
+      return decode(bytes, cacheWidth: width, cacheHeight: height);
+    };
+    return imageProvider.load(key.providerCacheKey, decodeResize);
+  }
+
+  @override
+  Future<_SizeAwareCacheKey> obtainKey(ImageConfiguration configuration) async {
+    final Object providerCacheKey = await imageProvider.obtainKey(configuration);
+    return _SizeAwareCacheKey(providerCacheKey, width, height);
   }
 }
 
@@ -472,6 +563,11 @@
 ///
 /// The image will be cached regardless of cache headers from the server.
 ///
+/// When a network image is used on the Web platform, the [cacheWidth] and
+/// [cacheHeight] parameters of the [DecoderCallback] are ignored as the Web
+/// engine delegates image decoding of network images to the Web, which does
+/// not support custom decode sizes.
+///
 /// See also:
 ///
 ///  * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
@@ -496,7 +592,7 @@
   Map<String, String> get headers;
 
   @override
-  ImageStreamCompleter load(NetworkImage key);
+  ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
 }
 
 /// Decodes the given [File] object as an image, associating it with the given
@@ -525,9 +621,9 @@
   }
 
   @override
-  ImageStreamCompleter load(FileImage key) {
+  ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
     return MultiFrameImageStreamCompleter(
-      codec: _loadAsync(key),
+      codec: _loadAsync(key, decode),
       scale: key.scale,
       informationCollector: () sync* {
         yield ErrorDescription('Path: ${file?.path}');
@@ -535,14 +631,14 @@
     );
   }
 
-  Future<ui.Codec> _loadAsync(FileImage key) async {
+  Future<ui.Codec> _loadAsync(FileImage key, DecoderCallback decode) async {
     assert(key == this);
 
     final Uint8List bytes = await file.readAsBytes();
     if (bytes.lengthInBytes == 0)
       return null;
 
-    return await PaintingBinding.instance.instantiateImageCodec(bytes);
+    return await decode(bytes);
   }
 
   @override
@@ -593,17 +689,17 @@
   }
 
   @override
-  ImageStreamCompleter load(MemoryImage key) {
+  ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
     return MultiFrameImageStreamCompleter(
-      codec: _loadAsync(key),
+      codec: _loadAsync(key, decode),
       scale: key.scale,
     );
   }
 
-  Future<ui.Codec> _loadAsync(MemoryImage key) {
+  Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
     assert(key == this);
 
-    return PaintingBinding.instance.instantiateImageCodec(bytes);
+    return decode(bytes);
   }
 
   @override
diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart
index 9dfa4a5..83f8a9e 100644
--- a/packages/flutter/lib/src/widgets/image.dart
+++ b/packages/flutter/lib/src/widgets/image.dart
@@ -55,6 +55,13 @@
   );
 }
 
+ImageProvider<dynamic> _resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider<dynamic> provider) {
+  if (cacheWidth != null || cacheHeight != null) {
+    return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
+  }
+  return provider;
+}
+
 /// Prefetches an image into the image cache.
 ///
 /// Returns a [Future] that will complete when the first image yielded by the
@@ -232,6 +239,17 @@
 /// ```
 /// {@end-tool}
 ///
+/// The [Image.asset], [Image.network], [Image.file], and [Image.memory]
+/// constructors allow a custom decode size to be specified through
+/// [cacheWidth] and [cacheHeight] parameters. The engine will decode the
+/// image to the specified size, which is primarily intended to reduce the
+/// memory usage of [ImageCache].
+///
+/// In the case where a network image is used on the Web platform, the
+/// [cacheWidth] and [cacheHeight] parameters are ignored as the Web engine
+/// delegates image decoding of network images to the Web, which does not support
+/// custom decode sizes.
+///
 /// See also:
 ///
 ///  * [Icon], which shows an image from a font.
@@ -305,6 +323,16 @@
   /// [FilterQuality.none] which corresponds to nearest-neighbor.
   ///
   /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
+  ///
+  /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
+  /// engine that the image should be decoded at the specified size. The image
+  /// will be rendered to the constraints of the layout or [width] and [height]
+  /// regardless of these parameters. These parameters are primarily intended
+  /// to reduce the memory usage of [ImageCache].
+  ///
+  /// In the case where the network image is on the Web platform, the [cacheWidth]
+  /// and [cacheHeight] parameters are ignored as the web engine delegates
+  /// image decoding to the web which does not support custom decode sizes.
   Image.network(
     String src, {
     Key key,
@@ -325,10 +353,14 @@
     this.gaplessPlayback = false,
     this.filterQuality = FilterQuality.low,
     Map<String, String> headers,
-  }) : image = NetworkImage(src, scale: scale, headers: headers),
+    int cacheWidth,
+    int cacheHeight,
+  }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
        assert(alignment != null),
        assert(repeat != null),
        assert(matchTextDirection != null),
+       assert(cacheWidth == null || cacheWidth > 0),
+       assert(cacheHeight == null || cacheHeight > 0),
        super(key: key);
 
   /// Creates a widget that displays an [ImageStream] obtained from a [File].
@@ -349,6 +381,12 @@
   /// [FilterQuality.none] which corresponds to nearest-neighbor.
   ///
   /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
+  ///
+  /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
+  /// engine that the image must be decoded at the specified size. The image
+  /// will be rendered to the constraints of the layout or [width] and [height]
+  /// regardless of these parameters. These parameters are primarily intended
+  /// to reduce the memory usage of [ImageCache].
   Image.file(
     File file, {
     Key key,
@@ -367,12 +405,16 @@
     this.matchTextDirection = false,
     this.gaplessPlayback = false,
     this.filterQuality = FilterQuality.low,
-  }) : image = FileImage(file, scale: scale),
+    int cacheWidth,
+    int cacheHeight,
+  }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, FileImage(file, scale: scale)),
        loadingBuilder = null,
        assert(alignment != null),
        assert(repeat != null),
        assert(filterQuality != null),
        assert(matchTextDirection != null),
+       assert(cacheWidth == null || cacheWidth > 0),
+       assert(cacheHeight == null || cacheHeight > 0),
        super(key: key);
 
 
@@ -404,6 +446,12 @@
   ///
   /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
   ///
+  /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
+  /// engine that the image must be decoded at the specified size. The image
+  /// will be rendered to the constraints of the layout or [width] and [height]
+  /// regardless of these parameters. These parameters are primarily intended
+  /// to reduce the memory usage of [ImageCache].
+  ///
   /// The [name] and [repeat] arguments must not be null.
   ///
   /// Either the [width] and [height] arguments should be specified, or the
@@ -520,13 +568,18 @@
     this.gaplessPlayback = false,
     String package,
     this.filterQuality = FilterQuality.low,
-  }) : image = scale != null
+    int cacheWidth,
+    int cacheHeight,
+  }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, scale != null
          ? ExactAssetImage(name, bundle: bundle, scale: scale, package: package)
-         : AssetImage(name, bundle: bundle, package: package),
+         : AssetImage(name, bundle: bundle, package: package)
+       ),
        loadingBuilder = null,
        assert(alignment != null),
        assert(repeat != null),
        assert(matchTextDirection != null),
+       assert(cacheWidth == null || cacheWidth > 0),
+       assert(cacheHeight == null || cacheHeight > 0),
        super(key: key);
 
   /// Creates a widget that displays an [ImageStream] obtained from a [Uint8List].
@@ -548,6 +601,12 @@
   /// [FilterQuality.none] which corresponds to nearest-neighbor.
   ///
   /// If [excludeFromSemantics] is true, then [semanticLabel] will be ignored.
+  ///
+  /// If [cacheWidth] or [cacheHeight] are provided, it indicates to the
+  /// engine that the image must be decoded at the specified size. The image
+  /// will be rendered to the constraints of the layout or [width] and [height]
+  /// regardless of these parameters. These parameters are primarily intended
+  /// to reduce the memory usage of [ImageCache].
   Image.memory(
     Uint8List bytes, {
     Key key,
@@ -566,11 +625,15 @@
     this.matchTextDirection = false,
     this.gaplessPlayback = false,
     this.filterQuality = FilterQuality.low,
-  }) : image = MemoryImage(bytes, scale: scale),
+    int cacheWidth,
+    int cacheHeight,
+  }) : image = _resizeIfNeeded(cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale)),
        loadingBuilder = null,
        assert(alignment != null),
        assert(repeat != null),
        assert(matchTextDirection != null),
+       assert(cacheWidth == null || cacheWidth > 0),
+       assert(cacheHeight == null || cacheHeight > 0),
        super(key: key);
 
   /// The image to display.
diff --git a/packages/flutter/test/painting/binding_test.dart b/packages/flutter/test/painting/binding_test.dart
index ea46e60..d849abd 100644
--- a/packages/flutter/test/painting/binding_test.dart
+++ b/packages/flutter/test/painting/binding_test.dart
@@ -19,7 +19,9 @@
 
     final Uint8List bytes = Uint8List.fromList(kTransparentImage);
     final MemoryImage memoryImage = MemoryImage(bytes);
-    memoryImage.load(memoryImage);
+    memoryImage.load(memoryImage, (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
+      return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
+    });
     expect(binding.instantiateImageCodecCalledCount, 1);
   });
 }
diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart
index c5ae1e5..872a8ca 100644
--- a/packages/flutter/test/painting/decoration_test.dart
+++ b/packages/flutter/test/painting/decoration_test.dart
@@ -33,7 +33,7 @@
   }
 
   @override
-  ImageStreamCompleter load(int key) {
+  ImageStreamCompleter load(int key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(
       SynchronousFuture<ImageInfo>(TestImageInfo(key, image: TestImage(), scale: 1.0))
     );
@@ -47,7 +47,7 @@
   }
 
   @override
-  ImageStreamCompleter load(int key) {
+  ImageStreamCompleter load(int key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(
       Future<ImageInfo>.value(TestImageInfo(key))
     );
@@ -63,7 +63,7 @@
   }
 
   @override
-  ImageStreamCompleter load(DelayedImageProvider key) {
+  ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(_completer.future);
   }
 
diff --git a/packages/flutter/test/painting/fake_image_provider.dart b/packages/flutter/test/painting/fake_image_provider.dart
index 58ebb1a..a8937e2 100644
--- a/packages/flutter/test/painting/fake_image_provider.dart
+++ b/packages/flutter/test/painting/fake_image_provider.dart
@@ -26,7 +26,7 @@
   }
 
   @override
-  ImageStreamCompleter load(FakeImageProvider key) {
+  ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) {
     assert(key == this);
     return MultiFrameImageStreamCompleter(
       codec: SynchronousFuture<ui.Codec>(_codec),
diff --git a/packages/flutter/test/painting/image_cache_test.dart b/packages/flutter/test/painting/image_cache_test.dart
index 13b14bf..da8758e 100644
--- a/packages/flutter/test/painting/image_cache_test.dart
+++ b/packages/flutter/test/painting/image_cache_test.dart
@@ -133,9 +133,9 @@
 
     test('Returns null if an error is caught resolving an image', () {
       final ErrorImageProvider errorImage = ErrorImageProvider();
-      expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage)), throwsA(isInstanceOf<Error>()));
+      expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isInstanceOf<Error>()));
       bool caughtError = false;
-      final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage), onError: (dynamic error, StackTrace stackTrace) {
+      final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null), onError: (dynamic error, StackTrace stackTrace) {
         caughtError = true;
       });
       expect(result, null);
diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart
index 3571dcf..de638f4 100644
--- a/packages/flutter/test/painting/image_provider_test.dart
+++ b/packages/flutter/test/painting/image_provider_test.dart
@@ -18,6 +18,11 @@
 import 'mocks_for_image_cache.dart';
 
 void main() {
+
+  final DecoderCallback basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
+    return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
+  };
+
   group(ImageProvider, () {
     setUpAll(() {
       TestRenderingFlutterBinding(); // initializes the imageCache
@@ -46,7 +51,7 @@
         final Uint8List bytes = Uint8List.fromList(kTransparentImage);
         final MemoryImage imageProvider = MemoryImage(bytes);
         final ImageStreamCompleter cacheStream = otherCache.putIfAbsent(
-          imageProvider, () => imageProvider.load(imageProvider),
+          imageProvider, () => imageProvider.load(imageProvider, basicDecoder),
         );
         final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
         final Completer<void> completer = Completer<void>();
@@ -195,7 +200,7 @@
 
         Future<void> loadNetworkImage() async {
           final NetworkImage networkImage = NetworkImage(nonconst('foo'));
-          final ImageStreamCompleter completer = networkImage.load(networkImage);
+          final ImageStreamCompleter completer = networkImage.load(networkImage, basicDecoder);
           completer.addListener(ImageStreamListener(
             (ImageInfo image, bool synchronousCall) { },
             onError: (dynamic error, StackTrace stackTrace) {
@@ -293,6 +298,83 @@
       }, skip: isBrowser);
     });
   });
+
+  test('ResizeImage resizes to the correct dimensions', () async {
+    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
+    final MemoryImage imageProvider = MemoryImage(bytes);
+    final Size rawImageSize = await _resolveAndGetSize(imageProvider);
+    expect(rawImageSize, const Size(1, 1));
+
+    const Size resizeDims = Size(14, 7);
+    final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round());
+    const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
+    final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
+    expect(resizedImageSize, resizeDims);
+  });
+
+  test('ResizeImage does not resize when no size is passed', () async {
+    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
+    final MemoryImage imageProvider = MemoryImage(bytes);
+    final Size rawImageSize = await _resolveAndGetSize(imageProvider);
+    expect(rawImageSize, const Size(1, 1));
+
+    // Cannot pass in two null arguments for cache dimensions, so will use the regular
+    // MemoryImage
+    final MemoryImage resizedImage = MemoryImage(bytes);
+    final Size resizedImageSize = await _resolveAndGetSize(resizedImage);
+    expect(resizedImageSize, const Size(1, 1));
+  });
+
+  test('ResizeImage stores values', () async {
+    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
+    final MemoryImage memoryImage = MemoryImage(bytes);
+    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: 20);
+    expect(resizeImage.width, 10);
+    expect(resizeImage.height, 20);
+    expect(resizeImage.imageProvider, memoryImage);
+
+    expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
+  });
+
+  test('ResizeImage takes one dim', () async {
+    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
+    final MemoryImage memoryImage = MemoryImage(bytes);
+    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: null);
+    expect(resizeImage.width, 10);
+    expect(resizeImage.height, null);
+    expect(resizeImage.imageProvider, memoryImage);
+
+    expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
+  });
+
+  test('ResizeImage forms closure', () async {
+    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
+    final MemoryImage memoryImage = MemoryImage(bytes);
+    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
+
+    final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
+      expect(cacheWidth, 123);
+      expect(cacheHeight, 321);
+      return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
+    };
+
+    resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode);
+  });
+}
+
+Future<Size> _resolveAndGetSize(ImageProvider imageProvider,
+    {ImageConfiguration configuration = ImageConfiguration.empty}) async {
+  final ImageStream stream = imageProvider.resolve(configuration);
+  final Completer<Size> completer = Completer<Size>();
+  final ImageStreamListener listener =
+    ImageStreamListener((ImageInfo image, bool synchronousCall) {
+      final int height = image.image.height;
+      final int width = image.image.width;
+      completer.complete(Size(width.toDouble(), height.toDouble()));
+    }
+  );
+  stream.addListener(listener);
+  return await completer.future;
 }
 
 class MockHttpClient extends Mock implements HttpClient {}
diff --git a/packages/flutter/test/painting/image_test_utils.dart b/packages/flutter/test/painting/image_test_utils.dart
index bce8798..923fd14 100644
--- a/packages/flutter/test/painting/image_test_utils.dart
+++ b/packages/flutter/test/painting/image_test_utils.dart
@@ -31,7 +31,7 @@
   }
 
   @override
-  ImageStreamCompleter load(TestImageProvider key) =>
+  ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) =>
       OneFrameImageStreamCompleter(_completer.future);
 
   ImageInfo complete() {
diff --git a/packages/flutter/test/painting/mocks_for_image_cache.dart b/packages/flutter/test/painting/mocks_for_image_cache.dart
index 656967e..dfc8901 100644
--- a/packages/flutter/test/painting/mocks_for_image_cache.dart
+++ b/packages/flutter/test/painting/mocks_for_image_cache.dart
@@ -37,7 +37,7 @@
   }
 
   @override
-  ImageStreamCompleter load(int key) {
+  ImageStreamCompleter load(int key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(
       SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image))
     );
@@ -51,7 +51,7 @@
   const FailingTestImageProvider(int key, int imageValue, { ui.Image image }) : super(key, imageValue, image: image);
 
   @override
-  ImageStreamCompleter load(int key) {
+  ImageStreamCompleter load(int key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(Future<ImageInfo>.sync(() => Future<ImageInfo>.error('loading failed!')));
   }
 }
@@ -85,7 +85,7 @@
 
 class ErrorImageProvider extends ImageProvider<ErrorImageProvider> {
   @override
-  ImageStreamCompleter load(ErrorImageProvider key) {
+  ImageStreamCompleter load(ErrorImageProvider key, DecoderCallback decode) {
     throw Error();
   }
 
@@ -97,7 +97,7 @@
 
 class ObtainKeyErrorImageProvider extends ImageProvider<ObtainKeyErrorImageProvider> {
   @override
-  ImageStreamCompleter load(ObtainKeyErrorImageProvider key) {
+  ImageStreamCompleter load(ObtainKeyErrorImageProvider key, DecoderCallback decode) {
     throw Error();
   }
 
@@ -109,7 +109,7 @@
 
 class LoadErrorImageProvider extends ImageProvider<LoadErrorImageProvider> {
   @override
-  ImageStreamCompleter load(LoadErrorImageProvider key) {
+  ImageStreamCompleter load(LoadErrorImageProvider key, DecoderCallback decode) {
     throw Error();
   }
 
@@ -121,7 +121,7 @@
 
 class LoadErrorCompleterImageProvider extends ImageProvider<LoadErrorCompleterImageProvider> {
   @override
-  ImageStreamCompleter load(LoadErrorCompleterImageProvider key) {
+  ImageStreamCompleter load(LoadErrorCompleterImageProvider key, DecoderCallback decode) {
     final Completer<void> completer = Completer<void>.sync();
     completer.completeError(Error());
     return OneFrameImageStreamCompleter(completer.future);
diff --git a/packages/flutter/test/painting/painting_utils.dart b/packages/flutter/test/painting/painting_utils.dart
index 378fb0d..ba6f953 100644
--- a/packages/flutter/test/painting/painting_utils.dart
+++ b/packages/flutter/test/painting/painting_utils.dart
@@ -14,7 +14,7 @@
   int get instantiateImageCodecCalledCount => counter;
 
   @override
-  Future<ui.Codec> instantiateImageCodec(Uint8List list) {
+  Future<ui.Codec> instantiateImageCodec(Uint8List list, {int cacheWidth, int cacheHeight}) {
     counter++;
     return ui.instantiateImageCodec(list);
   }
diff --git a/packages/flutter/test/painting/shape_decoration_test.dart b/packages/flutter/test/painting/shape_decoration_test.dart
index dbdcb62..c3f02d8 100644
--- a/packages/flutter/test/painting/shape_decoration_test.dart
+++ b/packages/flutter/test/painting/shape_decoration_test.dart
@@ -107,7 +107,7 @@
   }
 
   @override
-  ImageStreamCompleter load(TestImageProvider key) {
+  ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(
       SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage(), scale: 1.0)),
     );
diff --git a/packages/flutter/test/widgets/box_decoration_test.dart b/packages/flutter/test/widgets/box_decoration_test.dart
index 110fd3d..be7cd3e 100644
--- a/packages/flutter/test/widgets/box_decoration_test.dart
+++ b/packages/flutter/test/widgets/box_decoration_test.dart
@@ -28,7 +28,7 @@
   }
 
   @override
-  ImageStreamCompleter load(TestImageProvider key) {
+  ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(
       future.then<ImageInfo>((void value) => ImageInfo(image: image))
     );
diff --git a/packages/flutter/test/widgets/image_data.dart b/packages/flutter/test/widgets/image_data.dart
new file mode 100644
index 0000000..83a83af
--- /dev/null
+++ b/packages/flutter/test/widgets/image_data.dart
@@ -0,0 +1,25 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const List<int> kTransparentImage = <int>[
+  0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
+  0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
+  0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
+  0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
+  0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
+];
+
+/// An animated GIF image with 3 1x1 pixel frames (a red, green, and blue
+/// frames). The gif animates forever, and each frame has a 100ms delay.
+const List<int> kAnimatedGif = <int> [
+  0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xa1, 0x03, 0x00,
+  0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0x21,
+  0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30,
+  0x03, 0x01, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00,
+  0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c,
+  0x01, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00,
+  0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x54, 0x01, 0x00, 0x21,
+  0xf9, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01,
+  0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b,
+];
diff --git a/packages/flutter/test/widgets/image_resolution_test.dart b/packages/flutter/test/widgets/image_resolution_test.dart
index ebf2db3..4508e6d 100644
--- a/packages/flutter/test/widgets/image_resolution_test.dart
+++ b/packages/flutter/test/widgets/image_resolution_test.dart
@@ -13,6 +13,8 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 
+import 'image_data.dart';
+
 class TestImage implements ui.Image {
   TestImage(this.scale);
   final double scale;
@@ -104,7 +106,7 @@
   TestAssetImage(String name) : super(name);
 
   @override
-  ImageStreamCompleter load(AssetBundleImageKey key) {
+  ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
     ImageInfo imageInfo;
     key.bundle.load(key.name).then<void>((ByteData data) {
       final TestByteData testData = data;
@@ -150,6 +152,30 @@
   );
 }
 
+Widget buildImageCacheResized(String name, Key key, int width, int height, int cacheWidth, int cacheHeight) {
+  return Center(
+    child: RepaintBoundary(
+      child: Container(
+        width: 250,
+        height: 250,
+        child: Center(
+          child: Image.memory(
+            Uint8List.fromList(kTransparentImage),
+            key: key,
+            excludeFromSemantics: true,
+            color: const Color(0xFF00FFFF),
+            colorBlendMode: BlendMode.plus,
+            width: width.toDouble(),
+            height: height.toDouble(),
+            cacheWidth: cacheWidth,
+            cacheHeight: cacheHeight,
+          ),
+        ),
+      ),
+    ),
+  );
+}
+
 RenderImage getRenderImage(WidgetTester tester, Key key) {
   return tester.renderObject<RenderImage>(find.byKey(key));
 }
@@ -303,4 +329,22 @@
     expect(getTestImage(tester, key).scale, 10.0);
   });
 
+  testWidgets('Image cache resize upscale display 5', (WidgetTester tester) async {
+    final Key key = GlobalKey();
+    await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 20, 20));
+    expect(getRenderImage(tester, key).size, const Size(5.0, 5.0));
+  });
+
+  testWidgets('Image cache resize upscale display 50', (WidgetTester tester) async {
+    final Key key = GlobalKey();
+    await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 50, 50, 20, 20));
+    expect(getRenderImage(tester, key).size, const Size(50.0, 50.0));
+  });
+
+  testWidgets('Image cache resize downscale display 5', (WidgetTester tester) async {
+    final Key key = GlobalKey();
+    await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 1, 1));
+    expect(getRenderImage(tester, key).size, const Size(5.0, 5.0));
+  });
+
 }
diff --git a/packages/flutter/test/widgets/image_rtl_test.dart b/packages/flutter/test/widgets/image_rtl_test.dart
index c2e6daf..cd7222e 100644
--- a/packages/flutter/test/widgets/image_rtl_test.dart
+++ b/packages/flutter/test/widgets/image_rtl_test.dart
@@ -19,7 +19,7 @@
   }
 
   @override
-  ImageStreamCompleter load(TestImageProvider key) {
+  ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) {
     return OneFrameImageStreamCompleter(
       SynchronousFuture<ImageInfo>(ImageInfo(image: TestImage()))
     );
diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart
index da2a2f0..5a400fe 100644
--- a/packages/flutter/test/widgets/image_test.dart
+++ b/packages/flutter/test/widgets/image_test.dart
@@ -1199,7 +1199,7 @@
   }
 
   @override
-  ImageStreamCompleter load(TestImageProvider key) => _streamCompleter;
+  ImageStreamCompleter load(TestImageProvider key, DecoderCallback decode) => _streamCompleter;
 
   void complete() {
     _completer.complete(ImageInfo(image: TestImage()));