Retrofit OKHTTP Offline caching not working

2020-05-24 06:39发布

I read dozens of tutorial and Stackoverflow answers to my problem but nothing is working for me! Also, most of them are old so probably OKHTTP changed somehow.

All I want is to enable offline caching for Retrofit.

I am using GET

I tried using only offlineCacheInterceptor as an Interceptor, but I kept getting:

Unable to resolve host "jsonplaceholder.typicode.com": No address associated with hostname

I tried using a combination of offlineCacheInterceptoras an Interceptor + provideCacheInterceptor() as a NetworkInterceptor, but I kept getting:

504 Unsatisfiable Request (only-if-cached) and a null response.body()

I even made sure to add .removeHeader("Pragma") everywhere!


I tried all these Links:

https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html (One interceptor, Not working!!)

https://medium.com/mindorks/caching-with-retrofit-store-responses-offline-71439ed32fda (One interceptor, Not working!)

https://caster.io/lessons/retrofit-2-offline-cache (Separate Online + Offline caching, Not working)

https://www.journaldev.com/23297/android-retrofit-okhttp-offline-caching (Not working, 504 Unsatisfiable Request (only-if-cached))

http://mikescamell.com/gotcha-when-offline-caching-with-okhttp3/ (One interceptor, Not working!!)

https://stackoverflow.com/a/48295397/8086424 (Not Working) Unable to resolve host "jsonplaceholder.typicode.com": No address associated with hostname

Can Retrofit with OKHttp use cache data when offline (TOO confusing!)


Here's my code:

public static Retrofit getRetrofitInstance(Context context) {
        if (retrofit == null) {
            c = context;
            int cacheSize = 10 * 1024 * 1024; // 10 MB
            Cache cache = new Cache(context.getCacheDir(), cacheSize);
            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .addInterceptor(provideHttpLoggingInterceptor())
                    .addInterceptor(offlineCacheInterceptor)
                    .addNetworkInterceptor(provideCacheInterceptor())
                    .cache(cache)
                    .build();
            //////////////////////////
            retrofit = new retrofit2.Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .client(okHttpClient)
                    .build();
        }
        return retrofit;
    }

 public static Interceptor offlineCacheInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Log.e("bbbb", "bbbb");
            if (!checkInternetAvailability()) {
                Log.e("aaaaa", "aaaaaa");
                CacheControl cacheControl = new CacheControl.Builder()
                        .maxStale(30, TimeUnit.DAYS)
                        .build();

                request = request.newBuilder()
                        .cacheControl(cacheControl)
                        .removeHeader("Pragma")
                        .build();
            }
            return chain.proceed(request);
        }
    };

 public static Interceptor provideCacheInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Response response = chain.proceed(chain.request());

                // re-write response header to force use of cache
                CacheControl cacheControl = new CacheControl.Builder()
                        .maxAge(2, TimeUnit.MINUTES)
                        .build();

                return response.newBuilder()
                        .header(CACHE_CONTROL, cacheControl.toString())
                        .removeHeader("Pragma")
                        .build();
            }
        };
    }

I am using jsonplaceholder.typicode.com/photos that returns:

content-type: application/json; charset=utf-8
    date: Sun, 21 Oct 2018 14:26:41 GMT
    set-cookie: __cfduid=d9e935012d2f789245b1e2599a41e47511540132001; expires=Mon, 21-Oct-19 14:26:41 GMT; path=/; domain=.typicode.com; HttpOnly
    x-powered-by: Express
    vary: Origin, Accept-Encoding
    access-control-allow-credentials: true
    expires: Sun, 21 Oct 2018 18:26:41 GMT
    x-content-type-options: nosniff
    etag: W/"105970-HCYFejK2YCxztz8++2rHnutkPOQ"
    via: 1.1 vegur
    cf-cache-status: REVALIDATED
    expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
    server: cloudflare
    cf-ray: 46d466910cab3d77-MXP
    Cache-Control: public, max-age=60

1条回答
唯我独甜
2楼-- · 2020-05-24 07:05

Oct. 2018 (Retrofit 2.4 or OKHTTP 3.11) Complete Solution

Ok, so Online & Offline caching using OKHTTP or Retrofit has been causing so many problems for many people on stackoverflow and other forums. There are tons of misleading information and non-working code samples all over the internet.

