image_provider.dart 30.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:typed_data';
8
import 'dart:ui' as ui show Codec;
Ian Hickson's avatar
Ian Hickson committed
9
import 'dart:ui' show Size, Locale, TextDirection, hashValues;
10 11

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

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

/// Configuration information passed to the [ImageProvider.resolve] method to
/// select a specific image.
22 23 24 25 26 27 28
///
/// 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.
29
@immutable
30 31 32 33 34 35 36 37 38
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
39
    this.textDirection,
40
    this.size,
Ian Hickson's avatar
Ian Hickson committed
41
    this.platform,
42 43 44 45 46 47 48 49 50 51
  });

  /// 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({
    AssetBundle bundle,
    double devicePixelRatio,
    Locale locale,
Ian Hickson's avatar
Ian Hickson committed
52
    TextDirection textDirection,
53
    Size size,
54
    TargetPlatform platform,
55
  }) {
56
    return ImageConfiguration(
57 58 59
      bundle: bundle ?? this.bundle,
      devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
      locale: locale ?? this.locale,
Ian Hickson's avatar
Ian Hickson committed
60
      textDirection: textDirection ?? this.textDirection,
61
      size: size ?? this.size,
Ian Hickson's avatar
Ian Hickson committed
62
      platform: platform ?? this.platform,
63 64 65 66 67 68 69 70 71 72 73 74 75
    );
  }

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

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

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

Ian Hickson's avatar
Ian Hickson committed
76 77 78
  /// The reading direction of the language for which to select the image.
  final TextDirection textDirection;

79 80 81
  /// The size at which the image will be rendered.
  final Size size;

82 83 84 85 86
  /// 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.
  final TargetPlatform platform;
87 88 89 90

  /// An image configuration that provides no additional information.
  ///
  /// Useful when resolving an [ImageProvider] without any context.
91
  static const ImageConfiguration empty = ImageConfiguration();
92 93 94 95 96

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

  @override
  int get hashCode => hashValues(bundle, devicePixelRatio, locale, size, platform);

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

155 156 157 158 159 160 161 162 163 164
/// 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});

