Using Picasso with custom disk cache
Asked Answered
O

4

26

In Volley library, the NetworkImageView class requires an ImageLoader that handles all the image requests by searching for them inside an ImageCache implementation, the user is free to choose how the cache should work, the location and the name of the images.

I'm switching from Volley to Retrofit, and for the images I decided to try Picasso.

With the former library, I had a String parameter in each of my items containing the image URL, then I used myNetworkImageView.setImageUrl(item.getURL()) and it was able to determine if image was cached on disk. If the image existed in cache folder, the image was loaded, otherwise it was downloaded and loaded.

I would like to be able to do the same with Picasso, is it possible with Picasso APIs or should I code such feature by myself?

I was thinking to download the image to a folder (the cache folder), and use Picasso.with(mContext).load(File downloadedimage) on completion. Is this the proper way or are there any best practices?

Outmost answered 24/4, 2014 at 21:29 Comment(6)
@CommonsWare I basically want Picasso to download the image to a custom path with a custom name, so that I can implement some kind of cache that checks if the downloaded image exists and avoid downloading it. Both custom path and custom name are important because users should be able to replace the images with their custom ones if they want. Also I don't want to worry about occupied space, I just want permanently stored images in a folder reachable by the users.Outmost
AFAIK, that would require code changes to Picasso.Burgundy
@Burgundy I think I could just download the image from the url and ask Picasso to load that file once the download is completed, but I don't know if there's any best practice to do it or if it's not convenient for some reasons. I also read that somehow Picasso supports callbacks, so I could access the bitmap once it's downloaded and save it to a file where I wish to, then next executions will check if the file exists in cache folder. Would this be a solution?Outmost
Possibly. Now that I think about it, Picasso treats disk as cache, and you're treating it as something a bit more than that ("permanently stored images"), so trying to amend Picasso's disk-caching approach may not be the right answer.Burgundy
@Burgundy your advice is to switch to another library? If yes, is there any that you'd recommend and that fits my needs?Outmost
possible duplicate of How to implement my own disk cache with picasso library - Android?Discounter
G
52

Picasso doesn't have a disk cache. It delegates to whatever HTTP client you are using for that functionality (relying on HTTP cache semantics for cache control). Because of this, the behavior you seek comes for free.