So, today I will explain how you can implement online & offline caching using Retrofit & OKHTTP with clear steps + How to test and know whether you are getting the data from cache or network.

If you are getting a 504 Unsatisfiable Request (only-if-cached) OR an Unable to resolve host "HOST": No address associated with hostnamethen you can use any of the following solutions.

Before you begin, you must always remember to:

  • Make sure you are using a GET request and not a POST!
  • Always make sure you add .removeHeader("Pragma") as shown below (This lets you override the server's caching protocol)
  • Avoid using the HttpLoggingInterceptor while testing, it can cause some confusion in the beginning. Enable it in the end if you want.
  • ALWAYS ALWAYS ALWAYS delete your app from the device and reinstall it again upon every change in code, if you want to explore using Interceptors. Otherwise changing code while the old cache data is still on the device will cause you lots of confusion and misleading deductions!
  • The order of adding Interceptors to OKHTTPClient object matters!

N.B: If you want to depend on your server's caching protocol for online and offline caching, then don't read the 2 solutions. Just read this article. All you need is to create a cache object and attache it to OKHTTPClient object.


Solution 1: (Longer, but you have full control)

  • Step 1: (Create onlineInterceptor)

       static Interceptor onlineInterceptor = new Interceptor() {
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
            okhttp3.Response response = chain.proceed(chain.request());
            int maxAge = 60; // read from cache for 60 seconds even if there is internet connection
            return response.newBuilder()
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .removeHeader("Pragma")
                    .build();
        }
    };
    
  • Step 2: (Create Offline Interceptor) (Only if you want cache access when offline)

       static Interceptor offlineInterceptor= new Interceptor() {
       @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        if (!isInternetAvailable()) {
            int maxStale = 60 * 60 * 24 * 30; // Offline cache available for 30 days 
            request = request.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .removeHeader("Pragma")
                    .build();
          }
          return chain.proceed(request);
       }
     };
    
  • Step 3: (Create a cache object)

    int cacheSize = 10 * 1024 * 1024; // 10 MB
    Cache cache = new Cache(context.getCacheDir(), cacheSize);
    
  • Step 4: (Add interceptors and cache to an OKHTTPClient object)

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
     // .addInterceptor(provideHttpLoggingInterceptor()) // For HTTP request & Response data logging
        .addInterceptor(OFFLINE_INTERCEPTOR)
        .addNetworkInterceptor(ONLINE_INTERCEPTOR)
        .cache(cache)
        .build();
    
  • Step 5:(If you are using Retrofit, add the OKHTTPClient object to it)

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

DONE!


Solution 2: (Just use a library to do all that for you! But deal with the limitations)

Use OkCacheControl library

  • Step 1 (Create Cache object as shown above)
  • Step 2 (Create an OKHTTPClient object)

         OkHttpClient okHttpClient = OkCacheControl.on(new OkHttpClient.Builder())
         .overrideServerCachePolicy(1, MINUTES)
         .forceCacheWhenOffline(networkMonitor)
         .apply() // return to the OkHttpClient.Builder instance
       //.addInterceptor(provideHttpLoggingInterceptor())
         .cache(cache)
         .build();
    
  • Step 3:(Attach the OKHTTPClient object to Retrofit as shown above)

  • Step 4: (Create a NetworkMonitor Object)

       static OkCacheControl.NetworkMonitor networkMonitor=new 
       OkCacheControl.NetworkMonitor() {
       @Override
        public boolean isOnline() {
        return isInternetAvailable();
       }
      };
    

DONE!


Testing: In order to know whether your device is getting data from the network or from cache, simply add the following code to your onResponse method of Retrofit.

 public void onResponse(Call<List<RetroPhoto>> call, Response<List<RetroPhoto>> response) {
            if (response.raw().cacheResponse() != null) {
                Log.e("Network", "response came from cache");
            }

            if (response.raw().networkResponse() != null) {
                Log.e("Network", "response came from server");
            }
        }

If the device is using the Network, you will get "response came from server".

If device is using Cache, you will get both of the above responses! For more info about this read this article.


For more info about using OKHTTP interceptors go to this page.

查看更多
登录 后发表回答