Can Retrofit with OKHttp use cache data when offline
Asked Answered
M

7

158

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

Morissa answered 2/5, 2014 at 13:13 Comment(8)
Is the server you're calling responding with an appropriate Cache-Control header?Bolection
it returns this Cache-Control: s-maxage=1209600, max-age=1209600 I don't know if it's enough.Morissa
Seems like the public keyword was needed to be in response header to make it work offline. But, these headers doesn't let OkClient to use network when there is available. Is there anyway to set cache policy/strategy to use network when available?Morissa
I'm not sure whether you can do that in the same request. You can check the relevant CacheControl class, and the Cache-Control headers. If there's no such behavior, I would probably opt for making two requests, a cached only request (only-if-cached), followed by a network (max-age=0) one.Bolection
that was the first thing came to my mind. I spent days in that CacheControl and CacheStrategy classes. But two requests idea didn't made much sense. If max-stale + max-age is passed, it does request from network. But I want to set max-stale a week. This makes it read response from cache even if there is network available.Morissa
Isn't s-maxage for the server side only, not clients?Nicknack
Also checkout headers from server like here: #31322463Glary
rtm: square.github.io/okhttp/4.x/okhttp/okhttp3/-cache/…Verdieverdigris
M
199

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);
            }
        }
});
Morissa answered 6/5, 2014 at 19:56 Comment(21)
(I was wondering why this didn't work; turned out I forgot to set the actual cache for OkHttpClient to use. See the code in the question or in this answer.)Proffer
Just a word of advice: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).Coerce
Yes you are right, I've already added it to the question. I should also add it to the answerMorissa
I don't get interceptor called when network is not available. I am not sure how can the condition when network is not available will hit. Am I missing something here?Krusche
Why do you need to explicitly set the directive public? Is it private by default?Doctor
@toobsco42 which one? Header?Morissa
@Morissa Ya for the Cache-Control header.Doctor
I'm not really experienced with the headers. It was a trial and error solution for me. I said in a comment: "Seems like the public keyword was needed to be in response header to make it work offline." So it was private by default. It would be great if someone explained it.Morissa
is the if (Utils.isNetworkAvailable(context)) correct or is it supposed to be reversed i.e. if (!Utils.isNetworkAvailable(context)) ?Alunite
I'm using Retrofit 2.1.0 and when the phone is offline, public okhttp3.Response intercept(Chain chain) throws IOException is never called, it's only called when I'm onlineAlunite
I have a rails api and it already sets the cache-control headers for the response. Still it's not working. I'm using retrofit 2.1.0Kellerman
Sorry, it is working. Storage permission was toggled to off.Kellerman
The data is only available offline here for the time of 'max-age', after 60 seconds it cannot be loaded. @StarWars any idea?Rosenfeld
@Rosenfeld Actually still not working. It was working on one+ but not on nexus 5. I guess it's issue with nexus devices.Kellerman
@StarWars I gave up and started caching the data itself insteadRosenfeld
I've spent a lot of time because I was sending a date as a parameter and because of that the request couldn't be cached. Even though it may seem stupid, I though it could be useful if I wrote it in an answer that works, so here it is.Cogan
@Morissa Why we are checking internet connection while response interception? Thanks,though this solution is working for me but I am not getting the flow.Uhl
@RohitBandil This is a good question. You might be right about intercepting the request instead of response. I've added headers to the request on the original answer. I really don't remember why I did that.Morissa
@Morissa what is the purpose to add a header in a request when the internet is not available in your original answer.Anyways it will throw you a network not available exception.Uhl
I am having the same issue as @Alunite described. Using the above code for the interceptor on Retrofit 2.3.0 gives me a java.net.ConnectException when i'm offline. The exception is already thrown at the line Response originalResponse = chain.proceed(chain.request());. Hence I don't even get to the check whether the device is online or offline. Anyone knows how to fix this?Syncytium
Update to my comment. Using the the lib posted in an anser below by @Nicolas Cornette: github.com/ncornette/OkCacheControl it is working and the cache is used when i'm offline. Thanks for that. But still i would be interested in why above interceptor doesn't work for me?Syncytium
M
32

All of the anwsers above did not work for me. I tried to implement offline cache in retrofit 2.0.0-beta2. I added an interceptor using okHttpClient.networkInterceptors() method but received java.net.UnknownHostException when I tried to use the cache offline. It turned out that I had to add okHttpClient.interceptors() as well.

The problem was that cache wasn't written to flash storage because the server returned Pragma:no-cache which prevents OkHttp from storing the response. Offline cache didn't work even after modifying request header values. After some trial-and-error I got the cache to work without modifying the backend side by removing pragma from reponse instead of the request - response.newBuilder().removeHeader("Pragma");

Retrofit: 2.0.0-beta2; OkHttp: 2.5.0

