blob: b911bde0524ef0d64f378bd0057c9852a9f8da1a [file] [log] [blame]
// Copyright 2013 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.
/// Utilities for loading images from the network.
/// This library expands the capabilities of the basic [] and
/// [NetworkImage] provided by Flutter core libraries, to include a retry
/// mechanism and connectivity detection.
library network;
import 'dart:async';
import 'dart:io' as io;
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
/// Fetches the image from the given URL, associating it with the given scale.
/// If [fetchStrategy] is specified, uses it instead of the
/// [defaultFetchStrategy] to obtain instructions for fetching the URL.
/// The image will be cached regardless of cache headers from the server.
class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
/// Creates an object that fetches the image at the given [url].
const NetworkImageWithRetry(
this.url, {
this.scale = 1.0,
this.fetchStrategy = defaultFetchStrategy,
/// The HTTP client used to download images.
static final io.HttpClient _client = io.HttpClient();
/// The URL from which the image will be fetched.
final String url;
/// The scale to place in the [ImageInfo] object of the image.
final double scale;
/// The strategy used to fetch the [url] and retry when the fetch fails.
/// This function is called at least once and may be called multiple times.
/// The first time it is called, it is passed a null [FetchFailure], which
/// indicates that this is the first attempt to fetch the [url]. Subsequent
/// calls pass non-null [FetchFailure] values, which indicate that previous
/// fetch attempts failed.
final FetchStrategy fetchStrategy;
/// HTTP Headers applied to the request.
/// Keys from this map will be used as header field names and the
/// values will be used as header values. A list of header names can
/// be found at
/// If the value is a DateTime, an HTTP date format will be applied.
/// If the value is an Iterable, each element will be added separately.
/// For all other types the default Object.toString method will be used.
/// Header names are converted to lower-case. If two header names are
/// the same when converted to lower-case, they are considered to be
/// the same header, with one set of values.
/// For example, to add an authorization header to the request:
/// final NetworkImageWithRetry subject = NetworkImageWithRetry(
/// Uri.parse(''),
/// headers: <String, Object>{
/// 'Authorization': base64Encode(utf8.encode('user:password'))
/// },
/// );
final Map<String, Object>? headers;
/// Used by [defaultFetchStrategy].
/// This indirection is necessary because [defaultFetchStrategy] is used as
/// the default constructor argument value, which requires that it be a const
/// expression.
static final FetchStrategy _defaultFetchStrategyFunction =
const FetchStrategyBuilder().build();
/// The [FetchStrategy] that [NetworkImageWithRetry] uses by default.
static Future<FetchInstructions> defaultFetchStrategy(
Uri uri, FetchFailure? failure) {
return _defaultFetchStrategyFunction(uri, failure);
Future<NetworkImageWithRetry> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImageWithRetry>(this);
ImageStreamCompleter load(NetworkImageWithRetry key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(_loadWithRetry(key, decode),
informationCollector: () sync* {
yield ErrorDescription('Image provider: $this');
yield ErrorDescription('Image key: $key');
void _debugCheckInstructions(FetchInstructions? instructions) {
assert(() {
if (instructions == null) {
if (fetchStrategy == defaultFetchStrategy) {
throw StateError(
'The default FetchStrategy returned null FetchInstructions. This\n'
'is likely a bug in $runtimeType. Please file a bug at\n'
} else {
throw StateError(
'The custom FetchStrategy used to fetch $url returned null\n'
'FetchInstructions. FetchInstructions must never be null, but\n'
'instead instruct to either make another fetch attempt or give up.');
return true;
Future<ImageInfo> _loadWithRetry(
NetworkImageWithRetry key, DecoderCallback decode) async {
assert(key == this);
final Stopwatch stopwatch = Stopwatch()..start();
final Uri resolved = Uri.base.resolve(key.url);
FetchInstructions instructions = await fetchStrategy(resolved, null);
int attemptCount = 0;
FetchFailure? lastFailure;
while (!instructions.shouldGiveUp) {
attemptCount += 1;
io.HttpClientRequest? request;
try {
request = await _client
headers?.forEach((String key, Object value) {
request?.headers.add(key, value);
final io.HttpClientResponse response =
await request.close().timeout(instructions.timeout);
if (response.statusCode != 200) {
throw FetchFailure._(
totalDuration: stopwatch.elapsed,
attemptCount: attemptCount,
httpStatusCode: response.statusCode,
final _Uint8ListBuilder builder = await response
(_Uint8ListBuilder buffer, List<int> bytes) => buffer..add(bytes),
final Uint8List bytes =;
if (bytes.lengthInBytes == 0) {
throw FetchFailure._(
totalDuration: stopwatch.elapsed,
attemptCount: attemptCount,
httpStatusCode: response.statusCode,
final ui.Codec codec = await decode(bytes);
final ui.Image image = (await codec.getNextFrame()).image;
return ImageInfo(
image: image,
scale: key.scale,
} catch (error) {
lastFailure = error is FetchFailure
? error
: FetchFailure._(
totalDuration: stopwatch.elapsed,
attemptCount: attemptCount,
originalException: error,
instructions = await fetchStrategy(instructions.uri, lastFailure);
if (instructions.alternativeImage != null) {
return instructions.alternativeImage!;
assert(lastFailure != null);
if (FlutterError.onError != null) {
exception: lastFailure!,
library: 'package:flutter_image',
ErrorDescription('$runtimeType failed to load ${instructions.uri}'),
throw lastFailure!;
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) {
return false;
final NetworkImageWithRetry typedOther = other;
return url == typedOther.url && scale == typedOther.scale;
int get hashCode => hashValues(url, scale);
String toString() => '$runtimeType("$url", scale: $scale)';
/// This function is called to get [FetchInstructions] to fetch an image.
/// The instructions are executed as soon as possible after the returned
/// [Future] resolves. If a delay in necessary between retries, use a delayed
/// [Future], such as [Future.delayed]. This is useful for implementing
/// back-off strategies and for recovering from lack of connectivity.
/// [uri] is the last requested image URI. A [FetchStrategy] may choose to use
/// a different URI (see [FetchInstructions.uri]).
/// If [failure] is `null`, then this is the first attempt to fetch the image.
/// If the [failure] is not `null`, it contains the information about the
/// previous attempt to fetch the image. A [FetchStrategy] may attempt to
/// recover from the failure by returning [FetchInstructions] that instruct
/// [NetworkImageWithRetry] to try again.
/// See [NetworkImageWithRetry.defaultFetchStrategy] for an example.
typedef FetchStrategy = Future<FetchInstructions> Function(
Uri uri, FetchFailure? failure);
/// Instructions [NetworkImageWithRetry] uses to fetch the image.
class FetchInstructions {
/// Instructs [NetworkImageWithRetry] to give up trying to download the image.
const FetchInstructions.giveUp({
required this.uri,
}) : shouldGiveUp = true,
timeout =;
/// Instructs [NetworkImageWithRetry] to attempt to download the image from
/// the given [uri] and [timeout] if it takes too long.
const FetchInstructions.attempt({
required this.uri,
required this.timeout,
}) : shouldGiveUp = false,
alternativeImage = null;
/// Instructs to give up trying.
/// If [alternativeImage] is `null` reports the latest [FetchFailure] to
/// [FlutterError].
final bool shouldGiveUp;
/// Timeout for the next network call.
final Duration timeout;
/// The URI to use on the next attempt.
final Uri uri;
/// Instructs to give up and use this image instead.
final Future<ImageInfo>? alternativeImage;
String toString() {
return '$runtimeType(\n'
' shouldGiveUp: $shouldGiveUp\n'
' timeout: $timeout\n'
' uri: $uri\n'
' alternativeImage?: ${alternativeImage != null ? 'yes' : 'no'}\n'
/// Contains information about a failed attempt to fetch an image.
class FetchFailure implements Exception {
const FetchFailure._({
required this.totalDuration,
required this.attemptCount,
}) : assert(attemptCount > 0);
/// The total amount of time it has taken so far to download the image.
final Duration totalDuration;
/// The number of times [NetworkImageWithRetry] attempted to fetch the image
/// so far.
/// This value starts with 1 and grows by 1 with each attempt to fetch the
/// image.
final int attemptCount;
/// HTTP status code, such as 500.
final int? httpStatusCode;
/// The exception that caused the fetch failure.
final dynamic originalException;
String toString() {
return '$runtimeType(\n'
' attemptCount: $attemptCount\n'
' httpStatusCode: $httpStatusCode\n'
' totalDuration: $totalDuration\n'
' originalException: $originalException\n'
/// An indefinitely growing builder of a [Uint8List].
class _Uint8ListBuilder {
static const int _kInitialSize = 100000; // 100KB-ish
int _usedLength = 0;
Uint8List _buffer = Uint8List(_kInitialSize);
Uint8List get data => Uint8List.view(_buffer.buffer, 0, _usedLength);
void add(List<int> bytes) {
_buffer.setAll(_usedLength, bytes);
_usedLength += bytes.length;
void _ensureCanAdd(int byteCount) {
final int totalSpaceNeeded = _usedLength + byteCount;
int newLength = _buffer.length;
while (totalSpaceNeeded > newLength) {
newLength *= 2;
if (newLength != _buffer.length) {
final Uint8List newBuffer = Uint8List(newLength);
newBuffer.setAll(0, _buffer);
newBuffer.setRange(0, _usedLength, _buffer);
_buffer = newBuffer;
/// Determines whether the given HTTP [statusCode] is transient.
typedef TransientHttpStatusCodePredicate = bool Function(int statusCode);
/// Builds a [FetchStrategy] function that retries up to a certain amount of
/// times for up to a certain amount of time.
/// Pauses between retries with pauses growing exponentially (known as
/// exponential backoff). Each attempt is subject to a [timeout]. Retries only
/// those HTTP status codes considered transient by a
/// [transientHttpStatusCodePredicate] function.
class FetchStrategyBuilder {
/// Creates a fetch strategy builder.
/// All parameters must be non-null.
const FetchStrategyBuilder({
this.timeout = const Duration(seconds: 30),
this.totalFetchTimeout = const Duration(minutes: 1),
this.maxAttempts = 5,
this.initialPauseBetweenRetries = const Duration(seconds: 1),
this.exponentialBackoffMultiplier = 2,
this.transientHttpStatusCodePredicate =
/// A list of HTTP status codes that can generally be retried.
/// You may want to use a different list depending on the needs of your
/// application.
static const List<int> defaultTransientHttpStatusCodes = <int>[
0, // Network error
408, // Request timeout
500, // Internal server error
502, // Bad gateway
503, // Service unavailable
504 // Gateway timeout
/// Maximum amount of time a single fetch attempt is allowed to take.
final Duration timeout;
/// A strategy built by this builder will retry for up to this amount of time
/// before giving up.
final Duration totalFetchTimeout;
/// Maximum number of attempts a strategy will make before giving up.
final int maxAttempts;
/// Initial amount of time between retries.
final Duration initialPauseBetweenRetries;
/// The pause between retries is multiplied by this number with each attempt,
/// causing it to grow exponentially.
final num exponentialBackoffMultiplier;
/// A function that determines whether a given HTTP status code should be
/// retried.
final TransientHttpStatusCodePredicate transientHttpStatusCodePredicate;
/// Uses [defaultTransientHttpStatusCodes] to determine if the [statusCode] is
/// transient.
static bool defaultTransientHttpStatusCodePredicate(int statusCode) {
return defaultTransientHttpStatusCodes.contains(statusCode);
/// Builds a [FetchStrategy] that operates using the properties of this
/// builder.
FetchStrategy build() {
return (Uri uri, FetchFailure? failure) async {
if (failure == null) {
// First attempt. Just load.
return FetchInstructions.attempt(
uri: uri,
timeout: timeout,
final bool isRetriableFailure = (failure.httpStatusCode != null &&
transientHttpStatusCodePredicate(failure.httpStatusCode!)) ||
failure.originalException is io.SocketException;
// If cannot retry, give up.
if (!isRetriableFailure || // retrying will not help
failure.totalDuration > totalFetchTimeout || // taking too long
failure.attemptCount > maxAttempts) {
// too many attempts
return FetchInstructions.giveUp(uri: uri);
// Exponential back-off.
final Duration pauseBetweenRetries = initialPauseBetweenRetries *
math.pow(exponentialBackoffMultiplier, failure.attemptCount - 1);
await Future<void>.delayed(pauseBetweenRetries);
// Retry.
return FetchInstructions.attempt(
uri: uri,
timeout: timeout,