Android Volley + JSONObjectRequest Caching

AndroidCachingRequestResponseAndroid Volley

Android Problem Overview


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.

Android Solutions


Solution 1 - Android

See this answer - https://stackoverflow.com/questions/16783177/set-expiration-policy-for-cache-using-googles-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 - Android

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 - Android

+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

Solution 4 - Android

I was able to force Volley to cache all responses by extending StringRequest and replacing request I want to forcibly cache with CachingStringRequest.

In overridden method parseNetworkResponse I remove Cache-Control headers. This way Volley is persisting the response in it's built-in cache.

public class CachingStringRequest extends StringRequest {
    private static final String CACHE_CONTROL = "Cache-Control";

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

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        // I do this to ignore "no-cache" headers
        // and use built-in cache from Volley.
        if (response.headers.containsKey(CACHE_CONTROL)) {
            response.headers.remove(CACHE_CONTROL);
        }

        return super.parseNetworkResponse(response);
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questiongaara87View Question on Stackoverflow
Solution 1 - AndroidOleksandr YefremovView Answer on Stackoverflow
Solution 2 - AndroidskyfishjyView Answer on Stackoverflow
Solution 3 - Androidlarham1View Answer on Stackoverflow
Solution 4 - AndroidsakydpozruxView Answer on Stackoverflow