| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'binding.dart'; |
| import 'debug.dart'; |
| import 'image_provider.dart' as image_provider; |
| import 'image_stream.dart'; |
| |
| /// The dart:io implementation of [image_provider.NetworkImage]. |
| @immutable |
| class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage { |
| /// Creates an object that fetches the image at the given URL. |
| /// |
| /// The arguments [url] and [scale] must not be null. |
| const NetworkImage(this.url, { this.scale = 1.0, this.headers }); |
| |
| @override |
| final String url; |
| |
| @override |
| final double scale; |
| |
| @override |
| final Map<String, String>? headers; |
| |
| @override |
| Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) { |
| return SynchronousFuture<NetworkImage>(this); |
| } |
| |
| @override |
| ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { |
| // Ownership of this controller is handed off to [_loadAsync]; it is that |
| // method's responsibility to close the controller's stream when the image |
| // has been loaded or an error is thrown. |
| final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); |
| |
| return MultiFrameImageStreamCompleter( |
| codec: _loadAsync(key as NetworkImage, chunkEvents, decodeDeprecated: decode), |
| chunkEvents: chunkEvents.stream, |
| scale: key.scale, |
| debugLabel: key.url, |
| informationCollector: () => <DiagnosticsNode>[ |
| DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this), |
| DiagnosticsProperty<image_provider.NetworkImage>('Image key', key), |
| ], |
| ); |
| } |
| |
| @override |
| ImageStreamCompleter loadBuffer(image_provider.NetworkImage key, image_provider.DecoderBufferCallback decode) { |
| // Ownership of this controller is handed off to [_loadAsync]; it is that |
| // method's responsibility to close the controller's stream when the image |
| // has been loaded or an error is thrown. |
| final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); |
| |
| return MultiFrameImageStreamCompleter( |
| codec: _loadAsync(key as NetworkImage, chunkEvents, decodeBufferDeprecated: decode), |
| chunkEvents: chunkEvents.stream, |
| scale: key.scale, |
| debugLabel: key.url, |
| informationCollector: () => <DiagnosticsNode>[ |
| DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this), |
| DiagnosticsProperty<image_provider.NetworkImage>('Image key', key), |
| ], |
| ); |
| } |
| |
| @override |
| ImageStreamCompleter loadImage(image_provider.NetworkImage key, image_provider.ImageDecoderCallback decode) { |
| // Ownership of this controller is handed off to [_loadAsync]; it is that |
| // method's responsibility to close the controller's stream when the image |
| // has been loaded or an error is thrown. |
| final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); |
| |
| return MultiFrameImageStreamCompleter( |
| codec: _loadAsync(key as NetworkImage, chunkEvents, decode: decode), |
| chunkEvents: chunkEvents.stream, |
| scale: key.scale, |
| debugLabel: key.url, |
| informationCollector: () => <DiagnosticsNode>[ |
| DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this), |
| DiagnosticsProperty<image_provider.NetworkImage>('Image key', key), |
| ], |
| ); |
| } |
| |
| // Do not access this field directly; use [_httpClient] instead. |
| // We set `autoUncompress` to false to ensure that we can trust the value of |
| // the `Content-Length` HTTP header. We automatically uncompress the content |
| // in our call to [consolidateHttpClientResponseBytes]. |
| static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false; |
| |
| static HttpClient get _httpClient { |
| HttpClient client = _sharedHttpClient; |
| assert(() { |
| if (debugNetworkImageHttpClientProvider != null) { |
| client = debugNetworkImageHttpClientProvider!(); |
| } |
| return true; |
| }()); |
| return client; |
| } |
| |
| Future<ui.Codec> _loadAsync( |
| NetworkImage key, |
| StreamController<ImageChunkEvent> chunkEvents, { |
| image_provider.ImageDecoderCallback? decode, |
| image_provider.DecoderBufferCallback? decodeBufferDeprecated, |
| image_provider.DecoderCallback? decodeDeprecated, |
| }) async { |
| try { |
| assert(key == this); |
| |
| final Uri resolved = Uri.base.resolve(key.url); |
| |
| final HttpClientRequest request = await _httpClient.getUrl(resolved); |
| |
| headers?.forEach((String name, String value) { |
| request.headers.add(name, value); |
| }); |
| final HttpClientResponse response = await request.close(); |
| if (response.statusCode != HttpStatus.ok) { |
| // The network may be only temporarily unavailable, or the file will be |
| // added on the server later. Avoid having future calls to resolve |
| // fail to check the network again. |
| await response.drain<List<int>>(<int>[]); |
| throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved); |
| } |
| |
| final Uint8List bytes = await consolidateHttpClientResponseBytes( |
| response, |
| onBytesReceived: (int cumulative, int? total) { |
| chunkEvents.add(ImageChunkEvent( |
| cumulativeBytesLoaded: cumulative, |
| expectedTotalBytes: total, |
| )); |
| }, |
| ); |
| if (bytes.lengthInBytes == 0) { |
| throw Exception('NetworkImage is an empty file: $resolved'); |
| } |
| |
| if (decode != null) { |
| final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); |
| return decode(buffer); |
| } else if (decodeBufferDeprecated != null) { |
| final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes); |
| return decodeBufferDeprecated(buffer); |
| } else { |
| assert(decodeDeprecated != null); |
| return decodeDeprecated!(bytes); |
| } |
| } catch (e) { |
| // Depending on where the exception was thrown, the image cache may not |
| // have had a chance to track the key in the cache at all. |
| // Schedule a microtask to give the cache a chance to add the key. |
| scheduleMicrotask(() { |
| PaintingBinding.instance.imageCache.evict(key); |
| }); |
| rethrow; |
| } finally { |
| chunkEvents.close(); |
| } |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| return other is NetworkImage |
| && other.url == url |
| && other.scale == scale; |
| } |
| |
| @override |
| int get hashCode => Object.hash(url, scale); |
| |
| @override |
| String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)'; |
| } |