24
votes

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?

4
@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. - Vektor88
AFAIK, that would require code changes to Picasso. - CommonsWare
@CommonsWare 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? - Vektor88
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. - CommonsWare
@CommonsWare your advice is to switch to another library? If yes, is there any that you'd recommend and that fits my needs? - Vektor88

4 Answers

49
votes

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.

12
votes

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.+'
}
2
votes

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

1
votes

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);
    }
}