Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created February 27, 2020 16:57
Show Gist options
  • Save slightfoot/f79e2b6325cbf8b531e59583147ae7eb to your computer and use it in GitHub Desktop.
Save slightfoot/f79e2b6325cbf8b531e59583147ae7eb to your computer and use it in GitHub Desktop.
Cached Network Image
// MIT License
//
// Copyright (c) 2020 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'dart:io' as io;
import 'dart:math' as math;
import 'dart:ui' as ui show hashValues, Codec;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
class CachedNetworkImage extends StatefulWidget {
const CachedNetworkImage(
this.imageUrl, {
Key key,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.backgroundColor = Colors.white,
}) : assert(imageUrl != null),
assert(alignment != null),
assert(backgroundColor != null),
super(key: key);
final String imageUrl;
final double width;
final double height;
final BoxFit fit;
final AlignmentGeometry alignment;
final Color backgroundColor;
@override
_CachedNetworkImageState createState() => _CachedNetworkImageState();
}
class _CachedNetworkImageState extends State<CachedNetworkImage> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
if (widget.imageUrl.isEmpty) {
return DecoratedBox(
decoration: BoxDecoration(color: widget.backgroundColor),
child: const SizedBox.expand(),
);
} else {
return Image(
image: CachedNetworkImageProvider(widget.imageUrl),
width: widget.width,
height: widget.height,
fit: widget.fit,
alignment: widget.alignment,
frameBuilder: (BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) {
return child;
}
return AnimatedOpacity(
child: child,
opacity: frame == null ? 0.0 : 1.0,
duration: const Duration(milliseconds: 700),
curve: Curves.easeOut,
);
},
);
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('imageUrl', widget.imageUrl));
properties.add(DoubleProperty('width', widget.width, defaultValue: null));
properties.add(DoubleProperty('height', widget.height, defaultValue: null));
properties.add(EnumProperty<BoxFit>('fit', widget.fit, defaultValue: null));
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', widget.alignment, defaultValue: null));
}
}
class CachedNetworkImageProvider extends ImageProvider<NetworkImage> implements NetworkImage {
const CachedNetworkImageProvider(this.url, {this.scale = 1.0, this.headers})
: assert(url != null),
assert(scale != null);
@override
final String url;
@override
final double scale;
@override
final Map<String, String> headers;
@override
Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<NetworkImage>('Image key', key),
];
},
);
}
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
) async {
try {
assert(key == this);
final file = await getUrlWithRetry(key.url);
final bytes = await file.readAsBytes();
if (bytes.lengthInBytes == 0) {
throw Exception('NetworkImage is an empty file: ${key.url}');
}
return decode(bytes);
} finally {
await chunkEvents.close();
}
}
Future<io.File> getUrlWithRetry(String url, {int maxRetries = 5}) async {
final Stopwatch stopwatch = Stopwatch()..start();
try {
for (var attempt = 0; attempt < maxRetries; attempt++) {
try {
return await DefaultCacheManager().getSingleFile(url);
} catch (e) {
final statusCode = e is HttpExceptionWithStatus ? e.statusCode : 0;
final bool isRetryableFailure = _transientHttpStatusCodes.contains(statusCode) || e is io.SocketException;
if (!isRetryableFailure || // retrying will not help
stopwatch.elapsed > const Duration(minutes: 1) || // taking too long
attempt > maxRetries) // too many attempts
{
break;
}
await Future<void>.delayed(const Duration(seconds: 1) * math.pow(2, attempt));
}
}
} finally {
stopwatch.stop();
}
return throw Exception('Failed to load image with retries $maxRetries: $url');
}
@override
bool operator ==(dynamic other) {
if (other is CachedNetworkImageProvider) {
return url == other.url && scale == other.scale;
}
return false;
}
@override
int get hashCode => ui.hashValues(url, scale);
@override
String toString() => '$runtimeType("$url", scale: $scale)';
/// 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> _transientHttpStatusCodes = <int>[
0, // Network error
408, // Request timeout
500, // Internal server error
502, // Bad gateway
503, // Service unavailable
504 // Gateway timeout
];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment