image_provider.dart 62.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:io';
7
import 'dart:math' as math;
8
import 'dart:ui' as ui;
9
import 'dart:ui' show Locale, Size, TextDirection;
10 11

import 'package:flutter/foundation.dart';
12
import 'package:flutter/services.dart';
13

14
import '_network_image_io.dart'
15
  if (dart.library.js_util) '_network_image_web.dart' as network_image;
16
import 'binding.dart';
17
import 'image_cache.dart';
18 19
import 'image_stream.dart';

Dan Field's avatar
Dan Field committed
20 21 22 23
/// Signature for the callback taken by [_createErrorHandlerAndKey].
typedef _KeyAndErrorHandlerCallback<T> = void Function(T key, ImageErrorListener handleError);

/// Signature used for error handling by [_createErrorHandlerAndKey].
24
typedef _AsyncKeyErrorHandler<T> = Future<void> Function(T key, Object exception, StackTrace? stack);
Dan Field's avatar
Dan Field committed
25

26 27
/// Configuration information passed to the [ImageProvider.resolve] method to
/// select a specific image.
28 29 30 31 32 33 34
///
/// See also:
///
///  * [createLocalImageConfiguration], which creates an [ImageConfiguration]
///    based on ambient configuration in a [Widget] environment.
///  * [ImageProvider], which uses [ImageConfiguration] objects to determine
///    which image to obtain.
35
@immutable
36 37 38 39 40 41 42 43 44
class ImageConfiguration {
  /// Creates an object holding the configuration information for an [ImageProvider].
  ///
  /// All the arguments are optional. Configuration information is merely
  /// advisory and best-effort.
  const ImageConfiguration({
    this.bundle,
    this.devicePixelRatio,
    this.locale,
Ian Hickson's avatar
Ian Hickson committed
45
    this.textDirection,
46
    this.size,
Ian Hickson's avatar
Ian Hickson committed
47
    this.platform,
48 49 50 51 52 53 54
  });

  /// Creates an object holding the configuration information for an [ImageProvider].
  ///
  /// All the arguments are optional. Configuration information is merely
  /// advisory and best-effort.
  ImageConfiguration copyWith({
55 56 57 58 59 60
    AssetBundle? bundle,
    double? devicePixelRatio,
    Locale? locale,
    TextDirection? textDirection,
    Size? size,
    TargetPlatform? platform,
61
  }) {
62
    return ImageConfiguration(
63 64 65
      bundle: bundle ?? this.bundle,
      devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
      locale: locale ?? this.locale,
Ian Hickson's avatar
Ian Hickson committed
66
      textDirection: textDirection ?? this.textDirection,
67
      size: size ?? this.size,
Ian Hickson's avatar
Ian Hickson committed
68
      platform: platform ?? this.platform,
69 70 71 72 73
    );
  }

  /// The preferred [AssetBundle] to use if the [ImageProvider] needs one and
  /// does not have one already selected.
74
  final AssetBundle? bundle;
75 76

  /// The device pixel ratio where the image will be shown.
77
  final double? devicePixelRatio;
78 79

  /// The language and region for which to select the image.
80
  final Locale? locale;
81

Ian Hickson's avatar
Ian Hickson committed
82
  /// The reading direction of the language for which to select the image.
83
  final TextDirection? textDirection;
Ian Hickson's avatar
Ian Hickson committed
84

85
  /// The size at which the image will be rendered.
86
  final Size? size;
87

88 89 90 91
  /// The [TargetPlatform] for which assets should be used. This allows images
  /// to be specified in a platform-neutral fashion yet use different assets on
  /// different platforms, to match local conventions e.g. for color matching or
  /// shadows.
92
  final TargetPlatform? platform;
93 94 95 96

  /// An image configuration that provides no additional information.
  ///
  /// Useful when resolving an [ImageProvider] without any context.
97
  static const ImageConfiguration empty = ImageConfiguration();
98 99

  @override
100
  bool operator ==(Object other) {
101
    if (other.runtimeType != runtimeType) {
102
      return false;
103
    }
104 105 106 107 108 109 110
    return other is ImageConfiguration
        && other.bundle == bundle
        && other.devicePixelRatio == devicePixelRatio
        && other.locale == locale
        && other.textDirection == textDirection
        && other.size == size
        && other.platform == platform;
111 112 113
  }

  @override
114
  int get hashCode => Object.hash(bundle, devicePixelRatio, locale, size, platform);
115 116 117

  @override
  String toString() {
118
    final StringBuffer result = StringBuffer();
119 120 121 122
    result.write('ImageConfiguration(');
    bool hasArguments = false;
    if (bundle != null) {
      result.write('bundle: $bundle');
123
      hasArguments = true;
124 125
    }
    if (devicePixelRatio != null) {
126
      if (hasArguments) {
127
        result.write(', ');
128
      }
129
      result.write('devicePixelRatio: ${devicePixelRatio!.toStringAsFixed(1)}');
130
      hasArguments = true;
131 132
    }
    if (locale != null) {
133
      if (hasArguments) {
134
        result.write(', ');
135
      }
136
      result.write('locale: $locale');
137
      hasArguments = true;
138
    }
Ian Hickson's avatar
Ian Hickson committed
139
    if (textDirection != null) {
140
      if (hasArguments) {
Ian Hickson's avatar
Ian Hickson committed
141
        result.write(', ');
142
      }
Ian Hickson's avatar
Ian Hickson committed
143 144 145
      result.write('textDirection: $textDirection');
      hasArguments = true;
    }
146
    if (size != null) {
147
      if (hasArguments) {
148
        result.write(', ');
149
      }
150
      result.write('size: $size');
151
      hasArguments = true;
152 153
    }
    if (platform != null) {
154
      if (hasArguments) {
155
        result.write(', ');
156
      }
157
      result.write('platform: ${platform!.name}');
158
      hasArguments = true;
159 160 161 162 163 164
    }
    result.write(')');
    return result.toString();
  }
}

165 166
/// Performs the decode process for use in [ImageProvider.load].
///
167 168 169
/// This typedef is deprecated. Use [DecoderBufferCallback] with
/// [ImageProvider.loadBuffer] instead.
///
170 171 172
/// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and
/// `allowUpscaling` parameters from implementations of [ImageProvider] that do
/// not expose them.
173 174 175
///
/// See also:
///
176 177
///  * [ResizeImage], which uses this to override the `cacheWidth`,
///    `cacheHeight`, and `allowUpscaling` parameters.
178
@Deprecated(
179
  'Use ImageDecoderCallback with ImageProvider.loadImage instead. '
180 181 182 183 184 185 186 187 188 189 190 191 192 193
  'This feature was deprecated after v2.13.0-1.0.pre.',
)
typedef DecoderCallback = Future<ui.Codec> Function(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling});

/// Performs the decode process for use in [ImageProvider.loadBuffer].
///
/// This callback allows decoupling of the `cacheWidth`, `cacheHeight`, and
/// `allowUpscaling` parameters from implementations of [ImageProvider] that do
/// not expose them.
///
/// See also:
///
///  * [ResizeImage], which uses this to override the `cacheWidth`,
///    `cacheHeight`, and `allowUpscaling` parameters.
194 195 196 197
@Deprecated(
  'Use ImageDecoderCallback with ImageProvider.loadImage instead. '
  'This feature was deprecated after v3.7.0-1.4.pre.',
)
198
typedef DecoderBufferCallback = Future<ui.Codec> Function(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool allowUpscaling});
199

