Merge pull request #348 from stuartmorgan/import-flutter-image

[flutter_image] Import from flutter/flutter_image
This commit is contained in:
stuartmorgan
2021-05-13 13:31:48 -04:00
committed by GitHub
16 changed files with 876 additions and 1 deletions

View File

@ -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

View File

@ -37,6 +37,7 @@ These are the available packages in this repository.
| [css\_colors](./packages/css_colors/) | [![pub package](https://img.shields.io/pub/v/css_colors.svg)](https://pub.dev/packages/css_colors) |
| [extension\_google\_sign\_in\_as\_googleapis\_auth](./packages/extension_google_sign_in_as_googleapis_auth/) | [![pub package](https://img.shields.io/pub/v/extension_google_sign_in_as_googleapis_auth.svg)](https://pub.dev/packages/extension_google_sign_in_as_googleapis_auth) |
| [fuchsia\_ctl](./packages/fuchsia_ctl/) | [![pub package](https://img.shields.io/pub/v/fuchsia_ctl.svg)](https://pub.dev/packages/fuchsia_ctl) |
| [flutter\_image](./packages/flutter_image/) | [![pub package](https://img.shields.io/pub/v/flutter_image.svg)](https://pub.dev/packages/flutter_image) |
| [flutter\_lints](./packages/flutter_lints/) | [![pub package](https://img.shields.io/pub/v/flutter_lints.svg)](https://pub.dev/packages/flutter_lints) |
| [flutter\_markdown](./packages/flutter_markdown/) | [![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dev/packages/flutter_markdown) |
| [multicast\_dns](./packages/multicast_dns/) | [![pub package](https://img.shields.io/pub/v/multicast_dns.svg)](https://pub.dev/packages/multicast_dns) |

11
packages/flutter_image/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
.buildlog
.DS_Store
.idea/libraries/*
.idea/vcs.xml
.idea/workspace.xml
.pub/
.settings/
build/
packages
.packages
pubspec.lock

View 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>

View 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>

View 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

View 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.

View 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.

View 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.

View 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.

View 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';

View 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,
);
};
}
}

View 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"

View 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

View 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);
})),
);
}

View 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,
];