165 166 167 168 169 170 171
/// 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.
///
172
/// [ImageProvider] uses the global [imageCache] to cache images.
173 174 175
///
/// 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
176 177 178
/// should be immutable and implement the [==] operator and the [hashCode]
/// getter. Subclasses should subclass a variant of [ImageProvider] with an
/// explicit `T` type argument.
179 180 181
///
/// The type argument does not have to be specified when using the type as an
/// argument (where any image provider is acceptable).
182
///
183 184
/// The following image formats are supported: {@macro flutter.dart:ui.imageFormats}
///
185
/// {@tool sample}
186 187
///
/// The following shows the code required to write a widget that fully conforms
188
/// to the [ImageProvider] and [Widget] protocols. (It is essentially a
189
/// bare-bones version of the [widgets.Image] widget.)
190 191
///
/// ```dart
192 193
/// class MyImage extends StatefulWidget {
///   const MyImage({
194 195 196 197 198 199 200 201
///     Key key,
///     @required this.imageProvider,
///   }) : assert(imageProvider != null),
///        super(key: key);
///
///   final ImageProvider imageProvider;
///
///   @override
202
///   _MyImageState createState() => _MyImageState();
203 204
/// }
///
205
/// class _MyImageState extends State<MyImage> {
206 207 208 209 210 211 212 213 214 215 216 217 218
///   ImageStream _imageStream;
///   ImageInfo _imageInfo;
///
///   @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
219
///   void didUpdateWidget(MyImage oldWidget) {
220 221 222 223 224 225 226 227 228 229 230 231
///     super.didUpdateWidget(oldWidget);
///     if (widget.imageProvider != oldWidget.imageProvider)
///       _getImage();
///   }
///
///   void _getImage() {
///     final ImageStream oldImageStream = _imageStream;
///     _imageStream = widget.imageProvider.resolve(createLocalImageConfiguration(context));
///     if (_imageStream.key != oldImageStream?.key) {
///       // 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.
232 233 234
///       final ImageStreamListener listener = ImageStreamListener(_updateImage);
///       oldImageStream?.removeListener(listener);
///       _imageStream.addListener(listener);
235 236 237 238 239 240 241 242 243 244 245 246
///     }
///   }
///
///   void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
///     setState(() {
///       // Trigger a build whenever the image changes.
///       _imageInfo = imageInfo;
///     });
///   }
///
///   @override
///   void dispose() {
247
///     _imageStream.removeListener(ImageStreamListener(_updateImage));
248 249 250 251 252
///     super.dispose();
///   }
///
///   @override
///   Widget build(BuildContext context) {
253
///     return RawImage(
254 255 256 257 258 259
///       image: _imageInfo?.image, // this is a dart:ui Image object
///       scale: _imageInfo?.scale ?? 1.0,
///     );
///   }
/// }
/// ```
260
/// {@end-tool}
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
@optionalTypeArgs
abstract class ImageProvider<T> {
  /// 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
  /// method.
  ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
276
    final ImageStream stream = ImageStream();
277
    T obtainedKey;
278
    bool didError = false;
279
    Future<void> handleError(dynamic exception, StackTrace stack) async {
280 281 282 283
      if (didError) {
        return;
      }
      didError = true;
284 285 286 287 288 289
      await null; // wait an event turn in case a listener has been added to the image stream.
      final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
      stream.setCompleter(imageCompleter);
      imageCompleter.setError(
        exception: exception,
        stack: stack,
290
        context: ErrorDescription('while resolving an image'),
291
        silent: true, // could be a network error or whatnot
292 293 294 295
        informationCollector: () sync* {
          yield DiagnosticsProperty<ImageProvider>('Image provider', this);
          yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
          yield DiagnosticsProperty<T>('Image key', obtainedKey, defaultValue: null);
296
        },
297 298
      );
    }
299

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    // If an error is added to a synchronous completer before a listener has been
    // added, it can throw an error both into the zone and up the stack. Thus, it
    // looks like the error has been caught, but it is in fact also bubbling to the
    // zone. Since we cannot prevent all usage of Completer.sync here, or rather
    // that changing them would be too breaking, we instead hook into the same
    // zone mechanism to intercept the uncaught error and deliver it to the
    // image stream's error handler. Note that these errors may be duplicated,
    // hence the need for the `didError` flag.
    final Zone dangerZone = Zone.current.fork(
      specification: ZoneSpecification(
        handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
          handleError(error, stackTrace);
        }
      )
    );
    dangerZone.runGuarded(() {
      Future<T> key;
      try {
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
322
      }
323 324
      key.then<void>((T key) {
        obtainedKey = key;
325 326 327 328 329
        final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
          key,
          () => load(key, PaintingBinding.instance.instantiateImageCodec),
          onError: handleError,
        );
330 331 332 333 334
        if (completer != null) {
          stream.setCompleter(completer);
        }
      }).catchError(handleError);
    });
335 336 337
    return stream;
  }

338 339 340 341 342 343 344 345 346 347 348 349 350 351
  /// 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].
  ///
352
  /// {@tool sample}
353 354
  ///
  /// The following sample code shows how an image loaded using the [Image]
355
  /// widget can be evicted using a [NetworkImage] with a matching URL.
356 357 358 359 360 361 362
  ///
  /// ```dart
  /// class MyWidget extends StatelessWidget {
  ///   final String url = '...';
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
363
  ///     return Image.network(url);
364 365 366
  ///   }
  ///
  ///   void evictImage() {
367
  ///     final NetworkImage provider = NetworkImage(url);
368 369 370 371 372 373 374
  ///     provider.evict().then<void>((bool success) {
  ///       if (success)
  ///         debugPrint('removed image!');
  ///     });
  ///   }
  /// }
  /// ```
375
  /// {@end-tool}