200 201 202 203 204 205 206 207 208 209 210 211 212
/// Performs the decode process for use in [ImageProvider.loadImage].
///
/// This callback allows decoupling of the `getTargetSize` parameter from
/// implementations of [ImageProvider] that do not expose it.
///
/// See also:
///
///  * [ResizeImage], which uses this to load images at specific sizes.
typedef ImageDecoderCallback = Future<ui.Codec> Function(
  ui.ImmutableBuffer buffer, {
  ui.TargetImageSizeCallback? getTargetSize,
});

213 214 215 216 217 218 219
/// 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.
///
/// To obtain an [ImageStream] from an [ImageProvider], call [resolve],
/// passing it an [ImageConfiguration] object.
///
220
/// [ImageProvider] uses the global [imageCache] to cache images.
221 222 223
///
/// The type argument `T` is the type of the object used to represent a resolved
/// configuration. This is also the type used for the key in the image cache. It
Ian Hickson's avatar
Ian Hickson committed
224 225 226
/// should be immutable and implement the [==] operator and the [hashCode]
/// getter. Subclasses should subclass a variant of [ImageProvider] with an
/// explicit `T` type argument.
227 228 229
///
/// The type argument does not have to be specified when using the type as an
/// argument (where any image provider is acceptable).
230
///
231
/// The following image formats are supported: {@macro dart.ui.imageFormats}
232
///
233 234 235 236 237 238 239 240 241 242
/// ## Lifecycle of resolving an image
///
/// The [ImageProvider] goes through the following lifecycle to resolve an
/// image, once the [resolve] method is called:
///
///   1. Create an [ImageStream] using [createStream] to return to the caller.
///      This stream will be used to communicate back to the caller when the
///      image is decoded and ready to display, or when an error occurs.
///   2. Obtain the key for the image using [obtainKey].
///      Calling this method can throw exceptions into the zone asynchronously
243
///      or into the call stack synchronously. To handle that, an error handler
244 245 246 247 248 249 250 251
///      is created that catches both synchronous and asynchronous errors, to
///      make sure errors can be routed to the correct consumers.
///      The error handler is passed on to [resolveStreamForKey] and the
///      [ImageCache].
///   3. If the key is successfully obtained, schedule resolution of the image
///      using that key. This is handled by [resolveStreamForKey]. That method
///      may fizzle if it determines the image is no longer necessary, use the
///      provided [ImageErrorListener] to report an error, set the completer
252
///      from the cache if possible, or call [loadBuffer] to fetch the encoded image
253
///      bytes and schedule decoding.
254
///   4. The [loadBuffer] method is responsible for both fetching the encoded bytes
255 256 257
///      and decoding them using the provided [DecoderCallback]. It is called
///      in a context that uses the [ImageErrorListener] to report errors back.
///
258
/// Subclasses normally only have to implement the [loadBuffer] and [obtainKey]
259 260
/// methods. A subclass that needs finer grained control over the [ImageStream]
/// type must override [createStream]. A subclass that needs finer grained
261
/// control over the resolution, such as delaying calling [loadBuffer], must override
262 263 264 265 266 267 268 269
/// [resolveStreamForKey].
///
/// The [resolve] method is marked as [nonVirtual] so that [ImageProvider]s can
/// be properly composed, and so that the base class can properly set up error
/// handling for subsequent methods.
///
/// ## Using an [ImageProvider]
///
270
/// {@tool snippet}
271 272
///
/// The following shows the code required to write a widget that fully conforms
273
/// to the [ImageProvider] and [Widget] protocols. (It is essentially a
274
/// bare-bones version of the [widgets.Image] widget.)
275 276
///
/// ```dart
277 278
/// class MyImage extends StatefulWidget {
///   const MyImage({
279
///     super.key,
280
///     required this.imageProvider,
281
///   });
282 283 284 285
///
///   final ImageProvider imageProvider;
///
///   @override
286
///   State<MyImage> createState() => _MyImageState();
287 288
/// }
///
289
/// class _MyImageState extends State<MyImage> {
290 291
///   ImageStream? _imageStream;
///   ImageInfo? _imageInfo;
292 293 294 295 296 297 298 299 300 301 302
///
///   @override
///   void didChangeDependencies() {
///     super.didChangeDependencies();
///     // We call _getImage here because createLocalImageConfiguration() needs to
///     // be called again if the dependencies changed, in case the changes relate
///     // to the DefaultAssetBundle, MediaQuery, etc, which that method uses.
///     _getImage();
///   }
///
///   @override
303
///   void didUpdateWidget(MyImage oldWidget) {
304
///     super.didUpdateWidget(oldWidget);
305
///     if (widget.imageProvider != oldWidget.imageProvider) {
306
///       _getImage();
307
///     }
308 309 310
///   }
///
///   void _getImage() {
311
///     final ImageStream? oldImageStream = _imageStream;
312
///     _imageStream = widget.imageProvider.resolve(createLocalImageConfiguration(context));
313
///     if (_imageStream!.key != oldImageStream?.key) {
314 315 316
///       // If the keys are the same, then we got the same image back, and so we don't
///       // need to update the listeners. If the key changed, though, we must make sure
///       // to switch our listeners to the new image stream.
317 318
///       final ImageStreamListener listener = ImageStreamListener(_updateImage);
///       oldImageStream?.removeListener(listener);
319
///       _imageStream!.addListener(listener);
320 321 322 323 324 325
///     }
///   }
///
///   void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
///     setState(() {
///       // Trigger a build whenever the image changes.
326
///       _imageInfo?.dispose();
327 328 329 330 331 332
///       _imageInfo = imageInfo;
///     });
///   }
///
///   @override
///   void dispose() {
333
///     _imageStream?.removeListener(ImageStreamListener(_updateImage));
334 335
///     _imageInfo?.dispose();
///     _imageInfo = null;
336 337 338 339 340
///     super.dispose();
///   }
///
///   @override
///   Widget build(BuildContext context) {
341
///     return RawImage(
342 343 344 345 346 347
///       image: _imageInfo?.image, // this is a dart:ui Image object
///       scale: _imageInfo?.scale ?? 1.0,
///     );
///   }
/// }
/// ```
348
/// {@end-tool}
349
@optionalTypeArgs
350
abstract class ImageProvider<T extends Object> {
351 352 353 354 355 356 357 358 359 360
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const ImageProvider();

  /// Resolves this image provider using the given `configuration`, returning
  /// an [ImageStream].
  ///
  /// This is the public entry-point of the [ImageProvider] class hierarchy.
  ///
  /// Subclasses should implement [obtainKey] and [load], which are used by this
361 362 363 364 365 366
  /// method. If they need to change the implementation of [ImageStream] used,
  /// they should override [createStream]. If they need to manage the actual
  /// resolution of the image, they should override [resolveStreamForKey].
  ///
  /// See the Lifecycle documentation on [ImageProvider] for more information.
  @nonVirtual
367
  ImageStream resolve(ImageConfiguration configuration) {
368 369 370
    final ImageStream stream = createStream(configuration);
    // Load the key (potentially asynchronously), set up an error handling zone,
    // and call resolveStreamForKey.
Dan Field's avatar
Dan Field committed
371 372 373 374 375
    _createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener errorHandler) {
        resolveStreamForKey(configuration, stream, key, errorHandler);
      },
376
      (T? key, Object exception, StackTrace? stack) async {
Dan Field's avatar
Dan Field committed
377
        await null; // wait an event turn in case a listener has been added to the image stream.
378
        InformationCollector? collector;
379
        assert(() {
380 381 382 383 384
          collector = () => <DiagnosticsNode>[
            DiagnosticsProperty<ImageProvider>('Image provider', this),
            DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration),
            DiagnosticsProperty<T>('Image key', key, defaultValue: null),
          ];
385 386
          return true;
        }());
387 388 389 390
        if (stream.completer == null) {
          stream.setCompleter(_ErrorImageCompleter());
        }
        stream.completer!.reportError(
Dan Field's avatar
Dan Field committed
391 392 393 394
          exception: exception,
          stack: stack,
          context: ErrorDescription('while resolving an image'),
          silent: true, // could be a network error or whatnot
395 396 397 398
          informationCollector: collector,
        );
      },
    );