OkHttpClient okHttpClient = createCachedClient(context);
Retrofit retrofit = new Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl(API_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build();
service = retrofit.create(RestDataResource.class);

...

private OkHttpClient createCachedClient(final Context context) {
    File httpCacheDirectory = new File(context.getCacheDir(), "cache_file");

    Cache cache = new Cache(httpCacheDirectory, 20 * 1024 * 1024);
    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.setCache(cache);
    okHttpClient.interceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    okHttpClient.networkInterceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    return okHttpClient;
}

...

public interface RestDataResource {

    @GET("rest-data") 
    Call<List<RestItem>> getRestData();

}
Miquelon answered 14/12, 2015 at 11:34 Comment(4)
It looks like your interceptors () and networkInterceptors () are identical. Why did you duplicate this?Doctor
different types of interceptors read here. github.com/square/okhttp/wiki/InterceptorsUhl
yes but they both do the same things so I am pretty sure that 1 interceptor should be enough, right ?Centillion
is there a specific reason to not use the same interceptor instance for both .networkInterceptors().add() and interceptors().add()?Verdieverdigris
C
22

My solution:

private BackendService() {

    httpCacheDirectory = new File(context.getCacheDir(),  "responses");
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(httpCacheDirectory, cacheSize);

    httpClient = new OkHttpClient.Builder()
            .addNetworkInterceptor(REWRITE_RESPONSE_INTERCEPTOR)
            .addInterceptor(OFFLINE_INTERCEPTOR)
            .cache(cache)
            .build();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.backend.com")
            .client(httpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    backendApi = retrofit.create(BackendApi.class);
}

private static final Interceptor REWRITE_RESPONSE_INTERCEPTOR = chain -> {
    Response originalResponse = chain.proceed(chain.request());
    String cacheControl = originalResponse.header("Cache-Control");

    if (cacheControl == null || cacheControl.contains("no-store") || cacheControl.contains("no-cache") ||
            cacheControl.contains("must-revalidate") || cacheControl.contains("max-age=0")) {
        return originalResponse.newBuilder()
                .header("Cache-Control", "public, max-age=" + 10)
                .build();
    } else {
        return originalResponse;
    }
};

private static final Interceptor OFFLINE_INTERCEPTOR = chain -> {
    Request request = chain.request();

    if (!isOnline()) {
        Log.d(TAG, "rewriting request");

        int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
        request = request.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                .build();
    }

    return chain.proceed(request);
};

public static boolean isOnline() {
    ConnectivityManager cm = (ConnectivityManager) MyApplication.getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo netInfo = cm.getActiveNetworkInfo();
    return netInfo != null && netInfo.isConnectedOrConnecting();
}
Carnauba answered 22/4, 2016 at 13:28 Comment(5)
not working for me... Getting 504 Unsatisfiable Request (only-if-cached)Irrawaddy
Only yours solution helps me, thanks a lot. Waste 2 days to scroll downSort
yup, the only working solution in my case. (Retrofit 1.9.x + okHttp3)Cyclades
Works with Retrofit RETROFIT_VERSION=2.2.0 OKHTTP_VERSION=3.6.0Eros
how to add builder.addheader() in this method to access api with authorisation?Kendo
P
7

The answer is YES, based on the above answers, I started writing unit tests to verify all possible use cases :

  • Use cache when offline
  • Use cached response first until expired, then network
  • Use network first then cache for some requests
  • Do not store in cache for some responses

I built a small helper lib to configure OKHttp cache easily, you can see the related unittest here on Github : https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ncornette/cache/OkCacheControlTest.java

Unittest that demonstrates the use of cache when offline :

@Test
public void test_USE_CACHE_WHEN_OFFLINE() throws Exception {
    //given
    givenResponseInCache("Expired Response in cache", -5, MINUTES);
    given(networkMonitor.isOnline()).willReturn(false);

    //when
    //This response is only used to not block when test fails
    mockWebServer.enqueue(new MockResponse().setResponseCode(404));
    Response response = getResponse();

    //then
    then(response.body().string()).isEqualTo("Expired Response in cache");
    then(cache.hitCount()).isEqualTo(1);
}

As you can see, cache can be used even if it has expired. Hope it will help.

Phenyl answered 22/6, 2016 at 14:8 Comment(1)
Your lib is great! Thanks for your hard working. The lib: github.com/ncornette/OkCacheControlCyclades
D
6

building on @kosiara-bartosz-kasarzycki's answer, I created a sample project that properly loads from memory->disk->network using retrofit, okhttp, rxjava and guava. https://github.com/digitalbuddha/StoreDemo

Duck answered 21/12, 2015 at 17:4 Comment(0)
L
2

Cache with Retrofit2 and OkHTTP3:

OkHttpClient client = new OkHttpClient
  .Builder()
  .cache(new Cache(App.sApp.getCacheDir(), 10 * 1024 * 1024)) // 10 MB
  .addInterceptor(new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
      Request request = chain.request();
      if (NetworkUtils.isNetworkAvailable()) {
        request = request.newBuilder().header("Cache-Control", "public, max-age=" + 60).build();
      } else {
        request = request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7).build();
      }
      return chain.proceed(request);
    }
  })
  .build();

NetworkUtils.isNetworkAvailable() static method:

public static boolean isNetworkAvailable(Context context) {
        ConnectivityManager cm =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null &&
                activeNetwork.isConnectedOrConnecting();
    }

Then just add client to the retrofit builder:

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

Original source: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

Lemus answered 29/5, 2016 at 7:17 Comment(3)
when I first load with offline mode, it is crashing! otherwise it is working properlyUltramontane
this does not work for me. I copy-pasted it en tried it after I tried to integrate the principle of it, but do net get it to work.Rosenfeld
App.sApp.getCacheDir() what does this do?Denaedenarius
R
1

Attention! OkHttp build in cache only work for GET method(refer to above solution). If you want to cache POST request, you must implement yourself. enter image description here

Righteousness answered 9/2, 2022 at 5:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.