376
  Future<bool> evict({ ImageCache cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {
377 378 379 380 381
    cache ??= imageCache;
    final T key = await obtainKey(configuration);
    return cache.evict(key);
  }

382 383 384 385 386 387 388 389
  /// 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
390
  /// implements [==]).
391 392 393 394
  Future<T> obtainKey(ImageConfiguration configuration);

  /// Converts a key into an [ImageStreamCompleter], and begins fetching the
  /// image.
395 396 397 398 399 400
  ///
  /// The [decode] callback provides the logic to obtain the codec for the
  /// image.
  ///
  /// See also:
  ///
401
  ///  * [ResizeImage], for modifying the key to account for cache dimensions.
402
  @protected
403
  ImageStreamCompleter load(T key, DecoderCallback decode);
404 405 406 407 408

  @override
  String toString() => '$runtimeType()';
}

409
/// Key for the image obtained by an [AssetImage] or [ExactAssetImage].
410
///
411
/// This is used to identify the precise resource in the [imageCache].
412
@immutable
413 414 415 416 417 418 419
class AssetBundleImageKey {
  /// Creates the key for an [AssetImage] or [AssetBundleImageProvider].
  ///
  /// The arguments must not be null.
  const AssetBundleImageKey({
    @required this.bundle,
    @required this.name,
420
    @required this.scale,
421 422 423
  }) : assert(bundle != null),
       assert(name != null),
       assert(scale != null);
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441

  /// 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
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
442 443 444 445
    return other is AssetBundleImageKey
        && other.bundle == bundle
        && other.name == name
        && other.scale == scale;
446 447 448 449 450 451
  }

  @override
  int get hashCode => hashValues(bundle, name, scale);

  @override
452
  String toString() => '$runtimeType(bundle: $bundle, name: "$name", scale: $scale)';
453 454 455
}

/// A subclass of [ImageProvider] that knows about [AssetBundle]s.
456
///
457 458 459
/// 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> {
460 461
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
462
  const AssetBundleImageProvider();
463

464 465
  /// Converts a key into an [ImageStreamCompleter], and begins fetching the
  /// image using [loadAsync].
466
  @override
467
  ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
468
    return MultiFrameImageStreamCompleter(
469
      codec: _loadAsync(key, decode),
470
      scale: key.scale,
471 472 473
      informationCollector: () sync* {
        yield DiagnosticsProperty<ImageProvider>('Image provider', this);
        yield DiagnosticsProperty<AssetBundleImageKey>('Image key', key);
474
      },
475
    );
476 477
  }

478
  /// Fetches the image from the asset bundle, decodes it, and returns a
479 480 481 482
  /// corresponding [ImageInfo] object.
  ///
  /// This function is used by [load].
  @protected
483
  Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async {
484 485
    final ByteData data = await key.bundle.load(key.name);
    if (data == null)
486
      throw 'Unable to read data';
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
    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;
504 505 506 507
    return other is _SizeAwareCacheKey
        && other.providerCacheKey == providerCacheKey
        && other.width == width
        && other.height == height;
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
  }

  @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;

543 544 545 546 547 548 549 550 551 552 553 554
  /// 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.
  static ImageProvider<dynamic> resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider<dynamic> provider) {
    if (cacheWidth != null || cacheHeight != null) {
      return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
    }
    return provider;
  }

555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
  @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);
571
  }
572 573 574 575
}

/// Fetches the given URL from the network, associating it with the given scale.
///
576
/// The image will be cached regardless of cache headers from the server.
577
///
578 579 580 581 582
/// 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.
///
583 584 585
/// See also:
///
///  * [Image.network] for a shorthand of an [Image] widget backed by [NetworkImage].
586
// TODO(ianh): Find some way to honor cache headers to the extent that when the
587 588
// 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.
589
abstract class NetworkImage extends ImageProvider<NetworkImage> {
590 591
  /// Creates an object that fetches the image at the given URL.
  ///
592 593
  /// The arguments [url] and [scale] must not be null.
  const factory NetworkImage(String url, { double scale, Map<String, String> headers }) = network_image.NetworkImage;
594 595

  /// The URL from which the image will be fetched.
596
  String get url;
597 598

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

601
  /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
602 603 604
  ///
  /// When running flutter on the web, headers are not used.
  Map<String, String> get headers;
605 606

  @override
607
  ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);
608 609
}