399 400 401 402 403 404 405 406 407 408 409 410 411
    return stream;
  }

  /// Called by [resolve] to create the [ImageStream] it returns.
  ///
  /// Subclasses should override this instead of [resolve] if they need to
  /// return some subclass of [ImageStream]. The stream created here will be
  /// passed to [resolveStreamForKey].
  @protected
  ImageStream createStream(ImageConfiguration configuration) {
    return ImageStream();
  }

Dan Field's avatar
Dan Field committed
412 413 414 415 416 417 418 419 420 421
  /// Returns the cache location for the key that this [ImageProvider] creates.
  ///
  /// The location may be [ImageCacheStatus.untracked], indicating that this
  /// image provider's key is not available in the [ImageCache].
  ///
  /// The `cache` and `configuration` parameters must not be null. If the
  /// `handleError` parameter is null, errors will be reported to
  /// [FlutterError.onError], and the method will return null.
  ///
  /// A completed return value of null indicates that an error has occurred.
422
  Future<ImageCacheStatus?> obtainCacheStatus({
423 424
    required ImageConfiguration configuration,
    ImageErrorListener? handleError,
Dan Field's avatar
Dan Field committed
425
  }) {
426
    final Completer<ImageCacheStatus?> completer = Completer<ImageCacheStatus?>();
Dan Field's avatar
Dan Field committed
427 428 429
    _createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener innerHandleError) {
430
        completer.complete(PaintingBinding.instance.imageCache.statusForKey(key));
Dan Field's avatar
Dan Field committed
431
      },
432
      (T? key, Object exception, StackTrace? stack) async {
Dan Field's avatar
Dan Field committed
433 434 435
        if (handleError != null) {
          handleError(exception, stack);
        } else {
436
          InformationCollector? collector;
437
          assert(() {
438 439 440 441 442
            collector = () => <DiagnosticsNode>[
              DiagnosticsProperty<ImageProvider>('Image provider', this),
              DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration),
              DiagnosticsProperty<T>('Image key', key, defaultValue: null),
            ];
443 444
            return true;
          }());
445 446 447 448 449 450
          FlutterError.reportError(FlutterErrorDetails(
            context: ErrorDescription('while checking the cache location of an image'),
            informationCollector: collector,
            exception: exception,
            stack: stack,
          ));
Dan Field's avatar
Dan Field committed
451 452 453 454 455 456 457 458 459 460 461 462 463
          completer.complete(null);
        }
      },
    );
    return completer.future;
  }

  /// This method is used by both [resolve] and [obtainCacheStatus] to ensure
  /// that errors thrown during key creation are handled whether synchronous or
  /// asynchronous.
  void _createErrorHandlerAndKey(
    ImageConfiguration configuration,
    _KeyAndErrorHandlerCallback<T> successCallback,
464
    _AsyncKeyErrorHandler<T?> errorCallback,
Dan Field's avatar
Dan Field committed
465
  ) {
466
    T? obtainedKey;
467
    bool didError = false;
468
    Future<void> handleError(Object exception, StackTrace? stack) async {
469 470 471
      if (didError) {
        return;
      }
Dan Field's avatar
Dan Field committed
472
      if (!didError) {
473
        didError = true;
Dan Field's avatar
Dan Field committed
474 475
        errorCallback(obtainedKey, exception, stack);
      }
476
    }
477

478 479 480 481 482 483 484 485 486
    Future<T> key;
    try {
      key = obtainKey(configuration);
    } catch (error, stackTrace) {
      handleError(error, stackTrace);
      return;
    }
    key.then<void>((T key) {
      obtainedKey = key;
487
      try {
488
        successCallback(key, handleError);
489 490
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
491
      }
492
    }).catchError(handleError);
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
  }

  /// Called by [resolve] with the key returned by [obtainKey].
  ///
  /// Subclasses should override this method rather than calling [obtainKey] if
  /// they need to use a key directly. The [resolve] method installs appropriate
  /// error handling guards so that errors will bubble up to the right places in
  /// the framework, and passes those guards along to this method via the
  /// [handleError] parameter.
  ///
  /// It is safe for the implementation of this method to call [handleError]
  /// multiple times if multiple errors occur, or if an error is thrown both
  /// synchronously into the current part of the stack and thrown into the
  /// enclosing [Zone].
  ///
  /// The default implementation uses the key to interact with the [ImageCache],
  /// calling [ImageCache.putIfAbsent] and notifying listeners of the [stream].
  /// Implementers that do not call super are expected to correctly use the
  /// [ImageCache].
  @protected
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
514 515 516 517
    // 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) {
518
      final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
519
        key,
520
        () => stream.completer!,
521 522 523 524 525
        onError: handleError,
      );
      assert(identical(completer, stream.completer));
      return;
    }
526
    final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
527
      key,
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
      () {
        ImageStreamCompleter result = loadImage(key, PaintingBinding.instance.instantiateImageCodecWithSize);
        // This check exists as a fallback for backwards compatibility until the
        // deprecated `loadBuffer()` method is removed. Until then, ImageProvider
        // subclasses may have only overridden `loadBuffer()`, in which case the
        // base implementation of `loadWithSize()` will return a sentinel value
        // of type `_AbstractImageStreamCompleter`.
        if (result is _AbstractImageStreamCompleter) {
          result = loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer);
          if (result is _AbstractImageStreamCompleter) {
            // Same fallback as above but for the deprecated `load()` method.
            result = load(key, PaintingBinding.instance.instantiateImageCodec);
          }
        }
        return result;
      },
544 545 546 547 548
      onError: handleError,
    );
    if (completer != null) {
      stream.setCompleter(completer);
    }
549 550
  }

551 552 553 554 555 556 557 558 559 560 561 562 563 564
  /// Evicts an entry from the image cache.
  ///
  /// Returns a [Future] which indicates whether the value was successfully
  /// removed.
  ///
  /// The [ImageProvider] used does not need to be the same instance that was
  /// passed to an [Image] widget, but it does need to create a key which is
  /// equal to one.
  ///
  /// The [cache] is optional and defaults to the global image cache.
  ///
  /// The [configuration] is optional and defaults to
  /// [ImageConfiguration.empty].
  ///
565
  /// {@tool snippet}
566 567
  ///
  /// The following sample code shows how an image loaded using the [Image]
568
  /// widget can be evicted using a [NetworkImage] with a matching URL.
569 570 571
  ///
  /// ```dart
  /// class MyWidget extends StatelessWidget {
572
  ///   const MyWidget({
573
  ///     super.key,
574
  ///     this.url = ' ... ',
575
  ///   });
576 577
  ///
  ///   final String url;
578 579 580
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
581
  ///     return Image.network(url);
582 583 584
  ///   }
  ///
  ///   void evictImage() {
585
  ///     final NetworkImage provider = NetworkImage(url);
586
  ///     provider.evict().then<void>((bool success) {
587
  ///       if (success) {
588
  ///         debugPrint('removed image!');
