mirror of
https://github.com/flutter/packages.git
synced 2025-07-02 08:34:31 +08:00
[flutter_image] Import from flutter/flutter_image
This commit is contained in:
11
packages/flutter_image/.gitignore
vendored
Normal file
11
packages/flutter_image/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.buildlog
|
||||
.DS_Store
|
||||
.idea/libraries/*
|
||||
.idea/vcs.xml
|
||||
.idea/workspace.xml
|
||||
.pub/
|
||||
.settings/
|
||||
build/
|
||||
packages
|
||||
.packages
|
||||
pubspec.lock
|
17
packages/flutter_image/.idea/flutter_image.iml
generated
Normal file
17
packages/flutter_image/.idea/flutter_image.iml
generated
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="FLUTTER_MODULE_TYPE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tool/packages" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
8
packages/flutter_image/.idea/modules.xml
generated
Normal file
8
packages/flutter_image/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/flutter_image.iml" filepath="$PROJECT_DIR$/.idea/flutter_image.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
24
packages/flutter_image/.travis.yml
Normal file
24
packages/flutter_image/.travis.yml
Normal file
@ -0,0 +1,24 @@
|
||||
os:
|
||||
- linux
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
# Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version
|
||||
packages:
|
||||
- libstdc++6
|
||||
- fonts-droid-fallback
|
||||
|
||||
before_script:
|
||||
- git clone https://github.com/flutter/flutter.git -b master
|
||||
- export PATH=$PATH:$(pwd)/flutter/bin
|
||||
- export FLUTTER_HOME=$(pwd)/flutter
|
||||
- flutter doctor
|
||||
|
||||
script: ./tool/travis.sh
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.pub-cache
|
6
packages/flutter_image/AUTHORS
Normal file
6
packages/flutter_image/AUTHORS
Normal file
@ -0,0 +1,6 @@
|
||||
# Below is a list of people and organizations that have contributed
|
||||
# to the project. Names should be added to the list like so:
|
||||
#
|
||||
# Name/Organization <email address>
|
||||
|
||||
Google Inc.
|
35
packages/flutter_image/CHANGELOG.md
Normal file
35
packages/flutter_image/CHANGELOG.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 4.0.0
|
||||
|
||||
- Migrates to null safety
|
||||
- **Breaking change**: `NetworkImageWithRetry.load` now throws a `FetchFailure` if the fetched image data is zero bytes.
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* **Breaking change**. Updates for Flutter 1.10.15.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
- Update Flutter SDK version constraint.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* **Breaking change**. Updates for Flutter 1.5.9.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* **Breaking change**. SDK constraints to support Flutter beta versions and Dart 2 only.
|
||||
|
||||
## 0.0.3
|
||||
|
||||
- Moved `flutter_test` to dev_dependencies in `pubspec.yaml`, and fixed issues
|
||||
flagged by the analyzer.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- Add `NetworkImageWithRetry`, an `ImageProvider` with a retry mechanism.
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Contains no useful code.
|
26
packages/flutter_image/LICENSE
Normal file
26
packages/flutter_image/LICENSE
Normal file
@ -0,0 +1,26 @@
|
||||
Copyright 2016, the Flutter project authors. All rights reserved.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
22
packages/flutter_image/README.md
Normal file
22
packages/flutter_image/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Image utilities for Flutter
|
||||
|
||||
## NetworkImageWithRetry
|
||||
|
||||
Use `NetworkImageWithRetry` instead of `Image.network` to load images from the
|
||||
network with a retry mechanism.
|
||||
|
||||
Example:
|
||||
|
||||
```dart
|
||||
var avatar = new Image(
|
||||
image: new NetworkImageWithRetry('http://example.com/avatars/123.jpg'),
|
||||
);
|
||||
```
|
||||
|
||||
The retry mechanism may be customized by supplying a custom `FetchStrategy`
|
||||
function. `FetchStrategyBuilder` is a utility class that helps building fetch
|
||||
strategy functions.
|
||||
|
||||
## Features and bugs
|
||||
|
||||
Please file feature requests and bugs at https://github.com/flutter/flutter/issues.
|
437
packages/flutter_image/lib/network.dart
Normal file
437
packages/flutter_image/lib/network.dart
Normal file
@ -0,0 +1,437 @@
|
||||
// Copyright 2017, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. 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 [Image.network] 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.
|
||||
@immutable
|
||||
class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
||||
/// Creates an object that fetches the image at the given [url].
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
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;
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<NetworkImageWithRetry> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<NetworkImageWithRetry>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
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'
|
||||
'https://github.com/flutter/flutter/issues.');
|
||||
} 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);
|
||||
_debugCheckInstructions(instructions);
|
||||
int attemptCount = 0;
|
||||
FetchFailure? lastFailure;
|
||||
|
||||
while (!instructions.shouldGiveUp) {
|
||||
attemptCount += 1;
|
||||
io.HttpClientRequest? request;
|
||||
try {
|
||||
request = await _client
|
||||
.getUrl(instructions.uri)
|
||||
.timeout(instructions.timeout);
|
||||
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
|
||||
.fold(
|
||||
_Uint8ListBuilder(),
|
||||
(_Uint8ListBuilder buffer, List<int> bytes) => buffer..add(bytes),
|
||||
)
|
||||
.timeout(instructions.timeout);
|
||||
|
||||
final Uint8List bytes = builder.data;
|
||||
|
||||
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) {
|
||||
request?.close();
|
||||
lastFailure = error is FetchFailure
|
||||
? error
|
||||
: FetchFailure._(
|
||||
totalDuration: stopwatch.elapsed,
|
||||
attemptCount: attemptCount,
|
||||
originalException: error,
|
||||
);
|
||||
instructions = await fetchStrategy(instructions.uri, lastFailure);
|
||||
_debugCheckInstructions(instructions);
|
||||
}
|
||||
}
|
||||
|
||||
if (instructions.alternativeImage != null) {
|
||||
return instructions.alternativeImage!;
|
||||
}
|
||||
|
||||
assert(lastFailure != null);
|
||||
|
||||
if (FlutterError.onError != null) {
|
||||
FlutterError.onError!(FlutterErrorDetails(
|
||||
exception: lastFailure!,
|
||||
library: 'package:flutter_image',
|
||||
context:
|
||||
ErrorDescription('$runtimeType failed to load ${instructions.uri}'),
|
||||
));
|
||||
}
|
||||
|
||||
throw lastFailure!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
final NetworkImageWithRetry typedOther = other;
|
||||
return url == typedOther.url && scale == typedOther.scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(url, scale);
|
||||
|
||||
@override
|
||||
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.
|
||||
@immutable
|
||||
class FetchInstructions {
|
||||
/// Instructs [NetworkImageWithRetry] to give up trying to download the image.
|
||||
const FetchInstructions.giveUp({
|
||||
required this.uri,
|
||||
this.alternativeImage,
|
||||
}) : shouldGiveUp = true,
|
||||
timeout = Duration.zero;
|
||||
|
||||
/// 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;
|
||||
|
||||
@override
|
||||
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.
|
||||
@immutable
|
||||
class FetchFailure implements Exception {
|
||||
const FetchFailure._({
|
||||
required this.totalDuration,
|
||||
required this.attemptCount,
|
||||
this.httpStatusCode,
|
||||
this.originalException,
|
||||
}) : 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;
|
||||
|
||||
@override
|
||||
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) {
|
||||
_ensureCanAdd(bytes.length);
|
||||
_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 =
|
||||
defaultTransientHttpStatusCodePredicate,
|
||||
});
|
||||
|
||||
/// 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,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
20
packages/flutter_image/pubspec.yaml
Normal file
20
packages/flutter_image/pubspec.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
name: flutter_image
|
||||
version: 4.0.0
|
||||
description: >
|
||||
Image utilities for Flutter: providers, effects, etc
|
||||
author: Flutter Team <flutter-dev@googlegroups.com>
|
||||
homepage: https://github.com/flutter/flutter_image
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
quiver: ^3.0.0
|
||||
test: any
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
flutter: ">=1.10.15-pre.144"
|
152
packages/flutter_image/test/network_test.dart
Normal file
152
packages/flutter_image/test/network_test.dart
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2017, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. 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' show HttpOverrides;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_image/network.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:quiver/testing/async.dart';
|
||||
|
||||
String _imageUrl(String fileName) {
|
||||
return 'http://localhost:11111/$fileName';
|
||||
}
|
||||
|
||||
void main() {
|
||||
AutomatedTestWidgetsFlutterBinding();
|
||||
HttpOverrides.global = null;
|
||||
|
||||
group('NetworkImageWithRetry', () {
|
||||
group('succeeds', () {
|
||||
setUp(() {
|
||||
FlutterError.onError = (FlutterErrorDetails error) {
|
||||
fail('$error');
|
||||
};
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
FlutterError.onError = FlutterError.dumpErrorToConsole;
|
||||
});
|
||||
|
||||
test('loads image from network', () async {
|
||||
final NetworkImageWithRetry subject = NetworkImageWithRetry(
|
||||
_imageUrl('immediate_success.png'),
|
||||
);
|
||||
|
||||
assertThatImageLoadingSucceeds(subject);
|
||||
});
|
||||
|
||||
test('succeeds on successful retry', () async {
|
||||
final NetworkImageWithRetry subject = NetworkImageWithRetry(
|
||||
_imageUrl('error.png'),
|
||||
fetchStrategy: (Uri uri, FetchFailure? failure) async {
|
||||
if (failure == null) {
|
||||
return FetchInstructions.attempt(
|
||||
uri: uri,
|
||||
timeout: const Duration(minutes: 1),
|
||||
);
|
||||
} else {
|
||||
expect(failure.attemptCount, lessThan(2));
|
||||
return FetchInstructions.attempt(
|
||||
uri: Uri.parse(_imageUrl('immediate_success.png')),
|
||||
timeout: const Duration(minutes: 1),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
assertThatImageLoadingSucceeds(subject);
|
||||
});
|
||||
});
|
||||
|
||||
group('fails', () {
|
||||
final List<FlutterErrorDetails> errorLog = <FlutterErrorDetails>[];
|
||||
FakeAsync fakeAsync = FakeAsync();
|
||||
|
||||
setUp(() {
|
||||
FlutterError.onError = errorLog.add;
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
fakeAsync = FakeAsync();
|
||||
errorLog.clear();
|
||||
FlutterError.onError = FlutterError.dumpErrorToConsole;
|
||||
});
|
||||
|
||||
test('retries 6 times then gives up', () async {
|
||||
final dynamic maxAttemptCountReached = expectAsync0(() {});
|
||||
|
||||
int attemptCount = 0;
|
||||
Future<void> onAttempt() async {
|
||||
expect(attemptCount, lessThan(7));
|
||||
if (attemptCount == 6) {
|
||||
maxAttemptCountReached();
|
||||
}
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
fakeAsync.elapse(const Duration(seconds: 60));
|
||||
attemptCount++;
|
||||
}
|
||||
|
||||
final NetworkImageWithRetry subject = NetworkImageWithRetry(
|
||||
_imageUrl('error.png'),
|
||||
fetchStrategy: (Uri uri, FetchFailure? failure) {
|
||||
Timer.run(onAttempt);
|
||||
return fakeAsync.run((FakeAsync fakeAsync) {
|
||||
return NetworkImageWithRetry.defaultFetchStrategy(uri, failure);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assertThatImageLoadingFails(subject, errorLog);
|
||||
});
|
||||
|
||||
test('gives up immediately on non-retriable errors (HTTP 404)', () async {
|
||||
int attemptCount = 0;
|
||||
Future<void> onAttempt() async {
|
||||
expect(attemptCount, lessThan(2));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
fakeAsync.elapse(const Duration(seconds: 60));
|
||||
attemptCount++;
|
||||
}
|
||||
|
||||
final NetworkImageWithRetry subject = NetworkImageWithRetry(
|
||||
_imageUrl('does_not_exist.png'),
|
||||
fetchStrategy: (Uri uri, FetchFailure? failure) {
|
||||
Timer.run(onAttempt);
|
||||
return fakeAsync.run((FakeAsync fakeAsync) {
|
||||
return NetworkImageWithRetry.defaultFetchStrategy(uri, failure);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assertThatImageLoadingFails(subject, errorLog);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void assertThatImageLoadingFails(
|
||||
NetworkImageWithRetry subject, List<FlutterErrorDetails> errorLog) {
|
||||
subject
|
||||
.load(subject, PaintingBinding.instance!.instantiateImageCodec)
|
||||
.addListener(ImageStreamListener(
|
||||
(ImageInfo image, bool synchronousCall) {},
|
||||
onError: expectAsync2((Object error, StackTrace? _) {
|
||||
expect(errorLog.single.exception, isInstanceOf<FetchFailure>());
|
||||
expect(error, isInstanceOf<FetchFailure>());
|
||||
expect(error, equals(errorLog.single.exception));
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
void assertThatImageLoadingSucceeds(NetworkImageWithRetry subject) {
|
||||
subject
|
||||
.load(subject, PaintingBinding.instance!.instantiateImageCodec)
|
||||
.addListener(
|
||||
ImageStreamListener(expectAsync2((ImageInfo image, bool synchronousCall) {
|
||||
expect(image.image.height, 1);
|
||||
expect(image.image.width, 1);
|
||||
})),
|
||||
);
|
||||
}
|
31
packages/flutter_image/test/network_test_server.dart
Normal file
31
packages/flutter_image/test/network_test_server.dart
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2017, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. 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';
|
||||
|
||||
const int _kTestServerPort = 11111;
|
||||
|
||||
Future<void> main() async {
|
||||
final HttpServer testServer = await HttpServer.bind(InternetAddress.loopbackIPv4, _kTestServerPort);
|
||||
await for (HttpRequest request in testServer) {
|
||||
if (request.uri.path.endsWith('/immediate_success.png')) {
|
||||
request.response.add(_kTransparentImage);
|
||||
} else if (request.uri.path.endsWith('/error.png')) {
|
||||
request.response.statusCode = 500;
|
||||
} else {
|
||||
request.response.statusCode = 404;
|
||||
}
|
||||
await request.response.flush();
|
||||
await request.response.close();
|
||||
}
|
||||
}
|
||||
|
||||
const List<int> _kTransparentImage = <int>[
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
|
||||
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
|
||||
0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
|
||||
0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
|
||||
0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
|
||||
];
|
Reference in New Issue
Block a user