The underlying HTTP client will only download an image over the network if one does not exist in its local cache (and that image isn't expired).

That said, you can create custom cache implementation for java.net.HttpUrlConnection (via ResponseCache or OkHttp (via ResponseCache or OkResponseCache) which stores files in the format you desire. I would strongly advise against this, however.

Let Picasso and the HTTP client do the work for you!

You can call setIndicatorsEnabled(true) on the Picasso instance to see an indicator from where images are being loaded. It looks like this:

If you never see a blue indicator, it's likely that your remote images do not include proper cache headers to enable caching to disk.

Gal answered 24/4, 2014 at 22:43 Comment(13)
I perfectly understood your explanation, unfortunately this goes against my needs and I'll probably try another library, because: 1) The users of my applications should be able to access the cache to replace downloaded images with their custom ones, 2) The downloaded images should be stored permanently as I want the app to work offline and show every already downloaded file.Outmost
Yeah that's not a Picasso task then. You can give Picasso local File resources to load once you have them, but for such an atypical need you'll have to roll your own download and storage infrastructure.Gal
Yes, it's a real pity. Picasso would've been very easy to integrate inside my project.Outmost
@JakeWharton is there any way to get cached stream downloaded image in Picasso?Rosales
@JakeWharton I strongly disagree with you about this being an atypical need. Every app I worked on requires images to be available offline. Meaning that if the image can't be obtained from network but is available in the disk cache should be provided. Regardless of how old it is.Heterograft
@DanieleSegato Then use unique URLs and return cache headers that are obnoxiously large (1yr+). Besides, Picasso uses stale cached images if the network retrieval fails 3 times.Gal
@JakeWharton I agree with you unique URLs are good practice, but I often rely on third party services were I have no control. Speaking of the behavior of the library: it doesn't seem like that to me, I tried and also looked at the code: it start from retries = 2, decrease it and go in local cache at retries == 0. BUT the "shouldRetry" method return false if not retryCount > 0. Furthermore the Dispatcher "performRetry" method totally skip even the second try if there's no network and just perform an error. Seems like you expect it to work differently. Is this a bug?Heterograft
Also, if you use Target (square.github.io/picasso/javadoc/com/squareup/picasso/…) as listener instead of ImageView you won't see the indicators.Equilibrate
@JakeWharton - I'm all for letting Picasso do the work for me. But if you're going to provide a default http client/downloader and accompanying disk cache mechanism (OkHttp from what I understand), why do you not provide a way for clients to access it? At least in a limited way? The invalidate() method is great for clearing the memory cache, but I'm looking for similar ease-of-use on the disk cache side without having to create a custom cache implementation.Havenot
If you need to invalidate something in the disk cache then your server is not setting the appropriate headers in the first place.Gal
I've been seeking for something similar for several days. Is it possible to store the images on SDCard (in a known folder) and retrieve them from there, using Picasso? I've implemented my own class that implements Cache but I'm having problems converting Bitmaps to Files and viceversa. Can Picasso do this for me? Thx in advanceDomesday
@JakeWharton - That's a good point about the server not setting the appropriate headers. But what if I need to download an image from a constant URL where the image will occasionally be swapped out, yet I don't have control over the server-side headers? I want to invalidate Picasso's memory cache and the downloader's disk cache on a regular basis (say once a day) to pick up the image change within a reasonable timeframe. What is the best way to handle that scenario in Picasso?Havenot
Disk caching on Picasso is flawed especially when using it in a recycler list view. It only caches whats displayed currently in the recycer list onscreen and then when you scroll up or down to view more items it re-downloads the images again. Also you lose all cached images once you exit the appWrest
S
12

If your project is using the okhttp library then picasso will automatically use it as the default downloader and the disk caché will work automagically.

Assuming that you use Android Studio, just add these two lines under dependencies in the build.gradle file and you will be set. (No extra configurations with picasso needed)

dependencies {
    [...]
    compile 'com.squareup.okhttp:okhttp:2.+'
    compile 'com.squareup.okhttp:okhttp-urlconnection:2.+'
}
Sard answered 23/7, 2014 at 11:14 Comment(4)
I have configured instance of OkHttpClient that i use in app for REST Api calls. Is a way to pass this instance to Picasso ?Aerodrome
@Aerodrome You can do it with Picasso.Builder classDisbursement
is adding the second com.squareup.okhttp:okhttp-urlconnection needed or only the first is enough?Deceitful
adding this makes picasso become unresponsiveAsthenopia
G
2

As rightly pointed out by many people here, OkHttpClient is the way to go for caching.

When caching with OkHttp you might also want to gain more control on Cache-Control header in the HTTP response using OkHttp interceptors, see my response here

Gupton answered 21/10, 2015 at 21:25 Comment(0)
P
1

How it is was written previously, Picasso uses a cache of the underlying Http client.

HttpUrlConnection's built-in cache isn't working in truly offline mode and If using of OkHttpClient is unwanted by some reasons, it is possible to use your own implementation of disk-cache (of course based on DiskLruCache).

One of ways is subclassing com.squareup.picasso.UrlConnectionDownloader and programm whole logic at:

@Override
public Response load(final Uri uri, int networkPolicy) throws IOException {
...
}

And then use your implementation like this:

new Picasso.Builder(context).downloader(<your_downloader>).build();

Here is my implementation of UrlConnectionDownloader, that works with disk-cache and ships to Picasso bitmaps even in total offline mode:

public class PicassoBitmapDownloader extends UrlConnectionDownloader {

    private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
    private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB

    @NonNull private Context context;
    @Nullable private DiskLruCache diskCache;

    public class IfModifiedResponse extends Response {

        private final String ifModifiedSinceDate;

        public IfModifiedResponse(InputStream stream, boolean loadedFromCache, long contentLength, String ifModifiedSinceDate) {

            super(stream, loadedFromCache, contentLength);
            this.ifModifiedSinceDate = ifModifiedSinceDate;
        }

        public String getIfModifiedSinceDate() {

            return ifModifiedSinceDate;
        }
    }

    public PicassoBitmapDownloader(@NonNull Context context) {

        super(context);
        this.context = context;
    }

    @Override
    public Response load(final Uri uri, int networkPolicy) throws IOException {

        final String key = getKey(uri);
        {
            Response cachedResponse = getCachedBitmap(key);
            if (cachedResponse != null) {
                return cachedResponse;
            }
        }

        IfModifiedResponse response = _load(uri);

        if (cacheBitmap(key, response.getInputStream(), response.getIfModifiedSinceDate())) {

            IfModifiedResponse cachedResponse = getCachedBitmap(key);
            if (cachedResponse != null) {return cachedResponse;
            }
        }

        return response;
    }

    @NonNull
    protected IfModifiedResponse _load(Uri uri) throws IOException {

        HttpURLConnection connection = openConnection(uri);

        int responseCode = connection.getResponseCode();
        if (responseCode >= 300) {
            connection.disconnect();
            throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
                    0, responseCode);
        }

        long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
        String lastModified = connection.getHeaderField(Constants.HEADER_LAST_MODIFIED);
        return new IfModifiedResponse(connection.getInputStream(), false, contentLength, lastModified);
    }

    @Override
    protected HttpURLConnection openConnection(Uri path) throws IOException {

        HttpURLConnection conn = super.openConnection(path);

        DiskLruCache diskCache = getDiskCache();
        DiskLruCache.Snapshot snapshot = diskCache == null ? null : diskCache.get(getKey(path));
        if (snapshot != null) {
            String ifModifiedSince = snapshot.getString(1);
            if (!isEmpty(ifModifiedSince)) {
                conn.addRequestProperty(Constants.HEADER_IF_MODIFIED_SINCE, ifModifiedSince);
            }
        }

        return conn;
    }

    @Override public void shutdown() {

        try {
            if (diskCache != null) {
                diskCache.flush();
                diskCache.close();
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }

        super.shutdown();
    }

    public boolean cacheBitmap(@Nullable String key, @Nullable InputStream inputStream, @Nullable String ifModifiedSince) {

        if (inputStream == null || isEmpty(key)) {
            return false;
        }

        OutputStream outputStream = null;
        DiskLruCache.Editor edit = null;
        try {
            DiskLruCache diskCache = getDiskCache();
            edit = diskCache == null ? null : diskCache.edit(key);
            outputStream = edit == null ? null : new BufferedOutputStream(edit.newOutputStream(0));

            if (outputStream == null) {
                return false;
            }

            ChatUtils.copy(inputStream, outputStream);
            outputStream.flush();

            edit.set(1, ifModifiedSince == null ? "" : ifModifiedSince);
            edit.commit();

            return true;
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {

            if (edit != null) {
                edit.abortUnlessCommitted();
            }

            ChatUtils.closeQuietly(outputStream);
        }
        return false;
    }

    @Nullable
    public IfModifiedResponse getCachedBitmap(String key) {

        try {
            DiskLruCache diskCache = getDiskCache();
            DiskLruCache.Snapshot snapshot = diskCache == null ? null : diskCache.get(key);
            InputStream inputStream = snapshot == null ? null : snapshot.getInputStream(0);

            if (inputStream == null) {
                return null;
            }

            return new IfModifiedResponse(inputStream, true, snapshot.getLength(0), snapshot.getString(1));
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    @Nullable
    synchronized public DiskLruCache getDiskCache() {

        if (diskCache == null) {

            try {
                File file = new File(context.getCacheDir() + "/images");
                if (!file.exists()) {
                    //noinspection ResultOfMethodCallIgnored
                    file.mkdirs();
                }

                long maxSize = calculateDiskCacheSize(file);
                diskCache = DiskLruCache.open(file, BuildConfig.VERSION_CODE, 2, maxSize);
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }

        return diskCache;
    }

    @NonNull
    private String getKey(@NonNull Uri uri) {

        String key = md5(uri.toString());
        return isEmpty(key) ? String.valueOf(uri.hashCode()) : key;
    }

    @Nullable
    public static String md5(final String toEncrypt) {

        try {
            final MessageDigest digest = MessageDigest.getInstance("md5");
            digest.update(toEncrypt.getBytes());
            final byte[] bytes = digest.digest();
            final StringBuilder sb = new StringBuilder();
            for (byte aByte : bytes) {
                sb.append(String.format("%02X", aByte));
            }
            return sb.toString().toLowerCase();
        }
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    static long calculateDiskCacheSize(File dir) {

        long available = ChatUtils.bytesAvailable(dir);
        // Target 2% of the total space.
        long size = available / 50;
        // Bound inside min/max size for disk cache.
        return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);
    }
}
Penitentiary answered 25/3, 2016 at 12:49 Comment(1)
What are the imports you have used?Enoch

© 2022 - 2024 — McMap. All rights reserved.