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

Display cached image while requesting new image #1257

Closed
rkarodia opened this issue Jun 7, 2016 · 10 comments
Closed

Display cached image while requesting new image #1257

rkarodia opened this issue Jun 7, 2016 · 10 comments
Labels

Comments

@rkarodia
Copy link

rkarodia commented Jun 7, 2016

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.

@TWiStErRob
Copy link
Collaborator

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.

@rkarodia
Copy link
Author

rkarodia commented Jun 8, 2016

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.

@TWiStErRob
Copy link
Collaborator

Can you share your current related code?

@rkarodia
Copy link
Author

rkarodia commented Jun 9, 2016

HTTP Response Caching with OkHttp

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

Placeholder Problem

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.

Possible Solution using Glide Listeners

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

TWiStErRob added a commit to TWiStErRob/glide-support that referenced this issue Jun 9, 2016
@TWiStErRob
Copy link
Collaborator

TWiStErRob commented Jun 9, 2016

Take a look at the above commit. Here's what I managed to achieve:

  • While the resource is not expired it'll be loaded from cache without a network request and shown to the user
  • At the same time the server will be force-queried if it changed:
    • a 304 will result in loading the full-resolution image and fading from low resolution version
    • a 200 will result in loading that image and also it'll be cached by OkHttp

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.

@rkarodia
Copy link
Author

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?

@TWiStErRob
Copy link
Collaborator

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.

@rkarodia
Copy link
Author

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.

@TWiStErRob
Copy link
Collaborator

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?

@wouterdevos
Copy link

With regard to @TWiStErRob's solution, can the same thing be achieved using Glide 4's AppGlideModule?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants