Android Volley + JSONObjectRequest Caching

46,577

Solution 1

See this answer - Set expiration policy for cache using Google's Volley

This means Volley decides whether to cache response or not based only on headers "Cache-Control" and then "Expires", "maxAge".

What you could do is change this method com.android.volley.toolbox.HttpHeaderParser.parseCacheHeaders(NetworkResponse response) and ignore these headers, set entry.softTtl and entry.ttl fields to whatever value works for you and use your method in your request class. Here is an example:

/**
 * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
 * Cache-control headers are ignored. SoftTtl == 3 mins, ttl == 24 hours.
 * @param response The network response to parse headers from
 * @return a cache entry for the given response, or null if the response is not cacheable.
 */
public static Cache.Entry parseIgnoreCacheHeaders(NetworkResponse response) {
    long now = System.currentTimeMillis();

    Map<String, String> headers = response.headers;
    long serverDate = 0;
    String serverEtag = null;
    String headerValue;

    headerValue = headers.get("Date");
    if (headerValue != null) {
        serverDate = HttpHeaderParser.parseDateAsEpoch(headerValue);
    }

    serverEtag = headers.get("ETag");

    final long cacheHitButRefreshed = 3 * 60 * 1000; // in 3 minutes cache will be hit, but also refreshed on background
    final long cacheExpired = 24 * 60 * 60 * 1000; // in 24 hours this cache entry expires completely
    final long softExpire = now + cacheHitButRefreshed;
    final long ttl = now + cacheExpired;

    Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;
    entry.ttl = ttl;
    entry.serverDate = serverDate;
    entry.responseHeaders = headers;

    return entry;
}

Use this method in your Request class like this:

public class MyRequest extends com.android.volley.Request<MyResponse> {

    ...

    @Override
    protected Response<MyResponse> parseNetworkResponse(NetworkResponse response) {
        String jsonString = new String(response.data);
        MyResponse MyResponse = gson.fromJson(jsonString, MyResponse.class);
        return Response.success(MyResponse, HttpHeaderParser.parseIgnoreCacheHeaders(response));
    }

}

Solution 2

oleksandr_yefremov provides great codes that can help you when you dealing with cache strategy of Android Volley, especially when the REST API has improper "Cache-Control" headers or you just want more control on your own app cache strategy.

The key is HttpHeaderParser.parseCacheHeaders(NetworkResponse response)). If you want to have your own cache strategy. Replace it with parseIgnoreCacheHeaders(NetworkResponse response) in corresponding class.

If your class extends JsonObjectRequest, go to JsonObjectRequest and find

@Override
protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
    try {
            String jsonString =new String(response.data, HttpHeaderParser.parseCharset(response.headers));
            return Response.success(new JSONObject(jsonString),HttpHeaderParser.parseCacheHeaders(response));
        }catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        }catch (JSONException je) {
            return Response.error(new ParseError(je));
        }
}

and replace HttpHeaderParser.parseCacheHeaders(response) with HttpHeaderParser.parseIgnoreCacheHeaders

Solution 3

+1 for oleksandr_yefremov and skyfishjy also, and offering here a concrete, reusable class suitable for json or other string-based APIs:

public class CachingStringRequest extends StringRequest {
    public CachingStringRequest(int method, String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
        super(method, url, listener, errorListener);
    }

    public CachingStringRequest(String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
        super(url, listener, errorListener);
    }

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, parseIgnoreCacheHeaders(response));
    }
}

where the function parseIgnoreCacheHeaders() comes from the oleksandr_yefremov answer above. Use the CachingStringRequest class anywhere that the resulting json is ok to cache for 3 minutes (live) and 24 hours (expired but still available). A sample request:

CachingStringRequest stringRequest = new CachingStringRequest(MY_API_URL, callback);

and within the callback object's onResponse() function, parse the json. Set whatever caching limits you want--you could parameterize to add custom expiration per request.

For fun, try this in a simple app that downloads json and renders the downloaded info. Having filled the cache with the first successful download, watch the speedy rendering as you change orientations while cache is live (no download occurs given a live cache hit). Now kill the app, wait 3 minutes for that cache hit to expire (but not 24 hours for it to be removed from cache), enable airplane mode, and restart the app. The Volley error callback will occur, AND the "successful" onResponse() callback will occur from cached data, allowing your app to both render content and also know/warn that it came from expired cache.