589
  ///       }
590 591 592 593
  ///     });
  ///   }
  /// }
  /// ```
594
  /// {@end-tool}
595
  Future<bool> evict({ ImageCache? cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {
596 597
    cache ??= imageCache;
    final T key = await obtainKey(configuration);
598
    return cache.evict(key);
599 600
  }

601 602 603 604 605 606 607 608
  /// Converts an ImageProvider's settings plus an ImageConfiguration to a key
  /// that describes the precise image to load.
  ///
  /// The type of the key is determined by the subclass. It is a value that
  /// unambiguously identifies the image (_including its scale_) that the [load]
  /// method will fetch. Different [ImageProvider]s given the same constructor
  /// arguments and [ImageConfiguration] objects should return keys that are
  /// '==' to each other (possibly by using a class for the key that itself
Ian Hickson's avatar
Ian Hickson committed
609
  /// implements [==]).
610 611 612 613
  Future<T> obtainKey(ImageConfiguration configuration);

  /// Converts a key into an [ImageStreamCompleter], and begins fetching the
  /// image.
614
  ///
615 616 617 618 619 620 621 622 623 624 625 626
  /// This method is deprecated. Implement [loadBuffer] for faster image
  /// loading. Only one of [load] and [loadBuffer] must be implemented, and
  /// [loadBuffer] is preferred.
  ///
  /// 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
  @Deprecated(
627
    'Implement loadImage for faster image loading. '
628 629 630 631 632 633 634 635 636
    'This feature was deprecated after v2.13.0-1.0.pre.',
  )
  ImageStreamCompleter load(T key, DecoderCallback decode) {
    throw UnsupportedError('Implement loadBuffer for faster image loading');
  }

  /// Converts a key into an [ImageStreamCompleter], and begins fetching the
  /// image.
  ///
637 638 639 640
  /// For backwards-compatibility the default implementation of this method returns
  /// an object that will cause [resolveStreamForKey] to consult [load]. However,
  /// implementors of this interface should only override this method and not
  /// [load], which is deprecated.
641
  ///
642 643 644 645 646
  /// The [decode] callback provides the logic to obtain the codec for the
  /// image.
  ///
  /// See also:
  ///
647
  ///  * [ResizeImage], for modifying the key to account for cache dimensions.
648
  @protected
649 650 651 652
  @Deprecated(
    'Implement loadImage for image loading. '
    'This feature was deprecated after v3.7.0-1.4.pre.',
  )
653
  ImageStreamCompleter loadBuffer(T key, DecoderBufferCallback decode) {
654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
    return _AbstractImageStreamCompleter();
  }

  /// Converts a key into an [ImageStreamCompleter], and begins fetching the
  /// image.
  ///
  /// For backwards-compatibility the default implementation of this method returns
  /// an object that will cause [resolveStreamForKey] to consult [loadBuffer].
  /// However, implementors of this interface should only override this method
  /// and not [loadBuffer], which is deprecated.
  ///
  /// 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.
  // TODO(tvolkert): make abstract (https://github.com/flutter/flutter/issues/119209)
  @protected
  ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {
    return _AbstractImageStreamCompleter();
675
  }
676 677

  @override
678
  String toString() => '${objectRuntimeType(this, 'ImageConfiguration')}()';
679 680
}

681 682 683 684
/// A class that exists to facilitate backwards compatibility in the transition
/// from [ImageProvider.load] to [ImageProvider.loadBuffer] to [ImageProvider.loadImage]
class _AbstractImageStreamCompleter extends ImageStreamCompleter {}

685
/// Key for the image obtained by an [AssetImage] or [ExactAssetImage].
686
///
687
/// This is used to identify the precise resource in the [imageCache].
688
@immutable
689 690 691 692 693
class AssetBundleImageKey {
  /// Creates the key for an [AssetImage] or [AssetBundleImageProvider].
  ///
  /// The arguments must not be null.
  const AssetBundleImageKey({
694 695 696
    required this.bundle,
    required this.name,
    required this.scale,
697
  });
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712

  /// The bundle from which the image will be obtained.
  ///
  /// The image is obtained by calling [AssetBundle.load] on the given [bundle]
  /// using the key given by [name].
  final AssetBundle bundle;

  /// The key to use to obtain the resource from the [bundle]. This is the
  /// argument passed to [AssetBundle.load].
  final String name;

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

  @override
713
  bool operator ==(Object other) {
714
    if (other.runtimeType != runtimeType) {
715
      return false;
716
    }
717 718 719 720
    return other is AssetBundleImageKey
        && other.bundle == bundle
        && other.name == name
        && other.scale == scale;
721 722 723
  }

  @override
724
  int get hashCode => Object.hash(bundle, name, scale);
725 726

  @override
727
  String toString() => '${objectRuntimeType(this, 'AssetBundleImageKey')}(bundle: $bundle, name: "$name", scale: $scale)';
728 729 730
}

/// A subclass of [ImageProvider] that knows about [AssetBundle]s.
731
///
732 733 734
/// This factors out the common logic of [AssetBundle]-based [ImageProvider]
/// classes, simplifying what subclasses must implement to just [obtainKey].
abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKey> {
735 736
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
737
  const AssetBundleImageProvider();
738

739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
  @override
  ImageStreamCompleter loadImage(AssetBundleImageKey key, ImageDecoderCallback decode) {
    InformationCollector? collector;
    assert(() {
      collector = () => <DiagnosticsNode>[
            DiagnosticsProperty<ImageProvider>('Image provider', this),
            DiagnosticsProperty<AssetBundleImageKey>('Image key', key),
          ];
      return true;
    }());
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode: decode),
      scale: key.scale,
      debugLabel: key.name,
      informationCollector: collector,
    );
  }

757
  /// Converts a key into an [ImageStreamCompleter], and begins fetching the
758
  /// image.
759 760 761 762 763 764 765 766 767 768 769
  @override
  ImageStreamCompleter loadBuffer(AssetBundleImageKey key, DecoderBufferCallback decode) {
    InformationCollector? collector;
    assert(() {
      collector = () => <DiagnosticsNode>[
        DiagnosticsProperty<ImageProvider>('Image provider', this),
        DiagnosticsProperty<AssetBundleImageKey>('Image key', key),
      ];
      return true;
    }());
    return MultiFrameImageStreamCompleter(
770
      codec: _loadAsync(key, decodeBufferDeprecated: decode),
771 772 773 774 775 776
      scale: key.scale,
      debugLabel: key.name,
      informationCollector: collector,
    );
  }

777
  @override
778
  ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
779
    InformationCollector? collector;
780
    assert(() {
781 782 783 784
      collector = () => <DiagnosticsNode>[
        DiagnosticsProperty<ImageProvider>('Image provider', this),
        DiagnosticsProperty<AssetBundleImageKey>('Image key', key),
      ];
785 786
      return true;
    }());
787
    return MultiFrameImageStreamCompleter(
788
      codec: _loadAsync(key, decodeDeprecated: decode),
789
      scale: key.scale,
790
      debugLabel: key.name,
791
      informationCollector: collector,
792
    );
793 794
  }

795
  /// Fetches the image from the asset bundle, decodes it, and returns a
796 797 798 799
  /// corresponding [ImageInfo] object.
  ///
  /// This function is used by [load].
  @protected