Ian Hickson's avatar
Ian Hickson committed
610 611
/// Decodes the given [File] object as an image, associating it with the given
/// scale.
612 613 614 615
///
/// See also:
///
///  * [Image.file] for a shorthand of an [Image] widget backed by [FileImage].
Ian Hickson's avatar
Ian Hickson committed
616 617 618 619
class FileImage extends ImageProvider<FileImage> {
  /// Creates an object that decodes a [File] as an image.
  ///
  /// The arguments must not be null.
620
  const FileImage(this.file, { this.scale = 1.0 })
621 622
    : assert(file != null),
      assert(scale != null);
Ian Hickson's avatar
Ian Hickson committed
623 624 625 626 627 628 629 630 631

  /// 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) {
632
    return SynchronousFuture<FileImage>(this);
Ian Hickson's avatar
Ian Hickson committed
633 634 635
  }

  @override
636
  ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
637
    return MultiFrameImageStreamCompleter(
638
      codec: _loadAsync(key, decode),
639
      scale: key.scale,
640 641
      informationCollector: () sync* {
        yield ErrorDescription('Path: ${file?.path}');
642
      },
Ian Hickson's avatar
Ian Hickson committed
643 644 645
    );
  }

646
  Future<ui.Codec> _loadAsync(FileImage key, DecoderCallback decode) async {
Ian Hickson's avatar
Ian Hickson committed
647 648
    assert(key == this);

649
    final Uint8List bytes = await file.readAsBytes();
Ian Hickson's avatar
Ian Hickson committed
650 651 652
    if (bytes.lengthInBytes == 0)
      return null;

653
    return await decode(bytes);
Ian Hickson's avatar
Ian Hickson committed
654 655 656 657 658 659
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
660 661 662
    return other is FileImage
        && other.file?.path == file?.path
        && other.scale == scale;
Ian Hickson's avatar
Ian Hickson committed
663 664 665 666 667 668 669 670 671
  }

  @override
  int get hashCode => hashValues(file?.path, scale);

  @override
  String toString() => '$runtimeType("${file?.path}", scale: $scale)';
}

672 673
/// Decodes the given [Uint8List] buffer as an image, associating it with the
/// given scale.
674 675 676 677 678 679
///
/// 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.
680 681 682 683
///
/// See also:
///
///  * [Image.memory] for a shorthand of an [Image] widget backed by [MemoryImage].
684 685 686 687
class MemoryImage extends ImageProvider<MemoryImage> {
  /// Creates an object that decodes a [Uint8List] buffer as an image.
  ///
  /// The arguments must not be null.
688
  const MemoryImage(this.bytes, { this.scale = 1.0 })
689 690
    : assert(bytes != null),
      assert(scale != null);
691 692 693 694 695 696 697 698 699

  /// The bytes to decode into an image.
  final Uint8List bytes;

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

  @override
  Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
700
    return SynchronousFuture<MemoryImage>(this);
701 702 703
  }

  @override
704
  ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
705
    return MultiFrameImageStreamCompleter(
706
      codec: _loadAsync(key, decode),
707
      scale: key.scale,
708
    );
709 710
  }

711
  Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
712 713
    assert(key == this);

714
    return decode(bytes);
715 716 717 718 719 720
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
721 722 723
    return other is MemoryImage
        && other.bytes == bytes
        && other.scale == scale;
724 725 726 727 728 729
  }

  @override
  int get hashCode => hashValues(bytes.hashCode, scale);

  @override
730
  String toString() => '$runtimeType(${describeIdentity(bytes)}, scale: $scale)';
731
}
732

