mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 07:08:10 +08:00
Merge pull request #348 from stuartmorgan/import-flutter-image
[flutter_image] Import from flutter/flutter_image
This commit is contained in:
@ -39,7 +39,8 @@ task:
|
||||
depends_on:
|
||||
- format+analyze
|
||||
- name: test
|
||||
script: ./script/tool_runner.sh test
|
||||
# Exclude flutter_image; its tests need a test server, so are run via local_tests.sh
|
||||
script: ./script/tool_runner.sh test --exclude=flutter_image
|
||||
depends_on:
|
||||
- format+analyze
|
||||
- name: build-apks+java-test
|
||||
|
@ -37,6 +37,7 @@ These are the available packages in this repository.
|
||||
| [css\_colors](./packages/css_colors/) | [](https://pub.dev/packages/css_colors) |
|
||||
| [extension\_google\_sign\_in\_as\_googleapis\_auth](./packages/extension_google_sign_in_as_googleapis_auth/) | [](https://pub.dev/packages/extension_google_sign_in_as_googleapis_auth) |
|
||||
| [fuchsia\_ctl](./packages/fuchsia_ctl/) | [](https://pub.dev/packages/fuchsia_ctl) |
|
||||
| [flutter\_image](./packages/flutter_image/) | [](https://pub.dev/packages/flutter_image) |
|
||||
| [flutter\_lints](./packages/flutter_lints/) | [](https://pub.dev/packages/flutter_lints) |
|
||||
| [flutter\_markdown](./packages/flutter_markdown/) | [](https://pub.dev/packages/flutter_markdown) |
|
||||
| [multicast\_dns](./packages/multicast_dns/) | [](https://pub.dev/packages/multicast_dns) |
|
||||
|
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.
|
37
packages/flutter_image/CHANGELOG.md
Normal file
37
packages/flutter_image/CHANGELOG.md
Normal file
@ -0,0 +1,37 @@
|
||||
## 4.0.1
|
||||
|
||||
- Moved source to flutter/packages
|
||||
|
||||
## 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.
|
25
packages/flutter_image/LICENSE
Normal file
25
packages/flutter_image/LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright 2013 The Flutter 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.
|
5
packages/flutter_image/lib/flutter_image.dart
Normal file
5
packages/flutter_image/lib/flutter_image.dart
Normal file
@ -0,0 +1,5 @@
|
||||
// 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.
|
||||
|
||||
export 'network.dart';
|
437
packages/flutter_image/lib/network.dart
Normal file
437
packages/flutter_image/lib/network.dart
Normal file
@ -0,0 +1,437 @@
|
||||
// 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 [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,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
19
packages/flutter_image/pubspec.yaml
Normal file
19
packages/flutter_image/pubspec.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
name: flutter_image
|
||||
description: >
|
||||
Image utilities for Flutter: providers, effects, etc
|
||||
repository: https://github.com/flutter/packages/tree/master/packages/flutter_image
|
||||
version: 4.0.1
|
||||
|
||||
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"
|
19
packages/flutter_image/run_tests.sh
Executable file
19
packages/flutter_image/run_tests.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# 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.
|
||||
|
||||
# Fast fail the script on failures.
|
||||
set -e
|
||||
# Print commands to stdout
|
||||
set -x
|
||||
|
||||
flutter packages get
|
||||
flutter analyze lib/ test/
|
||||
|
||||
dart test/network_test_server.dart &
|
||||
SERVER_PID=$!
|
||||
sleep 2
|
||||
|
||||
flutter test
|
||||
kill $SERVER_PID
|
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 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.
|
||||
|
||||
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);
|
||||
})),
|
||||
);
|
||||
}
|
91
packages/flutter_image/test/network_test_server.dart
Normal file
91
packages/flutter_image/test/network_test_server.dart
Normal file
@ -0,0 +1,91 @@
|
||||
// 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.
|
||||
|
||||
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 (final 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