800 801 802 803 804 805
  Future<ui.Codec> _loadAsync(
    AssetBundleImageKey key, {
    ImageDecoderCallback? decode,
    DecoderBufferCallback? decodeBufferDeprecated,
    DecoderCallback? decodeDeprecated,
  }) async {
806
    if (decode != null) {
807
      ui.ImmutableBuffer buffer;
808 809 810 811 812 813 814 815 816 817
      // 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 {
        buffer = await key.bundle.loadBuffer(key.name);
      } on FlutterError {
        PaintingBinding.instance.imageCache.evict(key);
        rethrow;
      }
      return decode(buffer);
    }
818 819 820 821 822 823 824 825 826 827 828 829
    if (decodeBufferDeprecated != null) {
      ui.ImmutableBuffer buffer;
      // 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 {
        buffer = await key.bundle.loadBuffer(key.name);
      } on FlutterError {
        PaintingBinding.instance.imageCache.evict(key);
        rethrow;
      }
      return decodeBufferDeprecated(buffer);
    }
830
    ByteData data;
831 832 833 834 835
    // 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 {
836
      PaintingBinding.instance.imageCache.evict(key);
837 838
      rethrow;
    }
839
    return decodeDeprecated!(data.buffer.asUint8List());
840 841 842
  }
}

843 844 845
/// Key used internally by [ResizeImage].
///
/// This is used to identify the precise resource in the [imageCache].
846
@immutable
847 848 849
class ResizeImageKey {
  // Private constructor so nobody from the outside can poison the image cache
  // with this key. It's only accessible to [ResizeImage] internally.
850
  const ResizeImageKey._(this._providerCacheKey, this._policy, this._width, this._height, this._allowUpscaling);
851

852
  final Object _providerCacheKey;
853
  final ResizeImagePolicy _policy;
854 855
  final int? _width;
  final int? _height;
856
  final bool _allowUpscaling;
857 858 859

  @override
  bool operator ==(Object other) {
860
    if (other.runtimeType != runtimeType) {
861
      return false;
862
    }
863 864
    return other is ResizeImageKey
        && other._providerCacheKey == _providerCacheKey
865
        && other._policy == _policy
866
        && other._width == _width
867 868
        && other._height == _height
        && other._allowUpscaling == _allowUpscaling;
869 870 871
  }

  @override
872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265
  int get hashCode => Object.hash(_providerCacheKey, _policy, _width, _height, _allowUpscaling);
}

/// Configures the behavior for [ResizeImage].
///
/// This is used in [ResizeImage.policy] to affect how the [ResizeImage.width]
/// and [ResizeImage.height] properties are interpreted.
enum ResizeImagePolicy {
  /// Sizes the image to the exact width and height specified by
  /// [ResizeImage.width] and [ResizeImage.height].
  ///
  /// If [ResizeImage.width] and [ResizeImage.height] are both non-null, the
  /// output image will have the specified width and height (with the
  /// corresponding aspect ratio) regardless of whether it matches the source
  /// image's intrinsic aspect ratio. This case is similar to [BoxFit.fill].
  ///
  /// If only one of `width` and `height` is non-null, then the output image
  /// will be scaled to the associated width or height, and the other dimension
  /// will take whatever value is needed to maintain the image's original aspect
  /// ratio. These cases are simnilar to [BoxFit.fitWidth] and
  /// [BoxFit.fitHeight], respectively.
  ///
  /// If [ResizeImage.allowUpscaling] is false (the default), the width and the
  /// height of the output image will each be clamped to the intrinsic width and
  /// height of the image. This may result in a different aspect ratio than the
  /// aspect ratio specified by the target width and height (e.g. if the height
  /// gets clamped downwards but the width does not).
  ///
  /// ## Examples
  ///
  /// The examples below show how [ResizeImagePolicy.exact] works in various
  /// scenarios. In each example, the source image has a size of 300x200
  /// (landscape orientation), the red box is a 150x150 square, and the green
  /// box is a 400x400 square.
  ///
  /// <table>
  /// <tr>
  /// <td>Scenario</td>
  /// <td>Output</td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   width: 150,
  ///   height: 150,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_150x150_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   width: 150,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_150xnull_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   height: 150,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx150_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   width: 400,
  ///   height: 400,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400x400_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   width: 400,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400xnull_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   height: 400,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx400_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   width: 400,
  ///   height: 400,
  ///   allowUpscaling: true,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400x400_true.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   width: 400,
  ///   allowUpscaling: true,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_400xnull_true.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   height: 400,
  ///   allowUpscaling: true,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_exact_nullx400_true.png)
  ///
  /// </td>
  /// </tr>
  /// </table>
  exact,

  /// Scales the image as necessary to ensure that it fits within the bounding
  /// box specified by [ResizeImage.width] and [ResizeImage.height] while
  /// maintaining its aspect ratio.
  ///
  /// If [ResizeImage.allowUpscaling] is true, the image will be scaled up or
  /// down to best fit the bounding box; otherwise it will only ever be scaled
  /// down.
  ///
  /// This is conceptually similar to [BoxFit.contain].
  ///
  /// ## Examples
  ///
  /// The examples below show how [ResizeImagePolicy.fit] works in various
  /// scenarios. In each example, the source image has a size of 300x200
  /// (landscape orientation), the red box is a 150x150 square, and the green
  /// box is a 400x400 square.
  ///
  /// <table>
  /// <tr>
  /// <td>Scenario</td>
  /// <td>Output</td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   width: 150,
  ///   height: 150,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_150x150_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   width: 150,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_150xnull_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   height: 150,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx150_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   width: 400,
  ///   height: 400,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400x400_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   width: 400,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400xnull_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   height: 400,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx400_false.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   width: 400,
  ///   height: 400,
  ///   allowUpscaling: true,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400x400_true.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   width: 400,
  ///   allowUpscaling: true,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_400xnull_true.png)
  ///
  /// </td>
  /// </tr>
  /// <tr>
  /// <td>
  ///
  /// ```dart
  /// const ResizeImage(
  ///   AssetImage('dragon_cake.jpg'),
  ///   policy: ResizeImagePolicy.fit,
  ///   height: 400,
  ///   allowUpscaling: true,
  /// )
  /// ```
  ///
  /// </td>
  /// <td>
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/resize_image_policy_fit_nullx400_true.png)
  ///
  /// </td>
  /// </tr>
  /// </table>
  fit,
1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
}

/// 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.
1276
class ResizeImage extends ImageProvider<ResizeImageKey> {
1277 1278 1279 1280 1281
  /// 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.
1282 1283
  ///
  /// At least one of `width` and `height` must be non-null.
1284 1285 1286 1287
  const ResizeImage(
    this.imageProvider, {
    this.width,
    this.height,
1288
    this.policy = ResizeImagePolicy.exact,
1289
    this.allowUpscaling = false,
1290
  }) : assert(width != null || height != null);
1291 1292 1293 1294 1295

  /// The [ImageProvider] that this class wraps.
  final ImageProvider imageProvider;

  /// The width the image should decode to and cache.
1296 1297
  ///
  /// At least one of this and [height] must be non-null.
1298
  final int? width;
1299 1300

