Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR for review only - Feature/generic cache manager #6

Closed
wants to merge 5 commits into from

Conversation

FastNinja
Copy link

No description provided.

@@ -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;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would developers be able to change these config parameters . The inbetweenCleans for example might be too big?
Same with maxNrOfCacheObjects. if objects are small I might opt in to store more..?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are public statics, so they can be set when you start the app.


//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");
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should CacheManage know that it caches images? lib_cached_image_data implies it knows.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key is lib_cached_image_data just because I called it that when I made this library. If we change it we should also add some migration and I didn't think that was worth it.

});
}

// Get data about when the last clean action has been performed
var cleanMillis = _prefs.getInt("lib_cached_image_data_last_clean");
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of putting comment you could extract method with the same name
lastCacheClean = _getLastCleanTimestampFromPreferences();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I probably have to refactor a bit. I also want to extract the classes in multiple files.

_prefs.setString("lib_cached_image_data", JSON.encode(json));

if(await _shouldSaveAgain()){
await _saveDataInPrefs();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like _saveDataInPrefs calls _saveDataInPrefs again and uses number of special flags to control that behavior and avoid infinite loop.
what is the reason we need to do Save and then again Save?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic I use here is the following.
Imagine you download 5 files simultaneously. You would end up in 'Save' 5 times. If the first one is still busy saving the other 4 just indicate that something has changed and return. The first one will save one more time in the end, resulting in 2 saves instead of 5. It also only cleans the cache once.

await synchronized(_lock, () async {
var oldestDateAllowed = new DateTime.now().subtract(maxAgeCacheObject);

//Remove old objects
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be function _removeTooOldToKeepObjects (i.e. comment will not be necessary)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I'll refactor it a bit.

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--) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could simplify and use take(count) to get oldest objects. something like that:

  allValues.sort((c1, c2) => c2.touched.compareTo(c1.touched));  // sort OLDEST first
  allValues.take( _cacheData.length - maxNrOfCacheObjects);      // get them
  allValues.forEach( (item) {await _removeFile(lastItem)} )              //remove them

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I am still learning dart so didn't think about this.

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);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like sole folders are being created. I am not sure if we delete any of these folders..
what is the reason to have these folders? I thought that files will have just GUIDS as names so could stored flat?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes they are practically stored flat. It only creates one folder so all cache files are stored in a folder and cannot conflict with random other files in de cache folder.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting. that contradicts earlier Duraton of 30 days (static Duration maxAgeCacheObject = new Duration(days: 30);)
I think this is still CacheManager configuration - it belongs to CacheManager

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't really now how to name this variable, but the functionality is different.
The CacheManager configuration is about when to delete a file and checks when the file is last used by the app.
This 'ageDuration' indicates for how long the file is valid and should normally be set by the cache-control. When the user wants the file but it is not valid anymore it has to pull it from the server again whether the file is still in the cache or not. I didn't want to make it infinite as that might result in problems when a file is changed without the url changing.

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

setDataFromHeaders(Map<String, String> headers) async {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setDataFromHeaders is very coupled with HttpHeaders. Cache Objects is not support to know about headers - it knows the object, knows when it was touched etc...
parsing HttpHeaders sounds like an infrastructure concern? Currently Cache Manager knows that files are being downloaded from Http so it shall parse HttpHeaders (or delegate it to some parser etc..)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is probably a good idea.

Directory directory = await getTemporaryDirectory();
var folder = new Directory("${directory.path}/cache");
if (!(await folder.exists())) {
folder.createSync();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another place where we create folders ...?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same folder as earlier. It is probably better to only do it in one place. I did this because otherwise it broke when the file was gone but the filepath was already determined in the cache manager.

//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
Copy link
Author

@FastNinja FastNinja Dec 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why If we have never downloaded this file, do download has to be different to If file is removed from the cache storage, download again.
It can be exactly same flow - High level logic:

 if(!exsitsInCache){
   fetchFromNetwork();
}

return fromCache;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but you would get new File(null).exists(), is that allowed? Otherwise you still have to do some checks whether path == null and I don't directly see how this can be made cleaner.

@@ -170,11 +270,11 @@ class CacheManager {
var response;
try {
response = await http.get(url, headers: headers);
}catch(e){}
} catch (e) {}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. what is the default timeout on .get
  2. how we can let developers know that get failed with certain error.
  3. how developers can configure/get this error and decide what to do (present another image?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't think about it yet. I do agree that it is a good idea improve the error handling.

return true;
}
_isStoringData = false;
return false;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_shouldStoreDataAgain and _isStoringData two very puzzling fields...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment about saving. It is meant that you don't save more than necessary.

What do you think about the serialization of this data? It was a quick way, but I am not sure if it is the best way. I do think that sqlite is a bit overkill though.

Moved cache manager to flutter_cache_manager
Only support ImageProvider from now on
@cgowthamanmca cgowthamanmca mentioned this pull request Oct 17, 2019
@expilu expilu mentioned this pull request Jul 23, 2020
2 tasks
@sudha-anecure sudha-anecure mentioned this pull request Jan 10, 2023
2 tasks
@imijanur imijanur mentioned this pull request Sep 28, 2023
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants