_network_image_io.dart 5.59 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10
// 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';

11
import 'binding.dart';
12 13 14 15
import 'debug.dart';
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';

16 17 18
// Method signature for _loadAsync decode callbacks.
typedef _SimpleDecoderCallback = Future<ui.Codec> Function(ui.ImmutableBuffer buffer);

19
/// The dart:io implementation of [image_provider.NetworkImage].
20
@immutable
21 22
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
  /// Creates an object that fetches the image at the given URL.
23
  const NetworkImage(this.url, { this.scale = 1.0, this.headers });
24 25 26 27 28 29 30 31

  @override
  final String url;

  @override
  final double scale;

  @override
32
  final Map<String, String>? headers;
33 34 35 36 37 38

  @override
  Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
    return SynchronousFuture<NetworkImage>(this);
  }

39 40 41 42 43 44 45 46
  @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(
47
      codec: _loadAsync(key as NetworkImage, chunkEvents, decode: decode),
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
      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),
67 68
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
69
      debugLabel: key.url,
70 71 72 73
      informationCollector: () => <DiagnosticsNode>[
        DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
        DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
      ],
74 75 76
    );
  }

77 78 79 80 81 82 83
  // 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 {
84
    HttpClient? client;
85
    assert(() {
86
      if (debugNetworkImageHttpClientProvider != null) {
87
        client = debugNetworkImageHttpClientProvider!();
88
      }
89 90
      return true;
    }());
91
    return client ?? _sharedHttpClient;
92
  }
93 94 95

  Future<ui.Codec> _loadAsync(
    NetworkImage key,
96
    StreamController<ImageChunkEvent> chunkEvents, {
97
    required _SimpleDecoderCallback decode,
98
  }) async {
99 100 101 102
    try {
      assert(key == this);

      final Uri resolved = Uri.base.resolve(key.url);
103

104
      final HttpClientRequest request = await _httpClient.getUrl(resolved);
105

106 107
      headers?.forEach((String name, String value) {
        request.headers.add(name, value);
108
      });
109
      final HttpClientResponse response = await request.close();
110 111 112 113
      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.
114
        await response.drain<List<int>>(<int>[]);
115
        throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
116
      }
117

118 119
      final Uint8List bytes = await consolidateHttpClientResponseBytes(
        response,
120
        onBytesReceived: (int cumulative, int? total) {
121 122 123 124 125
          chunkEvents.add(ImageChunkEvent(
            cumulativeBytesLoaded: cumulative,
            expectedTotalBytes: total,
          ));
        },
126
      );
127
      if (bytes.lengthInBytes == 0) {
128
        throw Exception('NetworkImage is an empty file: $resolved');
129
      }
130

131
      return decode(await ui.ImmutableBuffer.fromUint8List(bytes));
132 133 134 135 136
    } 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(() {
137
        PaintingBinding.instance.imageCache.evict(key);
138 139
      });
      rethrow;
140 141 142 143 144 145
    } finally {
      chunkEvents.close();
    }
  }

  @override
146
  bool operator ==(Object other) {
147
    if (other.runtimeType != runtimeType) {
148
      return false;
149
    }
150 151 152
    return other is NetworkImage
        && other.url == url
        && other.scale == scale;
153 154 155
  }

  @override
156
  int get hashCode => Object.hash(url, scale);
157 158

  @override
159
  String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: ${scale.toStringAsFixed(1)})';
160
}