  /// The height the image should decode to and cache.
1301 1302
  ///
  /// At least one of this and [width] must be non-null.
1303
  final int? height;
1304

1305 1306 1307 1308 1309
  /// The policy that determines how [width] and [height] are interpreted.
  ///
  /// Defaults to [ResizeImagePolicy.exact].
  final ResizeImagePolicy policy;

1310 1311 1312 1313 1314 1315 1316 1317 1318
  /// Whether the [width] and [height] parameters should be clamped to the
  /// intrinsic width and height of the image.
  ///
  /// In general, it is better for memory usage to avoid scaling the image
  /// beyond its intrinsic dimensions when decoding it. If there is a need to
  /// scale an image larger, it is better to apply a scale to the canvas, or
  /// to use an appropriate [Image.fit].
  final bool allowUpscaling;

1319 1320 1321 1322 1323
  /// Composes the `provider` in a [ResizeImage] only when `cacheWidth` and
  /// `cacheHeight` are not both null.
  ///
  /// When `cacheWidth` and `cacheHeight` are both null, this will return the
  /// `provider` directly.
1324
  static ImageProvider<Object> resizeIfNeeded(int? cacheWidth, int? cacheHeight, ImageProvider<Object> provider) {
1325 1326 1327 1328 1329 1330
    if (cacheWidth != null || cacheHeight != null) {
      return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
    }
    return provider;
  }

1331
  @override
1332 1333 1334 1335
  @Deprecated(
    'Implement loadImage for faster image loading. '
    'This feature was deprecated after v2.13.0-1.0.pre.',
  )
1336
  ImageStreamCompleter load(ResizeImageKey key, DecoderCallback decode) {
1337
    Future<ui.Codec> decodeResize(Uint8List buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
1338
      assert(
1339 1340
        cacheWidth == null && cacheHeight == null && allowUpscaling == null,
        'ResizeImage cannot be composed with another ImageProvider that applies '
1341
        'cacheWidth, cacheHeight, or allowUpscaling.',
1342
      );
1343
      return decode(buffer, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
1344
    }
1345
    final ImageStreamCompleter completer = imageProvider.load(key._providerCacheKey, decodeResize);
1346
    if (!kReleaseMode) {
1347
      completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})';
1348 1349
    }
    return completer;
1350 1351
  }

1352
  @override
1353 1354 1355 1356
  @Deprecated(
    'Implement loadImage for image loading. '
    'This feature was deprecated after v3.7.0-1.4.pre.',
  )
1357 1358 1359 1360 1361 1362 1363 1364 1365
  ImageStreamCompleter loadBuffer(ResizeImageKey key, DecoderBufferCallback decode) {
    Future<ui.Codec> decodeResize(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
      assert(
        cacheWidth == null && cacheHeight == null && allowUpscaling == null,
        'ResizeImage cannot be composed with another ImageProvider that applies '
        'cacheWidth, cacheHeight, or allowUpscaling.',
      );
      return decode(buffer, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
    }
1366

1367 1368 1369 1370 1371 1372 1373
    final ImageStreamCompleter completer = imageProvider.loadBuffer(key._providerCacheKey, decodeResize);
    if (!kReleaseMode) {
      completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})';
    }
    return completer;
  }

1374 1375 1376 1377 1378 1379 1380 1381 1382
  @override
  ImageStreamCompleter loadImage(ResizeImageKey key, ImageDecoderCallback decode) {
    Future<ui.Codec> decodeResize(ui.ImmutableBuffer buffer, {ui.TargetImageSizeCallback? getTargetSize}) {
      assert(
        getTargetSize == null,
        'ResizeImage cannot be composed with another ImageProvider that applies '
        'getTargetSize.',
      );
      return decode(buffer, getTargetSize: (int intrinsicWidth, int intrinsicHeight) {
1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431
        switch (policy) {
          case ResizeImagePolicy.exact:
            int? targetWidth = width;
            int? targetHeight = height;

            if (!allowUpscaling) {
              if (targetWidth != null && targetWidth > intrinsicWidth) {
                targetWidth = intrinsicWidth;
              }
              if (targetHeight != null && targetHeight > intrinsicHeight) {
                targetHeight = intrinsicHeight;
              }
            }

            return ui.TargetImageSize(width: targetWidth, height: targetHeight);
          case ResizeImagePolicy.fit:
            final double aspectRatio = intrinsicWidth / intrinsicHeight;
            final int maxWidth = width ?? intrinsicWidth;
            final int maxHeight = height ?? intrinsicHeight;
            int targetWidth = intrinsicWidth;
            int targetHeight = intrinsicHeight;

            if (targetWidth > maxWidth) {
              targetWidth = maxWidth;
              targetHeight = targetWidth ~/ aspectRatio;
            }

            if (targetHeight > maxHeight) {
              targetHeight = maxHeight;
              targetWidth = (targetHeight * aspectRatio).floor();
            }

            if (allowUpscaling) {
              if (width == null) {
                assert(height != null);
                targetHeight = height!;
                targetWidth = (targetHeight * aspectRatio).floor();
              } else if (height == null) {
                targetWidth = width!;
                targetHeight = targetWidth ~/ aspectRatio;
              } else {
                final int derivedMaxWidth = (maxHeight * aspectRatio).floor();
                final int derivedMaxHeight = maxWidth ~/ aspectRatio;
                targetWidth = math.min(maxWidth, derivedMaxWidth);
                targetHeight = math.min(maxHeight, derivedMaxHeight);
              }
            }

            return ui.TargetImageSize(width: targetWidth, height: targetHeight);
1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442
        }
      });
    }

    final ImageStreamCompleter completer = imageProvider.loadImage(key._providerCacheKey, decodeResize);
    if (!kReleaseMode) {
      completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})';
    }
    return completer;
  }

1443
  @override
1444 1445
  Future<ResizeImageKey> obtainKey(ImageConfiguration configuration) {
    Completer<ResizeImageKey>? completer;
1446 1447
    // If the imageProvider.obtainKey future is synchronous, then we will be able to fill in result with
    // a value before completer is initialized below.
1448
    SynchronousFuture<ResizeImageKey>? result;
1449 1450 1451 1452
    imageProvider.obtainKey(configuration).then((Object key) {
      if (completer == null) {
        // This future has completed synchronously (completer was never assigned),
        // so we can directly create the synchronous result to return.
1453
        result = SynchronousFuture<ResizeImageKey>(ResizeImageKey._(key, policy, width, height, allowUpscaling));
1454 1455
      } else {
        // This future did not synchronously complete.
1456
        completer.complete(ResizeImageKey._(key, policy, width, height, allowUpscaling));
1457 1458 1459
      }
    });
    if (result != null) {
1460
      return result!;
1461
    }
1462
    // If the code reaches here, it means the imageProvider.obtainKey was not
1463
    // completed sync, so we initialize the completer for completion later.
1464
    completer = Completer<ResizeImageKey>();
1465
    return completer.future;
1466
  }
1467 1468 1469 1470
}

/// Fetches the given URL from the network, associating it with the given scale.
///
1471
/// The image will be cached regardless of cache headers from the server.
1472
///
1473
/// When a network image is used on the Web platform, the `cacheWidth` and
1474 1475 1476 1477
/// `cacheHeight` parameters of the [DecoderCallback] are only supported when the
/// application is running with the CanvasKit renderer. When the application is using
/// the HTML renderer, the web engine delegates image decoding of network images to the Web,
/// which does not support custom decode sizes.
1478
///
1479 1480 1481
/// See also:
///
///  * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
1482
// TODO(ianh): Find some way to honor cache headers to the extent that when the
1483 1484
// last reference to an image is released, we proactively evict the image from
// our cache if the headers describe the image as having expired at that point.
1485
abstract class NetworkImage extends ImageProvider<NetworkImage> {
1486 1487
  /// Creates an object that fetches the image at the given URL.
  ///
1488
  /// The arguments [url] and [scale] must not be null.
1489
  const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;
1490 1491

  /// The URL from which the image will be fetched.
1492
  String get url;
1493 1494

  /// The scale to place in the [ImageInfo] object of the image.
1495
  double get scale;
1496

1497
  /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
1498 1499
  ///
  /// When running flutter on the web, headers are not used.
1500
  Map<String, String>? get headers;
1501 1502

