I've read through the large number of caching questions and have Glide working perfectly with OkHTTP to achieve the 304 Not Modified use case (described in #463). Basically the app always does a network call, but only downloads the new image if the etag changes. Otherwise, it uses the cached version from OkHTTP. In order to achieve this I use DiskCacheStrategy.NONE and delegate all caching to OkHTTP. My question is, is there a simple way to use the cached image as a placeholder image? #463 doesn't explain how to achieve it.
Add a thumbnail load that does something similar. You can create a custom model which you likely already have and have a field to decide if you want to return the cached or not.
Placeholder must be a resource or drawable that was created prior to the load. Thumbnail is more dynamic.
Thanks @TWiStErRob, I'm not sure how to do a thumbnail load. I was contemplating using downloadOnly to manually refresh the cache if needed before hand. I also tried using a listener to reload the image. However that didn't work so well.
Can you share your current related code?
So to enable HTTP response caching with OkHttp I extend the GlideModule. This is based on your suggestion in another thread regarding custom OkHttp configuration.
public class CustomGlideModule implements GlideModule {
public static final int IMAGE_CACHE_SIZE = 10 * 1024 * 1024;
@Override
public void applyOptions(Context context, GlideBuilder builder) {
}
@Override
public void registerComponents(Context context, Glide glide) {
OkHttpClient client = new OkHttpClient().newBuilder().addNetworkInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
// I need to set the Cache-Control in order to force server side validation for the ETAG
return originalResponse.newBuilder().header("Cache-Control", "max-age=0").build();
}
}).cache(new Cache(context.getCacheDir(), IMAGE_CACHE_SIZE)).build();
glide.register(GlideUrl.class, InputStream.class,
new com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client));
}
}
I then use Glide with caching disabled so that the underlying network layer (OkHttp) always contacts the server.
DrawableRequestBuilder builder = Glide.with(context)
.load(Uri.parse("http://sample_image.jpg"))
.error(drawableResource)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(R.drawable.placeholder)
.crossFade()
.thumbnail(0.3f);
builder.into(target);
However this results in the placeholder image always being shown while the network call is being made. This is why I'd like to load the cached image from OkHttp as the placeholder.
I tried to fix this by using Glide listeners. First I load the image with Glide caching enabled. If the image isn't cached, it's fetched from the server and then stored. If it is cached it is loaded immediately and the server isn't contacted. I then have a listener for when this first load completes. If the load occurred from memory (ie. a cached image) it attempts another Glide.with(...).load(...), this time with caching disabled so that it always contacts the server. Then OkHttp can handle the etags and 304s, etc. This feels very hacky but it does work. I am however having NullPointerExceptions with the .into(target) when the target is an ImageView inside a RecyclerView holder.
DrawableRequestBuilder builder = Glide.with(context.getApplicationContext())
.load(Uri.parse("http://sample_image.jpg"))
.error(drawable)
.placeholder(R.drawable.placeholder)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.crossFade()
.thumbnail(0.3f)
.listener(new RequestListener<Uri, GlideDrawable>() {
@Override
public boolean onException(Exception e, Uri model, Target<GlideDrawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(GlideDrawable oldResource, Uri model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
if (isFromMemoryCache) {
DrawableRequestBuilder builder = Glide.with(context.getApplicationContext())
.load(Uri.parse("http://sample_image.jpg"))
.error(oldResource)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(oldResource)
.crossFade()
.dontAnimate()
.thumbnail(0.3f);
builder.into(target);
return true;
}
return false;
}
});
builder.into(target);
I'm hoping there's a more elegant solution. Perhaps you have a better idea @TWiStErRob
Take a look at the above commit. Here's what I managed to achieve:
For visibility I added a grayscale transform so it's easy to see which one is cached and which one is the real deal. The server side is a simple PHP script handling ETag validation manually and generating a new image every 5 seconds.
Be careful with .error() on full load: if there's no network but the image is cached the main load will fail fast and show the error drawable, but then the thumbnail will finish loading and show a stale image. So the error drawable will just flash for a short time (crossFade).
Here's the relevant code:
public class CachedGlideUrl extends GlideUrl {
public CachedGlideUrl(String url) { super(url); }
}
public class ForceLoadGlideUrl extends GlideUrl {
private static final Headers FORCE_ETAG_CHECK = new LazyHeaders.Builder()
// I need to set the Cache-Control in order to force server side validation for the ETAG
.addHeader("Cache-Control", "max-age=0")
.build();
public ForceLoadGlideUrl(String url) { super(url, FORCE_ETAG_CHECK); }
}
@Override public void registerComponents(Context context, Glide glide) {
final Cache cache = new Cache(new File(context.getCacheDir(), "okhttp"), IMAGE_CACHE_SIZE);
OkHttpClient client = new OkHttpClient().newBuilder().cache(cache).build();
glide.register(CachedGlideUrl.class, InputStream.class,
superFactory(new OkHttpUrlLoader.Factory(client), CachedGlideUrl.class));
glide.register(ForceLoadGlideUrl.class, InputStream.class,
superFactory(new OkHttpUrlLoader.Factory(client), ForceLoadGlideUrl.class));
}
@SuppressWarnings({"unchecked", "unused"})
private static <T> ModelLoaderFactory<T, InputStream> superFactory(
ModelLoaderFactory<? super T, InputStream> factory, Class<T> modelType) {
return (ModelLoaderFactory<T, InputStream>)factory;
}
Glide
.with(this)
.load(new ForceLoadGlideUrl(urlString))
.fitCenter()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.thumbnail(Glide
.with(this)
.load(new CachedGlideUrl(urlString))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.bitmapTransform(new FitCenter(context), new GrayscaleTransformation(context))
.sizeMultiplier(0.25f)
)
.into(imageView)
;
The trick I applied is using separate models for different behavior. This way Glide's default behavior is also preserved and available, that is: not every request is cached, only those loaded via these models. If you want to use the same OkHttpClient for all loads just add a register for GlideUrl as well. I also got rid of the interceptor, Glide supports headers in a network-library-agnostic way.
The NPE you mention seems weird, because target is already initialized at the point of the outer into call and onResourceReady is a sync callback in this case.
Thanks @TWiStErRob, I'm not sure how to setup the superFactory. I do like the fact that we can do network agnostic header injection. Is it possible to add this to the wiki?
You can find a full working example in my support project. There's a link just above my previous comment.
This feature was introduced in https://github.com/bumptech/glide/releases/tag/v3.6.0
See also https://github.com/bumptech/glide/issues?q=GlideUrl+LazyHeaders for possible use cases.
Sorry, completely missed that. Thanks, I managed to get working and the transition is a lot smoother from the old image to the new one. However I'm not sure if the Cache-Control header is being added correctly. I used Charles/Fiddler to examine the network traffic in the glide-support app, but the server always responds with a 200 and not a 304. The If-None-Match header is never set on the Request.
If you check the PHP file I was using to test, I had to respond with below the first time:
HTTP 200
ETag: <blah>
Cache-Control: public, max-age=<big number>
only then OkHttp was willing to revalidate when using the forced version. The default behaviour is to cache the image and OkHttp will respond from cache, but then if you add the max-age=0 header in the forced request it'll say: oh well, I know it's not valid any more, so let's try to ask the server if it changed via ETag. What is your first and subsequent request response pair headers?
With regard to @TWiStErRob's solution, can the same thing be achieved using Glide 4's AppGlideModule?