Skip to content

Commit

Permalink
Added functionality to clean the cache
Browse files Browse the repository at this point in the history
  • Loading branch information
renefloor committed Dec 27, 2017
1 parent 872d656 commit e1711d0
Showing 1 changed file with 169 additions and 56 deletions.
225 changes: 169 additions & 56 deletions lib/cached_network_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import 'package:synchronized/synchronized.dart';
import 'package:uuid/uuid.dart';
import 'dart:ui' as ui show Image, decodeImageFromList;


/**
* CachedNetworkImage for Flutter
*
Expand All @@ -32,11 +31,12 @@ class CachedNetworkImage extends StatefulWidget {
/// Image is downloaded from the url
/// Placeholder is shown while the image is being downloaded
CachedNetworkImage(
this.imageUrl, {
Key key,
this.fit,
this.placeholder,
}): super(key: key);
this.imageUrl, {
Key key,
this.fit,
this.placeholder,
})
: super(key: key);

@override
_CachedNetworkImageState createState() => new _CachedNetworkImageState();
Expand Down Expand Up @@ -71,6 +71,10 @@ class _CachedNetworkImageState extends State<CachedNetworkImage> {
}

class CacheManager {
static Duration inbetweenCleans = new Duration(days: 7);
static Duration maxAgeCacheObject = new Duration(days: 30);
static int maxNrOfCacheObjects = 200;

static CacheManager _instance;
static Future<CacheManager> getInstance() async {
if (_instance == null) {
Expand All @@ -86,81 +90,174 @@ class CacheManager {

CacheManager._();

SharedPreferences prefs;
Map<String, CacheObject> cacheData;
SharedPreferences _prefs;
Map<String, CacheObject> _cacheData;
DateTime lastCacheClean;

static Object _lock = new Object();

///Shared preferences is used to keep track of the information about the files
_init() async {
prefs = await SharedPreferences.getInstance();
_prefs = await SharedPreferences.getInstance();

//get saved cache data from shared prefs
var jsonCacheString = prefs.getString("lib_cached_image_data");
cacheData = new Map();
var jsonCacheString = _prefs.getString("lib_cached_image_data");
_cacheData = new Map();
if (jsonCacheString != null) {
Map jsonCache = JSON.decode(jsonCacheString);
jsonCache.forEach((key, data) {
cacheData[key] = new CacheObject.fromMap(data);
_cacheData[key] = new CacheObject.fromMap(key, data);
});
}

// Get data about when the last clean action has been performed
var cleanMillis = _prefs.getInt("lib_cached_image_data_last_clean");
if (cleanMillis != null) {
lastCacheClean = new DateTime.fromMillisecondsSinceEpoch(cleanMillis);
} else {
lastCacheClean = new DateTime.now();
_prefs.setInt("lib_cached_image_data_last_clean",
lastCacheClean.millisecondsSinceEpoch);
}
}

bool _isStoringData = false;
bool _shouldStoreDataAgain = false;
Object _storeLock = new Object();
///Store all data to shared preferences
_save() async {

if(!(await _canSave())){
return;
}

await synchronized(_lock, () async {
await _cleanCache();
await _saveDataInPrefs();
});
}

Future<bool> _canSave() async {
return await synchronized(_storeLock, (){
if(_isStoringData){
_shouldStoreDataAgain = true;
return false;
}
_isStoringData = true;
return true;
});
}

Future<bool> _shouldSaveAgain() async{
return await synchronized(_storeLock, (){
if(_shouldStoreDataAgain){
_shouldStoreDataAgain = false;
return true;
}
_isStoringData = false;
return false;
});
}

_saveDataInPrefs() async{
Map json = new Map();
await synchronized(_lock, () {
cacheData.forEach((key, cache) {
json[key] = cache._map;
});
_cacheData.forEach((key, cache) {
json[key] = cache._map;
});
prefs.setString("lib_cached_image_data", JSON.encode(json));
_prefs.setString("lib_cached_image_data", JSON.encode(json));

if(await _shouldSaveAgain()){
await _saveDataInPrefs();
}
}

_cleanCache({force: false}) async {
var sinceLastClean = new DateTime.now().difference(lastCacheClean);

if (force ||
sinceLastClean > inbetweenCleans ||
_cacheData.length > maxNrOfCacheObjects) {
var oldestDateAllowed = new DateTime.now().subtract(maxAgeCacheObject);

//Remove old objects
var oldValues =
_cacheData.values.where((c) => c.touched.isBefore(oldestDateAllowed));
for (var oldValue in oldValues) {
await _removeFile(oldValue);
}

//Remove oldest objects when cache contains to many items
if (_cacheData.length > maxNrOfCacheObjects) {
var allValues = _cacheData.values.toList();
allValues.sort((c1, c2) => c1.touched.compareTo(c2.touched));
for (var i = allValues.length; i > maxNrOfCacheObjects; i--) {
var lastItem = allValues[i - 1];
await _removeFile(lastItem);
}
}

lastCacheClean = new DateTime.now();
_prefs.setInt("lib_cached_image_data_last_clean",
lastCacheClean.millisecondsSinceEpoch);
}
}

_removeFile(CacheObject cacheObject) async {
var file = new File(cacheObject.filePath);
if (await file.exists()) {
file.delete();
}
_cacheData.remove(cacheObject.url);
}

///Get the file from the cache or online. Depending on availability and age
Future<File> getFile(String url) async {
if (!cacheData.containsKey(url)) {
if (!_cacheData.containsKey(url)) {
await synchronized(_lock, () {
if (!cacheData.containsKey(url)) {
cacheData[url] = new CacheObject();
if (!_cacheData.containsKey(url)) {
_cacheData[url] = new CacheObject(url);
}
});
}

var cacheObject = cacheData[url];
var cacheObject = _cacheData[url];
await synchronized(cacheObject.lock, () async {
// Set touched date to show that this object is being used recently
cacheObject.touch();

//If we have never downloaded this file, do download
if (cacheObject.filePath == null) {
cacheData[url] = await downloadFile(url);
_cacheData[url] = await downloadFile(url);
return;
}
//If file is removed from the cache storage, download again
var cachedFile = new File(cacheObject.filePath);
var cachedFileExists = await cachedFile.exists();
if (!cachedFileExists) {
cacheData[url] = await downloadFile(url, path: cacheObject.filePath);
_cacheData[url] = await downloadFile(url, path: cacheObject.filePath);
return;
}
//If file is old, download if server has newer one
if (cacheObject.validTill == null ||
cacheObject.validTill.isBefore(new DateTime.now())) {
var newCacheData = await downloadFile(url,
path: cacheObject.filePath, eTag: cacheObject.eTag);
if(newCacheData != null){
cacheData[url] = newCacheData;
if (newCacheData != null) {
_cacheData[url] = newCacheData;
}
return;
}
});

//If non of the above is true, than we don't have to download anything.
_save();
return new File(cacheData[url].filePath);
return new File(_cacheData[url].filePath);
}

///Download the file from the url
Future<CacheObject> downloadFile(String url,
{String path, String eTag}) async {
var newCache = new CacheObject();
var newCache = new CacheObject(url);
newCache.setPath(path);
var headers = new Map<String, String>();
if (eTag != null) {
Expand All @@ -170,11 +267,11 @@ class CacheManager {
var response;
try {
response = await http.get(url, headers: headers);
}catch(e){}
} catch (e) {}
if (response != null) {
if (response.statusCode == 200) {
await newCache.setDataFromHeaders(response.headers);
var folder = new File(newCache.filePath).parent;
var folder = new File(newCache.filePath).parent;
if (!(await folder.exists())) {
folder.createSync(recursive: true);
}
Expand Down Expand Up @@ -215,40 +312,62 @@ class CacheObject {
return null;
}

DateTime touched;
String url;

Object lock;
Map _map;

CacheObject() {
CacheObject(String url) {
this.url = url;
_map = new Map();
touch();
lock = new Object();
}

CacheObject.fromMap(Map map) {
CacheObject.fromMap(String url, Map map) {
this.url = url;
_map = map;

if (_map.containsKey("touched")) {
touched = new DateTime.fromMillisecondsSinceEpoch(_map["touched"]);
} else {
touch();
}

lock = new Object();
}

Map toMap() {
return _map;
}

touch() {
touched = new DateTime.now();
_map["touched"] = touched.millisecondsSinceEpoch;
}

setDataFromHeaders(Map<String, String> headers) async {
//Without a cache-control header we keep the file for a week
var ageDuration = new Duration(days: 7);

if (headers.containsKey("cache-control")) {
var cacheControl = headers["cache-control"];
var controlSettings = cacheControl.split(", ");
controlSettings.forEach((setting) {
if (setting.startsWith("max-age=")) {
var validSeconds =
int.parse(setting.split("=")[1], onError: (source) => 0);
int.parse(setting.split("=")[1], onError: (source) => 0);
if (validSeconds > 0) {
_map["validTill"] = new DateTime.now()
.add(new Duration(seconds: validSeconds))
.millisecondsSinceEpoch;
ageDuration = new Duration(seconds: validSeconds);
}
}
});
}

_map["validTill"] =
new DateTime.now().add(ageDuration).millisecondsSinceEpoch;

if (headers.containsKey("etag")) {
_map["ETag"] = headers["etag"];
}
Expand All @@ -274,9 +393,9 @@ class CacheObject {
}
}

class CachedNetworkImageProvider extends ImageProvider<CachedNetworkImageProvider> {

const CachedNetworkImageProvider(this.url, { this.scale: 1.0 })
class CachedNetworkImageProvider
extends ImageProvider<CachedNetworkImageProvider> {
const CachedNetworkImageProvider(this.url, {this.scale: 1.0})
: assert(url != null),
assert(scale != null);

Expand All @@ -285,19 +404,18 @@ class CachedNetworkImageProvider extends ImageProvider<CachedNetworkImageProvide
final double scale;

@override
Future<CachedNetworkImageProvider> obtainKey(ImageConfiguration configuration) {
Future<CachedNetworkImageProvider> obtainKey(
ImageConfiguration configuration) {
return new SynchronousFuture<CachedNetworkImageProvider>(this);
}

@override
ImageStreamCompleter load(CachedNetworkImageProvider key) {
return new OneFrameImageStreamCompleter(
_loadAsync(key),
return new OneFrameImageStreamCompleter(_loadAsync(key),
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
}
);
information.writeln('Image provider: $this');
information.write('Image key: $key');
});
}

Future<ImageInfo> _loadAsync(CachedNetworkImageProvider key) async {
Expand All @@ -306,37 +424,32 @@ class CachedNetworkImageProvider extends ImageProvider<CachedNetworkImageProvide
return _loadAsyncFromFile(key, file);
}

Future<ImageInfo> _loadAsyncFromFile(CachedNetworkImageProvider key, File file) async {
Future<ImageInfo> _loadAsyncFromFile(
CachedNetworkImageProvider key, File file) async {
assert(key == this);

final Uint8List bytes = await file.readAsBytes();
if (bytes.lengthInBytes == 0)
return null;
if (bytes.lengthInBytes == 0) return null;

final ui.Image image = await decodeImageFromList(bytes);
if (image == null)
return null;
if (image == null) return null;

return new ImageInfo(
image: image,
scale: key.scale,
);
}


@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
if (other.runtimeType != runtimeType) return false;
final CachedNetworkImageProvider typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
return url == typedOther.url && scale == typedOther.scale;
}

@override
int get hashCode => hashValues(url, scale);

@override
String toString() => '$runtimeType("$url", scale: $scale)';

}

0 comments on commit e1711d0

Please sign in to comment.