  @override
1503
  ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
1504 1505 1506

  @override
  ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode);
1507 1508 1509

  @override
  ImageStreamCompleter loadImage(NetworkImage key, ImageDecoderCallback decode);
1510 1511
}

Ian Hickson's avatar
Ian Hickson committed
1512 1513
/// Decodes the given [File] object as an image, associating it with the given
/// scale.
1514
///
1515 1516 1517
/// The provider does not monitor the file for changes. If you expect the
/// underlying data to change, you should call the [evict] method.
///
1518 1519 1520
/// See also:
///
///  * [Image.file] for a shorthand of an [Image] widget backed by [FileImage].
1521
@immutable
Ian Hickson's avatar
Ian Hickson committed
1522 1523 1524 1525
class FileImage extends ImageProvider<FileImage> {
  /// Creates an object that decodes a [File] as an image.
  ///
  /// The arguments must not be null.
1526
  const FileImage(this.file, { this.scale = 1.0 });
Ian Hickson's avatar
Ian Hickson committed
1527 1528 1529 1530 1531 1532 1533 1534 1535

  /// The file to decode into an image.
  final File file;

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

  @override
  Future<FileImage> obtainKey(ImageConfiguration configuration) {
1536
    return SynchronousFuture<FileImage>(this);
Ian Hickson's avatar
Ian Hickson committed
1537 1538 1539
  }

  @override
1540
  ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
1541
    return MultiFrameImageStreamCompleter(
1542
      codec: _loadAsync(key, decodeDeprecated: decode),
1543
      scale: key.scale,
1544
      debugLabel: key.file.path,
1545 1546 1547
      informationCollector: () => <DiagnosticsNode>[
        ErrorDescription('Path: ${file.path}'),
      ],
Ian Hickson's avatar
Ian Hickson committed
1548 1549 1550
    );
  }

1551 1552 1553
  @override
  ImageStreamCompleter loadBuffer(FileImage key, DecoderBufferCallback decode) {
    return MultiFrameImageStreamCompleter(
1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567
      codec: _loadAsync(key, decodeBufferDeprecated: decode),
      scale: key.scale,
      debugLabel: key.file.path,
      informationCollector: () => <DiagnosticsNode>[
        ErrorDescription('Path: ${file.path}'),
      ],
    );
  }

  @override
  @protected
  ImageStreamCompleter loadImage(FileImage key, ImageDecoderCallback decode) {
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode: decode),
1568 1569 1570 1571 1572 1573 1574 1575
      scale: key.scale,
      debugLabel: key.file.path,
      informationCollector: () => <DiagnosticsNode>[
        ErrorDescription('Path: ${file.path}'),
      ],
    );
  }

1576 1577 1578 1579 1580 1581
  Future<ui.Codec> _loadAsync(
    FileImage key, {
    ImageDecoderCallback? decode,
    DecoderBufferCallback? decodeBufferDeprecated,
    DecoderCallback? decodeDeprecated,
  }) async {
Ian Hickson's avatar
Ian Hickson committed
1582 1583
    assert(key == this);

1584 1585 1586 1587 1588 1589
    // TODO(jonahwilliams): making this sync caused test failures that seem to
    // indicate that we can fail to call evict unless at least one await has
    // occurred in the test.
    // https://github.com/flutter/flutter/issues/113044
    final int lengthInBytes = await file.length();
    if (lengthInBytes == 0) {
1590
      // The file may become available later.
1591
      PaintingBinding.instance.imageCache.evict(key);
1592
      throw StateError('$file is empty and cannot be loaded as an image.');
1593
    }
1594
    if (decode != null) {
1595 1596 1597 1598
      if (file.runtimeType == File) {
        return decode(await ui.ImmutableBuffer.fromFilePath(file.path));
      }
      return decode(await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes()));
1599
    }
1600 1601 1602 1603 1604 1605
    if (decodeBufferDeprecated != null) {
      if (file.runtimeType == File) {
        return decodeBufferDeprecated(await ui.ImmutableBuffer.fromFilePath(file.path));
      }
      return decodeBufferDeprecated(await ui.ImmutableBuffer.fromUint8List(await file.readAsBytes()));
    }
1606
    return decodeDeprecated!(await file.readAsBytes());
Ian Hickson's avatar
Ian Hickson committed
1607 1608 1609
  }

  @override
1610
  bool operator ==(Object other) {
1611
    if (other.runtimeType != runtimeType) {
Ian Hickson's avatar
Ian Hickson committed
1612
      return false;
1613
    }
1614
    return other is FileImage
1615
        && other.file.path == file.path
1616
        && other.scale == scale;
Ian Hickson's avatar
Ian Hickson committed
1617 1618 1619
  }

  @override
1620
  int get hashCode => Object.hash(file.path, scale);
Ian Hickson's avatar
Ian Hickson committed
1621 1622

  @override
1623
  String toString() => '${objectRuntimeType(this, 'FileImage')}("${file.path}", scale: $scale)';
Ian Hickson's avatar
Ian Hickson committed
1624 1625
}

1626 1627
/// Decodes the given [Uint8List] buffer as an image, associating it with the
/// given scale.
1628 1629 1630 1631 1632 1633
///
/// The provided [bytes] buffer should not be changed after it is provided
/// to a [MemoryImage]. To provide an [ImageStream] that represents an image
/// that changes over time, consider creating a new subclass of [ImageProvider]
/// whose [load] method returns a subclass of [ImageStreamCompleter] that can
/// handle providing multiple images.
1634 1635 1636 1637
///
/// See also:
///
///  * [Image.memory] for a shorthand of an [Image] widget backed by [MemoryImage].
1638
@immutable
1639 1640 1641 1642
class MemoryImage extends ImageProvider<MemoryImage> {
  /// Creates an object that decodes a [Uint8List] buffer as an image.
  ///
  /// The arguments must not be null.
1643
  const MemoryImage(this.bytes, { this.scale = 1.0 });
1644 1645

  /// The bytes to decode into an image.
1646 1647
  ///
  /// The bytes represent encoded image bytes and can be encoded in any of the
1648
  /// following supported image formats: {@macro dart.ui.imageFormats}
1649 1650 1651 1652
  ///
  /// See also:
  ///
  ///  * [PaintingBinding.instantiateImageCodec]
1653 1654 1655
  final Uint8List bytes;

  /// The scale to place in the [ImageInfo] object of the image.
1656 1657 1658 1659 1660
  ///
  /// See also:
  ///
  ///  * [ImageInfo.scale], which gives more information on how this scale is
  ///    applied.
1661 1662 1663 1664
  final double scale;

  @override
  Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
1665
    return SynchronousFuture<MemoryImage>(this);
1666 1667 1668
  }

  @override
1669
  ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
1670
    return MultiFrameImageStreamCompleter(
1671
      codec: _loadAsync(key, decodeDeprecated: decode),
1672
      scale: key.scale,
1673
      debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
1674
    );
1675 1676
  }

1677 1678 1679
  @override
  ImageStreamCompleter loadBuffer(MemoryImage key, DecoderBufferCallback decode) {
    return MultiFrameImageStreamCompleter(
1680
      codec: _loadAsync(key, decodeBufferDeprecated: decode),
1681 1682 1683 1684
      scale: key.scale,
      debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
    );
  }
1685

