javacachingretrofitokhttpoffline-caching

Can Retrofit with OKHttp use cache data when offline


I'm trying to use Retrofit & OKHttp to cache HTTP responses. I followed this gist and, ended up with this code:

File httpCacheDirectory = new File(context.getCacheDir(), "responses");

HttpResponseCache httpResponseCache = null;
try {
     httpResponseCache = new HttpResponseCache(httpCacheDirectory, 10 * 1024 * 1024);
} catch (IOException e) {
     Log.e("Retrofit", "Could not create http cache", e);
}

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setResponseCache(httpResponseCache);

api = new RestAdapter.Builder()
          .setEndpoint(API_URL)
          .setLogLevel(RestAdapter.LogLevel.FULL)
          .setClient(new OkClient(okHttpClient))
          .build()
          .create(MyApi.class);

And this is MyApi with the Cache-Control headers

public interface MyApi {
   @Headers("Cache-Control: public, max-age=640000, s-maxage=640000 , max-stale=2419200")
   @GET("/api/v1/person/1/")
   void requestPerson(
           Callback<Person> callback
   );

First I request online and check the cache files. The correct JSON response and headers are there. But when I try to request offline, I always get RetrofitError UnknownHostException. Is there anything else I should do to make Retrofit read the response from cache?

EDIT: Since OKHttp 2.0.x HttpResponseCache is Cache, setResponseCache is setCache


Solution

  • Edit for Retrofit 2.x:

    OkHttp Interceptor is the right way to access cache when offline:

    1) Create Interceptor:

    private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
        @Override public Response intercept(Chain chain) throws IOException {
            Response originalResponse = chain.proceed(chain.request());
            if (Utils.isNetworkAvailable(context)) {
                int maxAge = 60; // read from cache for 1 minute
                return originalResponse.newBuilder()
                        .header("Cache-Control", "public, max-age=" + maxAge)
                        .build();
            } else {
                int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                return originalResponse.newBuilder()
                        .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                        .build();
            }
        }
    

    2) Setup client:

    OkHttpClient client = new OkHttpClient();
    client.networkInterceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR);
    
    //setup cache
    File httpCacheDirectory = new File(context.getCacheDir(), "responses");
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(httpCacheDirectory, cacheSize);
    
    //add cache to the client
    client.setCache(cache);
    

    3) Add client to retrofit

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    

    Also check @kosiara - Bartosz Kosarzycki's answer. You may need to remove some header from the response.


    OKHttp 2.0.x (Check the original answer):

    Since OKHttp 2.0.x HttpResponseCache is Cache, setResponseCache is setCache. So you should setCache like this:

            File httpCacheDirectory = new File(context.getCacheDir(), "responses");
    
            Cache cache = null;
            try {
                cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
            } catch (IOException e) {
                Log.e("OKHttp", "Could not create http cache", e);
            }
    
            OkHttpClient okHttpClient = new OkHttpClient();
            if (cache != null) {
                okHttpClient.setCache(cache);
            }
            String hostURL = context.getString(R.string.host_url);
    
            api = new RestAdapter.Builder()
                    .setEndpoint(hostURL)
                    .setClient(new OkClient(okHttpClient))
                    .setRequestInterceptor(/*rest of the answer here */)
                    .build()
                    .create(MyApi.class);
    

    Original Answer:

    It turns out that server response must have Cache-Control: public to make OkClient to read from cache.

    Also If you want to request from network when available, you should add Cache-Control: max-age=0 request header. This answer shows how to do it parameterized. This is how I used it:

    RestAdapter.Builder builder= new RestAdapter.Builder()
       .setRequestInterceptor(new RequestInterceptor() {
            @Override
            public void intercept(RequestFacade request) {
                request.addHeader("Accept", "application/json;versions=1");
                if (MyApplicationUtils.isNetworkAvailable(context)) {
                    int maxAge = 60; // read from cache for 1 minute
                    request.addHeader("Cache-Control", "public, max-age=" + maxAge);
                } else {
                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                    request.addHeader("Cache-Control", 
                        "public, only-if-cached, max-stale=" + maxStale);
                }
            }
    });