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:
|
depends_on:
|
||||||
- format+analyze
|
- format+analyze
|
||||||
- name: test
|
- 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:
|
depends_on:
|
||||||
- format+analyze
|
- format+analyze
|
||||||
- name: build-apks+java-test
|
- 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) |
|
| [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) |
|
| [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) |
|
| [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\_lints](./packages/flutter_lints/) | [](https://pub.dev/packages/flutter_lints) |
|
||||||
| [flutter\_markdown](./packages/flutter_markdown/) | [](https://pub.dev/packages/flutter_markdown) |
|
| [flutter\_markdown](./packages/flutter_markdown/) | [](https://pub.dev/packages/flutter_markdown) |
|
||||||
| [multicast\_dns](./packages/multicast_dns/) | [](https://pub.dev/packages/multicast_dns) |
|
| [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