733 734
/// Fetches an image from an [AssetBundle], associating it with the given scale.
///
735
/// This implementation requires an explicit final [assetName] and [scale] on
736 737 738 739
/// 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].
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
///
/// ## 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
/// `icons` with an image `heart.png`. First, the [pubspec.yaml] of the project
/// should specify its assets in the `flutter` section:
///
/// ```yaml
/// flutter:
///   assets:
///     - icons/heart.png
/// ```
///
/// Then, to fetch the image and associate it with scale `1.5`, use
///
/// ```dart
757
/// AssetImage('icons/heart.png', scale: 1.5)
758 759
/// ```
///
760
/// ## Assets in packages
761 762 763 764 765 766
///
/// 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:
///
/// ```dart
767
/// AssetImage('icons/heart.png', scale: 1.5, package: 'my_icons')
768 769 770 771 772
/// ```
///
/// Assets used by the package itself should also be fetched using the [package]
/// argument as above.
///
773
/// If the desired asset is specified in the `pubspec.yaml` of the package, it
774
/// is bundled automatically with the app. In particular, assets used by the
775
/// package itself must be specified in its `pubspec.yaml`.
776 777
///
/// A package can also choose to have assets in its 'lib/' folder that are not
778
/// specified in its `pubspec.yaml`. In this case for those images to be
779 780 781 782 783 784 785
/// bundled, the app has to specify which ones to include. For instance a
/// package named `fancy_backgrounds` could have:
///
/// ```
/// lib/backgrounds/background1.png
/// lib/backgrounds/background2.png
/// lib/backgrounds/background3.png
786
/// ```
787
///
788
/// To include, say the first image, the `pubspec.yaml` of the app should specify
789 790 791
/// it in the `assets` section:
///
/// ```yaml
792 793
///   assets:
///     - packages/fancy_backgrounds/backgrounds/background1.png
794 795
/// ```
///
Ian Hickson's avatar
Ian Hickson committed
796
/// The `lib/` is implied, so it should not be included in the asset path.
797
///
798 799 800 801
/// See also:
///
///  * [Image.asset] for a shorthand of an [Image] widget backed by
///    [ExactAssetImage] when using a scale.
802 803 804
class ExactAssetImage extends AssetBundleImageProvider {
  /// Creates an object that fetches the given image from an asset bundle.
  ///
805
  /// The [assetName] and [scale] arguments must not be null. The [scale] arguments
806 807 808
  /// 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.
809 810 811 812
  ///
  /// 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.
813 814
  const ExactAssetImage(
    this.assetName, {
815
    this.scale = 1.0,
816 817 818
    this.bundle,
    this.package,
  }) : assert(assetName != null),
819
       assert(scale != null);
820

821 822 823
  /// The name of the asset.
  final String assetName;

824 825
  /// The key to use to obtain the resource from the [bundle]. This is the
  /// argument passed to [AssetBundle.load].
826
  String get keyName => package == null ? assetName : 'packages/$package/$assetName';
827 828 829 830 831 832 833 834 835 836 837

  /// 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]
838
  /// using the key given by [keyName].
839 840
  final AssetBundle bundle;

841 842 843 844
  /// The name of the package from which the image is included. See the
  /// documentation for the [ExactAssetImage] class itself for details.
  final String package;

845 846
  @override
  Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
847
    return SynchronousFuture<AssetBundleImageKey>(AssetBundleImageKey(
848
      bundle: bundle ?? configuration.bundle ?? rootBundle,
849
      name: keyName,
850
      scale: scale,
851 852 853 854 855 856 857
    ));
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
858 859 860 861
    return other is ExactAssetImage
        && other.keyName == keyName
        && other.scale == scale
        && other.bundle == bundle;
862 863 864
  }

  @override
865
  int get hashCode => hashValues(keyName, scale, bundle);
866 867

  @override
868
  String toString() => '$runtimeType(name: "$keyName", scale: $scale, bundle: $bundle)';
869
}
870 871 872 873 874 875

// A completer used when resolving an image fails sync.
class _ErrorImageCompleter extends ImageStreamCompleter {
  _ErrorImageCompleter();

  void setError({
876
    DiagnosticsNode context,
877 878 879 880 881 882 883 884 885 886 887 888 889 890
    dynamic exception,
    StackTrace stack,
    InformationCollector informationCollector,
    bool silent = false,
  }) {
    reportError(
      context: context,
      exception: exception,
      stack: stack,
      informationCollector: informationCollector,
      silent: silent,
    );
  }
}
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906

/// The exception thrown when the HTTP request to load a network image fails.
class NetworkImageLoadException implements Exception {
  /// Creates a [NetworkImageLoadException] with the specified http status
  /// [code] and the [uri]
  NetworkImageLoadException({@required this.statusCode, @required this.uri})
      : assert(uri != null),
        assert(statusCode != null),
        _message = 'HTTP request failed, statusCode: $statusCode, $uri';

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

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

907
  /// Resolved URL of the requested image.
908 909 910 911 912
  final Uri uri;

  @override
  String toString() => _message;
}