mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 15:23:25 +08:00
Migrates to null safety (#22)
This commit is contained in:
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 4.0.0
|
||||||
|
|
||||||
|
- Migrates to null safety
|
||||||
|
- **Breaking change**: `NetworkImageWithRetry.load` now throws a `FetchFailure` if the fetched image data is zero bytes.
|
||||||
|
|
||||||
## 3.0.0
|
## 3.0.0
|
||||||
|
|
||||||
* **Breaking change**. Updates for Flutter 1.10.15.
|
* **Breaking change**. Updates for Flutter 1.10.15.
|
||||||
|
@ -29,10 +29,11 @@ class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
|||||||
/// Creates an object that fetches the image at the given [url].
|
/// Creates an object that fetches the image at the given [url].
|
||||||
///
|
///
|
||||||
/// The arguments must not be null.
|
/// The arguments must not be null.
|
||||||
const NetworkImageWithRetry(this.url, { this.scale = 1.0, this.fetchStrategy = defaultFetchStrategy })
|
const NetworkImageWithRetry(
|
||||||
: assert(url != null),
|
this.url, {
|
||||||
assert(scale != null),
|
this.scale = 1.0,
|
||||||
assert(fetchStrategy != null);
|
this.fetchStrategy = defaultFetchStrategy,
|
||||||
|
});
|
||||||
|
|
||||||
/// The HTTP client used to download images.
|
/// The HTTP client used to download images.
|
||||||
static final io.HttpClient _client = io.HttpClient();
|
static final io.HttpClient _client = io.HttpClient();
|
||||||
@ -57,10 +58,12 @@ class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
|||||||
/// This indirection is necessary because [defaultFetchStrategy] is used as
|
/// This indirection is necessary because [defaultFetchStrategy] is used as
|
||||||
/// the default constructor argument value, which requires that it be a const
|
/// the default constructor argument value, which requires that it be a const
|
||||||
/// expression.
|
/// expression.
|
||||||
static final FetchStrategy _defaultFetchStrategyFunction = const FetchStrategyBuilder().build();
|
static final FetchStrategy _defaultFetchStrategyFunction =
|
||||||
|
const FetchStrategyBuilder().build();
|
||||||
|
|
||||||
/// The [FetchStrategy] that [NetworkImageWithRetry] uses by default.
|
/// The [FetchStrategy] that [NetworkImageWithRetry] uses by default.
|
||||||
static Future<FetchInstructions> defaultFetchStrategy(Uri uri, FetchFailure failure) {
|
static Future<FetchInstructions> defaultFetchStrategy(
|
||||||
|
Uri uri, FetchFailure? failure) {
|
||||||
return _defaultFetchStrategyFunction(uri, failure);
|
return _defaultFetchStrategyFunction(uri, failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,37 +74,34 @@ class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter load(NetworkImageWithRetry key, DecoderCallback decode) {
|
ImageStreamCompleter load(NetworkImageWithRetry key, DecoderCallback decode) {
|
||||||
return OneFrameImageStreamCompleter(
|
return OneFrameImageStreamCompleter(_loadWithRetry(key, decode),
|
||||||
_loadWithRetry(key, decode),
|
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('Image provider: $this');
|
yield ErrorDescription('Image provider: $this');
|
||||||
yield ErrorDescription('Image key: $key');
|
yield ErrorDescription('Image key: $key');
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _debugCheckInstructions(FetchInstructions instructions) {
|
void _debugCheckInstructions(FetchInstructions? instructions) {
|
||||||
assert(() {
|
assert(() {
|
||||||
if (instructions == null) {
|
if (instructions == null) {
|
||||||
if (fetchStrategy == defaultFetchStrategy) {
|
if (fetchStrategy == defaultFetchStrategy) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'The default FetchStrategy returned null FetchInstructions. This\n'
|
'The default FetchStrategy returned null FetchInstructions. This\n'
|
||||||
'is likely a bug in $runtimeType. Please file a bug at\n'
|
'is likely a bug in $runtimeType. Please file a bug at\n'
|
||||||
'https://github.com/flutter/flutter/issues.'
|
'https://github.com/flutter/flutter/issues.');
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'The custom FetchStrategy used to fetch $url returned null\n'
|
'The custom FetchStrategy used to fetch $url returned null\n'
|
||||||
'FetchInstructions. FetchInstructions must never be null, but\n'
|
'FetchInstructions. FetchInstructions must never be null, but\n'
|
||||||
'instead instruct to either make another fetch attempt or give up.'
|
'instead instruct to either make another fetch attempt or give up.');
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ImageInfo> _loadWithRetry(NetworkImageWithRetry key, DecoderCallback decode) async {
|
Future<ImageInfo> _loadWithRetry(
|
||||||
|
NetworkImageWithRetry key, DecoderCallback decode) async {
|
||||||
assert(key == this);
|
assert(key == this);
|
||||||
|
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
@ -109,16 +109,19 @@ class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
|||||||
FetchInstructions instructions = await fetchStrategy(resolved, null);
|
FetchInstructions instructions = await fetchStrategy(resolved, null);
|
||||||
_debugCheckInstructions(instructions);
|
_debugCheckInstructions(instructions);
|
||||||
int attemptCount = 0;
|
int attemptCount = 0;
|
||||||
FetchFailure lastFailure;
|
FetchFailure? lastFailure;
|
||||||
|
|
||||||
while (!instructions.shouldGiveUp) {
|
while (!instructions.shouldGiveUp) {
|
||||||
attemptCount += 1;
|
attemptCount += 1;
|
||||||
io.HttpClientRequest request;
|
io.HttpClientRequest? request;
|
||||||
try {
|
try {
|
||||||
request = await _client.getUrl(instructions.uri).timeout(instructions.timeout);
|
request = await _client
|
||||||
final io.HttpClientResponse response = await request.close().timeout(instructions.timeout);
|
.getUrl(instructions.uri)
|
||||||
|
.timeout(instructions.timeout);
|
||||||
|
final io.HttpClientResponse response =
|
||||||
|
await request.close().timeout(instructions.timeout);
|
||||||
|
|
||||||
if (response == null || response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw FetchFailure._(
|
throw FetchFailure._(
|
||||||
totalDuration: stopwatch.elapsed,
|
totalDuration: stopwatch.elapsed,
|
||||||
attemptCount: attemptCount,
|
attemptCount: attemptCount,
|
||||||
@ -126,21 +129,25 @@ class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _Uint8ListBuilder builder = await response.fold(
|
final _Uint8ListBuilder builder = await response
|
||||||
_Uint8ListBuilder(),
|
.fold(
|
||||||
|
_Uint8ListBuilder(),
|
||||||
(_Uint8ListBuilder buffer, List<int> bytes) => buffer..add(bytes),
|
(_Uint8ListBuilder buffer, List<int> bytes) => buffer..add(bytes),
|
||||||
).timeout(instructions.timeout);
|
)
|
||||||
|
.timeout(instructions.timeout);
|
||||||
|
|
||||||
final Uint8List bytes = builder.data;
|
final Uint8List bytes = builder.data;
|
||||||
|
|
||||||
if (bytes.lengthInBytes == 0)
|
if (bytes.lengthInBytes == 0) {
|
||||||
return null;
|
throw FetchFailure._(
|
||||||
|
totalDuration: stopwatch.elapsed,
|
||||||
|
attemptCount: attemptCount,
|
||||||
|
httpStatusCode: response.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final ui.Codec codec = await decode(bytes);
|
final ui.Codec codec = await decode(bytes);
|
||||||
final ui.Image image = (await codec.getNextFrame()).image;
|
final ui.Image image = (await codec.getNextFrame()).image;
|
||||||
if (image == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ImageInfo(
|
return ImageInfo(
|
||||||
image: image,
|
image: image,
|
||||||
scale: key.scale,
|
scale: key.scale,
|
||||||
@ -159,27 +166,31 @@ class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instructions.alternativeImage != null)
|
if (instructions.alternativeImage != null) {
|
||||||
return instructions.alternativeImage;
|
return instructions.alternativeImage!;
|
||||||
|
}
|
||||||
|
|
||||||
assert(lastFailure != null);
|
assert(lastFailure != null);
|
||||||
|
|
||||||
FlutterError.onError(FlutterErrorDetails(
|
if (FlutterError.onError != null) {
|
||||||
exception: lastFailure,
|
FlutterError.onError!(FlutterErrorDetails(
|
||||||
library: 'package:flutter_image',
|
exception: lastFailure!,
|
||||||
context: ErrorDescription('$runtimeType failed to load ${instructions.uri}'),
|
library: 'package:flutter_image',
|
||||||
));
|
context:
|
||||||
|
ErrorDescription('$runtimeType failed to load ${instructions.uri}'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
throw lastFailure!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
if (other.runtimeType != runtimeType)
|
if (other.runtimeType != runtimeType) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
final NetworkImageWithRetry typedOther = other;
|
final NetworkImageWithRetry typedOther = other;
|
||||||
return url == typedOther.url
|
return url == typedOther.url && scale == typedOther.scale;
|
||||||
&& scale == typedOther.scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -207,26 +218,26 @@ class NetworkImageWithRetry extends ImageProvider<NetworkImageWithRetry> {
|
|||||||
/// [NetworkImageWithRetry] to try again.
|
/// [NetworkImageWithRetry] to try again.
|
||||||
///
|
///
|
||||||
/// See [NetworkImageWithRetry.defaultFetchStrategy] for an example.
|
/// See [NetworkImageWithRetry.defaultFetchStrategy] for an example.
|
||||||
typedef FetchStrategy = Future<FetchInstructions> Function(Uri uri, FetchFailure failure);
|
typedef FetchStrategy = Future<FetchInstructions> Function(
|
||||||
|
Uri uri, FetchFailure? failure);
|
||||||
|
|
||||||
/// Instructions [NetworkImageWithRetry] uses to fetch the image.
|
/// Instructions [NetworkImageWithRetry] uses to fetch the image.
|
||||||
@immutable
|
@immutable
|
||||||
class FetchInstructions {
|
class FetchInstructions {
|
||||||
/// Instructs [NetworkImageWithRetry] to give up trying to download the image.
|
/// Instructs [NetworkImageWithRetry] to give up trying to download the image.
|
||||||
const FetchInstructions.giveUp({
|
const FetchInstructions.giveUp({
|
||||||
@required this.uri,
|
required this.uri,
|
||||||
this.alternativeImage,
|
this.alternativeImage,
|
||||||
})
|
}) : shouldGiveUp = true,
|
||||||
: shouldGiveUp = true,
|
timeout = Duration.zero;
|
||||||
timeout = null;
|
|
||||||
|
|
||||||
/// Instructs [NetworkImageWithRetry] to attempt to download the image from
|
/// Instructs [NetworkImageWithRetry] to attempt to download the image from
|
||||||
/// the given [uri] and [timeout] if it takes too long.
|
/// the given [uri] and [timeout] if it takes too long.
|
||||||
const FetchInstructions.attempt({
|
const FetchInstructions.attempt({
|
||||||
@required this.uri,
|
required this.uri,
|
||||||
@required this.timeout,
|
required this.timeout,
|
||||||
}) : shouldGiveUp = false,
|
}) : shouldGiveUp = false,
|
||||||
alternativeImage = null;
|
alternativeImage = null;
|
||||||
|
|
||||||
/// Instructs to give up trying.
|
/// Instructs to give up trying.
|
||||||
///
|
///
|
||||||
@ -241,16 +252,16 @@ class FetchInstructions {
|
|||||||
final Uri uri;
|
final Uri uri;
|
||||||
|
|
||||||
/// Instructs to give up and use this image instead.
|
/// Instructs to give up and use this image instead.
|
||||||
final Future<ImageInfo> alternativeImage;
|
final Future<ImageInfo>? alternativeImage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '$runtimeType(\n'
|
return '$runtimeType(\n'
|
||||||
' shouldGiveUp: $shouldGiveUp\n'
|
' shouldGiveUp: $shouldGiveUp\n'
|
||||||
' timeout: $timeout\n'
|
' timeout: $timeout\n'
|
||||||
' uri: $uri\n'
|
' uri: $uri\n'
|
||||||
' alternativeImage?: ${alternativeImage != null ? 'yes' : 'no'}\n'
|
' alternativeImage?: ${alternativeImage != null ? 'yes' : 'no'}\n'
|
||||||
')';
|
')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,12 +269,11 @@ class FetchInstructions {
|
|||||||
@immutable
|
@immutable
|
||||||
class FetchFailure implements Exception {
|
class FetchFailure implements Exception {
|
||||||
const FetchFailure._({
|
const FetchFailure._({
|
||||||
@required this.totalDuration,
|
required this.totalDuration,
|
||||||
@required this.attemptCount,
|
required this.attemptCount,
|
||||||
this.httpStatusCode,
|
this.httpStatusCode,
|
||||||
this.originalException,
|
this.originalException,
|
||||||
}) : assert(totalDuration != null),
|
}) : assert(attemptCount > 0);
|
||||||
assert(attemptCount > 0);
|
|
||||||
|
|
||||||
/// The total amount of time it has taken so far to download the image.
|
/// The total amount of time it has taken so far to download the image.
|
||||||
final Duration totalDuration;
|
final Duration totalDuration;
|
||||||
@ -276,7 +286,7 @@ class FetchFailure implements Exception {
|
|||||||
final int attemptCount;
|
final int attemptCount;
|
||||||
|
|
||||||
/// HTTP status code, such as 500.
|
/// HTTP status code, such as 500.
|
||||||
final int httpStatusCode;
|
final int? httpStatusCode;
|
||||||
|
|
||||||
/// The exception that caused the fetch failure.
|
/// The exception that caused the fetch failure.
|
||||||
final dynamic originalException;
|
final dynamic originalException;
|
||||||
@ -294,7 +304,7 @@ class FetchFailure implements Exception {
|
|||||||
|
|
||||||
/// An indefinitely growing builder of a [Uint8List].
|
/// An indefinitely growing builder of a [Uint8List].
|
||||||
class _Uint8ListBuilder {
|
class _Uint8ListBuilder {
|
||||||
static const int _kInitialSize = 100000; // 100KB-ish
|
static const int _kInitialSize = 100000; // 100KB-ish
|
||||||
|
|
||||||
int _usedLength = 0;
|
int _usedLength = 0;
|
||||||
Uint8List _buffer = Uint8List(_kInitialSize);
|
Uint8List _buffer = Uint8List(_kInitialSize);
|
||||||
@ -344,20 +354,16 @@ class FetchStrategyBuilder {
|
|||||||
this.maxAttempts = 5,
|
this.maxAttempts = 5,
|
||||||
this.initialPauseBetweenRetries = const Duration(seconds: 1),
|
this.initialPauseBetweenRetries = const Duration(seconds: 1),
|
||||||
this.exponentialBackoffMultiplier = 2,
|
this.exponentialBackoffMultiplier = 2,
|
||||||
this.transientHttpStatusCodePredicate = defaultTransientHttpStatusCodePredicate,
|
this.transientHttpStatusCodePredicate =
|
||||||
}) : assert(timeout != null),
|
defaultTransientHttpStatusCodePredicate,
|
||||||
assert(totalFetchTimeout != null),
|
});
|
||||||
assert(maxAttempts != null),
|
|
||||||
assert(initialPauseBetweenRetries != null),
|
|
||||||
assert(exponentialBackoffMultiplier != null),
|
|
||||||
assert(transientHttpStatusCodePredicate != null);
|
|
||||||
|
|
||||||
/// A list of HTTP status codes that can generally be retried.
|
/// 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
|
/// You may want to use a different list depending on the needs of your
|
||||||
/// application.
|
/// application.
|
||||||
static const List<int> defaultTransientHttpStatusCodes = <int>[
|
static const List<int> defaultTransientHttpStatusCodes = <int>[
|
||||||
0, // Network error
|
0, // Network error
|
||||||
408, // Request timeout
|
408, // Request timeout
|
||||||
500, // Internal server error
|
500, // Internal server error
|
||||||
502, // Bad gateway
|
502, // Bad gateway
|
||||||
@ -395,7 +401,7 @@ class FetchStrategyBuilder {
|
|||||||
/// Builds a [FetchStrategy] that operates using the properties of this
|
/// Builds a [FetchStrategy] that operates using the properties of this
|
||||||
/// builder.
|
/// builder.
|
||||||
FetchStrategy build() {
|
FetchStrategy build() {
|
||||||
return (Uri uri, FetchFailure failure) async {
|
return (Uri uri, FetchFailure? failure) async {
|
||||||
if (failure == null) {
|
if (failure == null) {
|
||||||
// First attempt. Just load.
|
// First attempt. Just load.
|
||||||
return FetchInstructions.attempt(
|
return FetchInstructions.attempt(
|
||||||
@ -404,18 +410,21 @@ class FetchStrategyBuilder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool isRetriableFailure = transientHttpStatusCodePredicate(failure.httpStatusCode) ||
|
final bool isRetriableFailure = (failure.httpStatusCode != null &&
|
||||||
|
transientHttpStatusCodePredicate(failure.httpStatusCode!)) ||
|
||||||
failure.originalException is io.SocketException;
|
failure.originalException is io.SocketException;
|
||||||
|
|
||||||
// If cannot retry, give up.
|
// If cannot retry, give up.
|
||||||
if (!isRetriableFailure || // retrying will not help
|
if (!isRetriableFailure || // retrying will not help
|
||||||
failure.totalDuration > totalFetchTimeout || // taking too long
|
failure.totalDuration > totalFetchTimeout || // taking too long
|
||||||
failure.attemptCount > maxAttempts) { // too many attempts
|
failure.attemptCount > maxAttempts) {
|
||||||
|
// too many attempts
|
||||||
return FetchInstructions.giveUp(uri: uri);
|
return FetchInstructions.giveUp(uri: uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exponential back-off.
|
// Exponential back-off.
|
||||||
final Duration pauseBetweenRetries = initialPauseBetweenRetries * math.pow(exponentialBackoffMultiplier, failure.attemptCount - 1);
|
final Duration pauseBetweenRetries = initialPauseBetweenRetries *
|
||||||
|
math.pow(exponentialBackoffMultiplier, failure.attemptCount - 1);
|
||||||
await Future<void>.delayed(pauseBetweenRetries);
|
await Future<void>.delayed(pauseBetweenRetries);
|
||||||
|
|
||||||
// Retry.
|
// Retry.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: flutter_image
|
name: flutter_image
|
||||||
version: 3.0.0
|
version: 4.0.0
|
||||||
description: >
|
description: >
|
||||||
Image utilities for Flutter: providers, effects, etc
|
Image utilities for Flutter: providers, effects, etc
|
||||||
author: Flutter Team <flutter-dev@googlegroups.com>
|
author: Flutter Team <flutter-dev@googlegroups.com>
|
||||||
@ -12,9 +12,9 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
quiver: '>=0.24.0 <3.0.0'
|
quiver: ^3.0.0
|
||||||
test: any
|
test: any
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.28.0 <3.0.0"
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
flutter: ">=1.10.15-pre.144"
|
flutter: ">=1.10.15-pre.144"
|
||||||
|
@ -19,122 +19,134 @@ void main() {
|
|||||||
HttpOverrides.global = null;
|
HttpOverrides.global = null;
|
||||||
|
|
||||||
group('NetworkImageWithRetry', () {
|
group('NetworkImageWithRetry', () {
|
||||||
setUp(() {
|
group('succeeds', () {
|
||||||
FlutterError.onError = (FlutterErrorDetails error) {
|
setUp(() {
|
||||||
fail('$error');
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
group('fails', () {
|
||||||
FlutterError.onError = FlutterError.dumpErrorToConsole;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('loads image from network', () async {
|
|
||||||
final NetworkImageWithRetry subject = NetworkImageWithRetry(
|
|
||||||
_imageUrl('immediate_success.png'),
|
|
||||||
);
|
|
||||||
|
|
||||||
subject.load(subject, PaintingBinding.instance.instantiateImageCodec).addListener(
|
|
||||||
ImageStreamListener(expectAsync2((ImageInfo image, bool synchronousCall) {
|
|
||||||
expect(image.image.height, 1);
|
|
||||||
expect(image.image.width, 1);
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('retries 6 times then gives up', () async {
|
|
||||||
final List<FlutterErrorDetails> errorLog = <FlutterErrorDetails>[];
|
final List<FlutterErrorDetails> errorLog = <FlutterErrorDetails>[];
|
||||||
FlutterError.onError = errorLog.add;
|
FakeAsync fakeAsync = FakeAsync();
|
||||||
|
|
||||||
final FakeAsync fakeAsync = FakeAsync();
|
setUp(() {
|
||||||
final dynamic maxAttemptCountReached = expectAsync0(() {});
|
FlutterError.onError = errorLog.add;
|
||||||
|
});
|
||||||
|
|
||||||
int attemptCount = 0;
|
tearDown(() {
|
||||||
Future<void> onAttempt() async {
|
fakeAsync = FakeAsync();
|
||||||
expect(attemptCount, lessThan(7));
|
errorLog.clear();
|
||||||
if (attemptCount == 6) {
|
FlutterError.onError = FlutterError.dumpErrorToConsole;
|
||||||
maxAttemptCountReached();
|
});
|
||||||
}
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
fakeAsync.elapse(const Duration(seconds: 60));
|
|
||||||
attemptCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
final NetworkImageWithRetry subject = NetworkImageWithRetry(
|
test('retries 6 times then gives up', () async {
|
||||||
_imageUrl('error.png'),
|
final dynamic maxAttemptCountReached = expectAsync0(() {});
|
||||||
fetchStrategy: (Uri uri, FetchFailure failure) {
|
|
||||||
Timer.run(onAttempt);
|
|
||||||
return fakeAsync.run((FakeAsync fakeAsync) {
|
|
||||||
return NetworkImageWithRetry.defaultFetchStrategy(uri, failure);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
subject.load(subject, PaintingBinding.instance.instantiateImageCodec).addListener(
|
int attemptCount = 0;
|
||||||
ImageStreamListener(expectAsync2((ImageInfo image, bool synchronousCall) {
|
Future<void> onAttempt() async {
|
||||||
expect(errorLog.single.exception, isInstanceOf<FetchFailure>());
|
expect(attemptCount, lessThan(7));
|
||||||
expect(image, null);
|
if (attemptCount == 6) {
|
||||||
})),
|
maxAttemptCountReached();
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('gives up immediately on non-retriable errors (HTTP 404)', () async {
|
|
||||||
final List<FlutterErrorDetails> errorLog = <FlutterErrorDetails>[];
|
|
||||||
FlutterError.onError = errorLog.add;
|
|
||||||
|
|
||||||
final FakeAsync fakeAsync = FakeAsync();
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
subject.load(subject, PaintingBinding.instance.instantiateImageCodec).addListener(
|
|
||||||
ImageStreamListener(expectAsync2((ImageInfo image, bool synchronousCall) {
|
|
||||||
expect(errorLog.single.exception, isInstanceOf<FetchFailure>());
|
|
||||||
expect(image, null);
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
await Future<void>.delayed(Duration.zero);
|
||||||
);
|
fakeAsync.elapse(const Duration(seconds: 60));
|
||||||
|
attemptCount++;
|
||||||
|
}
|
||||||
|
|
||||||
subject.load(subject, PaintingBinding.instance.instantiateImageCodec).addListener(
|
final NetworkImageWithRetry subject = NetworkImageWithRetry(
|
||||||
ImageStreamListener(expectAsync2((ImageInfo image, bool synchronousCall) {
|
_imageUrl('error.png'),
|
||||||
expect(image.image.height, 1);
|
fetchStrategy: (Uri uri, FetchFailure? failure) {
|
||||||
expect(image.image.width, 1);
|
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);
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user