1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700
  @override
  ImageStreamCompleter loadImage(MemoryImage key, ImageDecoderCallback decode) {
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode: decode),
      scale: key.scale,
      debugLabel: 'MemoryImage(${describeIdentity(key.bytes)})',
    );
  }

  Future<ui.Codec> _loadAsync(
    MemoryImage key, {
    ImageDecoderCallback? decode,
    DecoderBufferCallback? decodeBufferDeprecated,
    DecoderCallback? decodeDeprecated,
  }) async {
1701 1702 1703 1704 1705
    assert(key == this);
    if (decode != null) {
      final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
      return decode(buffer);
    }
1706 1707 1708 1709 1710
    if (decodeBufferDeprecated != null) {
      final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
      return decodeBufferDeprecated(buffer);
    }
    return decodeDeprecated!(bytes);
1711 1712 1713
  }

  @override
1714
  bool operator ==(Object other) {
1715
    if (other.runtimeType != runtimeType) {
1716
      return false;
1717
    }
1718 1719 1720
    return other is MemoryImage
        && other.bytes == bytes
        && other.scale == scale;
1721 1722 1723
  }

  @override
1724
  int get hashCode => Object.hash(bytes.hashCode, scale);
1725 1726

  @override
1727
  String toString() => '${objectRuntimeType(this, 'MemoryImage')}(${describeIdentity(bytes)}, scale: $scale)';
1728
}
1729

1730 1731
/// Fetches an image from an [AssetBundle], associating it with the given scale.
///
1732
/// This implementation requires an explicit final [assetName] and [scale] on
1733 1734 1735 1736
/// construction, and ignores the device pixel ratio and size in the
/// configuration passed into [resolve]. For a resolution-aware variant that
/// uses the configuration to pick an appropriate image based on the device
/// pixel ratio and size, see [AssetImage].
1737 1738 1739 1740 1741
///
/// ## Fetching assets
///
/// When fetching an image provided by the app itself, use the [assetName]
/// argument to name the asset to choose. For instance, consider a directory
1742
/// `icons` with an image `heart.png`. First, the `pubspec.yaml` of the project
1743 1744 1745 1746 1747 1748 1749 1750
/// should specify its assets in the `flutter` section:
///
/// ```yaml
/// flutter:
///   assets:
///     - icons/heart.png
/// ```
///
1751
/// Then, to fetch the image and associate it with scale `1.5`, use:
1752
///
1753
/// {@tool snippet}
1754
/// ```dart
1755
/// const ExactAssetImage('icons/heart.png', scale: 1.5)
1756
/// ```
1757
/// {@end-tool}
1758
///
1759
/// ## Assets in packages
1760 1761 1762 1763 1764
///
/// To fetch an asset from a package, the [package] argument must be provided.
/// For instance, suppose the structure above is inside a package called
/// `my_icons`. Then to fetch the image, use:
///
1765
/// {@tool snippet}
1766
/// ```dart
1767
/// const ExactAssetImage('icons/heart.png', scale: 1.5, package: 'my_icons')
1768
/// ```
1769
/// {@end-tool}
1770 1771 1772 1773
///
/// Assets used by the package itself should also be fetched using the [package]
/// argument as above.
///
1774
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
1775
/// is bundled automatically with the app. In particular, assets used by the
1776
/// package itself must be specified in its `pubspec.yaml`.
1777 1778
///
/// A package can also choose to have assets in its 'lib/' folder that are not
1779
/// specified in its `pubspec.yaml`. In this case for those images to be
1780 1781 1782
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
1783 1784 1785
///     lib/backgrounds/background1.png
///     lib/backgrounds/background2.png
///     lib/backgrounds/background3.png
1786
///
1787
/// To include, say the first image, the `pubspec.yaml` of the app should specify
1788 1789 1790
/// it in the `assets` section:
///
/// ```yaml
1791 1792
///   assets:
///     - packages/fancy_backgrounds/backgrounds/background1.png
1793 1794
/// ```
///
Ian Hickson's avatar
Ian Hickson committed
1795
/// The `lib/` is implied, so it should not be included in the asset path.
1796
///
1797 1798 1799 1800
/// See also:
///
///  * [Image.asset] for a shorthand of an [Image] widget backed by
///    [ExactAssetImage] when using a scale.
1801
@immutable
1802 1803 1804
class ExactAssetImage extends AssetBundleImageProvider {
  /// Creates an object that fetches the given image from an asset bundle.
  ///
1805
  /// The [assetName] and [scale] arguments must not be null. The [scale] arguments
1806 1807 1808
  /// defaults to 1.0. The [bundle] argument may be null, in which case the
  /// bundle provided in the [ImageConfiguration] passed to the [resolve] call
  /// will be used instead.
1809 1810 1811 1812
  ///
  /// The [package] argument must be non-null when fetching an asset that is
  /// included in a package. See the documentation for the [ExactAssetImage] class
  /// itself for details.
1813 1814
  const ExactAssetImage(
    this.assetName, {
1815
    this.scale = 1.0,
1816 1817
    this.bundle,
    this.package,
1818
  });
1819

1820 1821 1822
  /// The name of the asset.
  final String assetName;

1823 1824
  /// The key to use to obtain the resource from the [bundle]. This is the
  /// argument passed to [AssetBundle.load].
1825
  String get keyName => package == null ? assetName : 'packages/$package/$assetName';
1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836

  /// The scale to place in the [ImageInfo] object of the image.
  final double scale;

  /// The bundle from which the image will be obtained.
  ///
  /// If the provided [bundle] is null, the bundle provided in the
  /// [ImageConfiguration] passed to the [resolve] call will be used instead. If
  /// that is also null, the [rootBundle] is used.
  ///
  /// The image is obtained by calling [AssetBundle.load] on the given [bundle]
1837
  /// using the key given by [keyName].
1838
  final AssetBundle? bundle;
1839

1840 1841
  /// The name of the package from which the image is included. See the
  /// documentation for the [ExactAssetImage] class itself for details.
1842
  final String? package;
1843

1844 1845
  @override
  Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
1846
    return SynchronousFuture<AssetBundleImageKey>(AssetBundleImageKey(
1847
      bundle: bundle ?? configuration.bundle ?? rootBundle,
1848
      name: keyName,
1849
      scale: scale,
1850 1851 1852 1853
    ));
  }

  @override
1854
  bool operator ==(Object other) {
1855
    if (other.runtimeType != runtimeType) {
1856
      return false;
1857
    }
1858 1859 1860 1861
    return other is ExactAssetImage
        && other.keyName == keyName
        && other.scale == scale
        && other.bundle == bundle;
1862 1863 1864
  }

  @override
1865
  int get hashCode => Object.hash(keyName, scale, bundle);
1866 1867

  @override
1868
  String toString() => '${objectRuntimeType(this, 'ExactAssetImage')}(name: "$keyName", scale: $scale, bundle: $bundle)';
1869
}
1870 1871

// A completer used when resolving an image fails sync.
1872
class _ErrorImageCompleter extends ImageStreamCompleter { }
1873 1874 1875

/// The exception thrown when the HTTP request to load a network image fails.
class NetworkImageLoadException implements Exception {
1876 1877
  /// Creates a [NetworkImageLoadException] with the specified http [statusCode]
  /// and [uri].
1878
  NetworkImageLoadException({required this.statusCode, required this.uri})
1879
      : _message = 'HTTP request failed, statusCode: $statusCode, $uri';
1880 1881 1882 1883 1884 1885 1886

  /// The HTTP status code from the server.
  final int statusCode;

  /// A human-readable error message.
  final String _message;

1887
  /// Resolved URL of the requested image.
1888 1889 1890 1891 1892
  final Uri uri;

  @override
  String toString() => _message;
}