One use of this kind of caching would be to obviate Loaders and other means of dealing with orientation change. If a request goes through a Volley singleton, and results are cached, refreshes that happen via orientation change are rendered quickly from cache, automatically by Volley, without the Loader.

Of course, this doesn't fit all requirements. YMMV

Share:
46,577
gaara87
Author by

gaara87

Updated on August 09, 2020

Comments

  • gaara87
    gaara87 almost 4 years
    public class CustomRequest extends JsonObjectRequest {
    
        public CustomRequest(String url, JSONObject params,
                Listener<JSONObject> listener, ErrorListener errorListener)
                throws JSONException {
            super(Method.POST,url, params, listener,
                    errorListener);
            this.setShouldCache(Boolean.TRUE);
        }
    }
    

    I was hoping that this piece of code would be enough for me to get implicit caching of responses. I'm not sure if it works or not, because i was under the assumption when a request is sent:

    1. it would hit the cache first and send that to onresponse

    2. then when the results come through from the remote server it would provide it to the onresponse

    Update:

    I figured how to manually retrieve the cache and reconstruct it into a JSONObject and send it through OnResponse function but that doesn't seem to efficient considering there is implicit caching. JsonObjectRequest class should return JSONObject as the cached entry instead of raw response data.

    But i'm still interested to know if i'm making some mistake.

    The ambiguity is solely due to the lack of documentation, so i apologize if i'm missing something quite obvious.

  • gaara87
    gaara87 almost 11 years
    does this mean the cache will last beyond, onDestroy? So that the next time the app is created, it will fetch from the cache?
  • Oleksandr Yefremov
    Oleksandr Yefremov almost 11 years
    Yes, cache is saved not only in memory but also on disk (see DiskBasedCache class for details). As a quick test, load some data, exit your app, switch off wifi or 3g and enter your app again. You can also specify cache size in mMaxCacheSizeInBytes field.
  • gaara87
    gaara87 almost 11 years
    Yes, it caches while i'm in the app, but when i quit the app and return into the app, fetching from cache returned null. Hence the question of whether it lives in between activity life cycles.
  • Oleksandr Yefremov
    Oleksandr Yefremov almost 11 years
    Caching on disk works for me, so I can only suggest to double check if you really ignore "Cache-control" header and set CacheEntry.softTtl and CacheEntry.ttl values correctly for some time in future. These are values which make Volley decide if it should check for cache anyway. Then you could debug what is being written into cache when your response comes.
  • Brett Duncavage
    Brett Duncavage over 9 years
    This is useful, however it still just caches the raw response, not the parsed response. So even on cache hits, you must re-parse the response. For non trivial JSON payloads this can add significant load times even for cached responses. As illustrated by the Volley logs: (+177 ) [687] cache-hit-parsed 177ms for re-parsing the response.
  • Oleksandr Yefremov
    Oleksandr Yefremov over 9 years
    @BrettDuncavage, sure thing it's a quickest solution. Parsed response is a model object, so one would need to use SQLite for this. And that's far more code than rewriting one method.
  • Kunu
    Kunu about 9 years
    @oleksandr_yefremov Can you please tell me how to edit parseIgnoreCacheHeaders method, because for me it is not editable right now.
  • Oleksandr Yefremov
    Oleksandr Yefremov about 9 years
    @Kunu, I assume you import it with gradle now? You need to download Volley sources from android.googlesource.com/platform/frameworks/volley as a library instead and change you build.gradle file to include it as a library module. Then you can make changes into its sources.
  • Kunu
    Kunu about 9 years
    @oleksandr_yefremov Yes, you are right. I am importing with gradle for now. Thanks I will try your solution.
  • Ibrahim Disouki
    Ibrahim Disouki over 8 years
    @oleksandr_yefremov It's working, but there is one problem when using Method.POST with the same URL but not same JsonBody. Volley considered that it's the same response, so whatever i changed the JsonBody i still have the old cache the from first request. Please Help Me!
  • JosephM
    JosephM over 8 years
    i'm doing same but it's not clearing cache. the expiration time is 1 minute.
  • iBobb
    iBobb about 8 years
    How would we use this cache with an image loader? I load images with myNetworkImageView.SetImageUrl(). I'd like to have my images cached for a day just like in your example.
  • Chantell Osejo
    Chantell Osejo about 8 years
    There is no need whatsoever to make changes directly to the library here. It's a static method. I'd recommend creating a separate class to contain this logic in your own project.
  • sdabet
    sdabet over 7 years
    Is it somehow possible to do that with NetworkImageView ?