Android Retrofit OkHttp Offline Caching

Filed Under: Android

In this tutorial, we’ll be discussing and implementing Offline caching in our android application.
We’ll be using Retrofit and Okhttp libraries.

Offline Caching

Opening your application with no internet and seeing no previous data is a very common occurrence.
Two ways to deal with loading network requests that come first to our minds are:

  • Shared Preferences
  • SQLite

Using either of them comes with its fair share of cons (and more code to write).

While Adding Data in SharedPreferences is easy. It’s time-consuming to retrieve the required data. Plus scalability is an issue.

SQLite with tables makes it hard to do many changes in the future. Plus SQLite operations are heavy.

Why use both when OkHtttp caches the HTTP responses built-in?

Caching Requests

We know that OkHttp is the default HttpClient for Retrofit.
OkHttp comes with a powerful component Interceptors.

Interceptors are generally of two types:

  • Application Interceptors – Gets you the final response.
  • Network Interceptors – To intercept intermediate requests.

Using Interceptors you can read and modify the requests. And obviously, we’ll add Cache control on the responses.

Cache-control is an header used to specify caching policies in client requests and server responses.

You cannot cache POST requests. Only GET requests can be cached.

Inside the interceptors, you need to get chain.request() to get the current request and add Cache options to it.

For example, we can add a header “Cache-Control” to the request as: "public, only-if-cached, max-stale=60"

Then do a chain.proceed(request) to proceed with the modified request to return the response.

max-age vs max-stale

max-age is the oldest limit ( lower limit) till which the response can be returned from the cache.
max-stale is the highest limit beyond which cache cannot be returned.

In the following section, we’ll do a Retrofit Request with OkHttp as the Client and using RxJava.
We’ll cache the requests such that they can be displayed the next time if there is no internet/problem in getting the latest request.

Project Structure

android retrofit offline caching project

The app‘s build.gradle is given below:

implementation('com.squareup.retrofit2:retrofit:2.1.0') {
        exclude module: 'okhttp'
    implementation ''
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
    implementation 'com.squareup.okhttp3:okhttps:3.10.0'

    implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'


The code for the activity_main.xml layout is given below:

<?xml version="1.0" encoding="utf-8"?>
< xmlns:android=""

        android:text="Hello World! See this space for jokes"
        app:layout_constraintTop_toTopOf="parent" />

        android:text="GET RANDOM JOKE"
        app:layout_constraintTop_toBottomOf="@+id/textView" />


The code for the is given below:

package com.journaldev.androidretrofitofflinecaching;

import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface APIService {

    String BASE_URL = "";

    Observable<Jokes> getRandomJoke(@Path("path") String path);

The code for the model class is given below:

package com.journaldev.androidretrofitofflinecaching;


public class Jokes {

    public String url;
    public String icon_url;
    public String value;

The code for the is given below:

package com.journaldev.androidretrofitofflinecaching;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;


import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

import static com.journaldev.androidretrofitofflinecaching.APIService.BASE_URL;

public class MainActivity extends AppCompatActivity {

    TextView textView;
    Button btnGetRandomJoke;

    APIService apiService;

    protected void onCreate(Bundle savedInstanceState) {

        textView = findViewById(;
        btnGetRandomJoke = findViewById(;


        btnGetRandomJoke.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {



    private void setupRetrofitAndOkHttp() {

        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();

        File httpCacheDirectory = new File(getCacheDir(), "offlineCache");

        //10 MB
        Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);

        OkHttpClient httpClient = new OkHttpClient.Builder()

        Retrofit retrofit = new Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create(new Gson()))

        apiService = retrofit.create(APIService.class);


    public void getRandomJokeFromAPI() {
        Observable<Jokes> observable = apiService.getRandomJoke("random");
                .map(new Function<Jokes, String>() {
                    public String apply(Jokes jokes) throws Exception {
                        return jokes.value;
                }).subscribe(new Observer<String>() {
            public void onSubscribe(Disposable d) {

            public void onNext(String s) {

            public void onError(Throwable e) {
                Toast.makeText(getApplicationContext(), "An error occurred in the Retrofit request. Perhaps no response/cache", Toast.LENGTH_SHORT).show();

            public void onComplete() {



    private Interceptor provideCacheInterceptor() {
        return new Interceptor() {
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                Response originalResponse = chain.proceed(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-stale=0")) {

                    CacheControl cc = new CacheControl.Builder()
                            .maxStale(1, TimeUnit.DAYS)

                    request = request.newBuilder()

                    return chain.proceed(request);

                } else {
                    return originalResponse;


    private Interceptor provideOfflineCacheInterceptor() {
        return new Interceptor() {
            public Response intercept(Chain chain) throws IOException {
                try {
                    return chain.proceed(chain.request());
                } catch (Exception e) {

                    CacheControl cacheControl = new CacheControl.Builder()
                            .maxStale(1, TimeUnit.DAYS)

                    Request offlineRequest = chain.request().newBuilder()
                    return chain.proceed(offlineRequest);

The order of the interceptors in the OkHttpClient Builder is important.
The addNetworkInterceptor adds the cache control to the request.

In the addInterceptor, provideOfflineCacheInterceptor is called. If there is an exception which would typically be a ConnectException or a NoRouteFoundException, the request is retried again, this time with a header to get the response from the Cache.

Alternatively, you can set the cache in the header as:

return originalResponse.newBuilder()
                            .header("Cache-Control", "public, max-stale=" + 60 * 60 * 24)

It’s a good practice to add removeHeader("Pragma").

The output of the above application in action is given below:

android retrofit offline caching output

So we’ve disabled the wifi on our device after the first Retrofit Request.
And we are still able to load the response from the Http Cache.

This brings an end to this tutorial. You can download the project from the link below:


  1. Bitwise says:

    Great tutorial however there is wrong with the flow here, if the app does not yet perform fetch before or the user clear the cache then you will hit HTTP 504 Unsatisfiable Request (only-if-cached)

  2. K Pradeep Kumar Reddy says:

    Can we cache only certain requests instead of an interceptor that caches all the requests ?

  3. KANFUSED says:

    Why all peoples declare only 5 MB or 10 MB cache size?
    how much cache size we can set?

  4. Stefanus Anggara says:

    Sorry, I got crash at provideCacheInterceptor(). it says that we can’t have 2 .proceed in interceptor.
    So I use this commented code:
    will it give same result?

  5. Stefanus Anggara says:

    Thanks for the tutorial. btw, where is exactly I should put removeHeader(“pragma”) in the interceptors?
    and actually I still have no idea about code “String cacheControl = originalResponse.header(“Cache-Control”);” and its below.
    Can you explain it more?

  6. Steve NDENDE says:

    There is an error at the line : implementation ‘com.squareup.okhttp3:okhttps:3.10.0’

    You put okhttps instead of okhttp

    Nice tutorial, i like

  7. SadBa says:

    Hello. Thanks for the tutorial. I tried to implement caching method in a recyclerView with a list of values. But the caching method does not work. Can you help?

    1. Anupam says:

      Can you log and paste the headers here?

      1. SadBa says:

        I got this “http 504 unsatisfied request retrofit””
        I can share my code if you want

        1. Anupam says:

          504 means that the interceptor needs to use your cache in the request but it did not find any cache.

          Make sure removeHeader(“pragma”) is set.
          The request type isn’t POST.

          1. Bitwise says:

            Yes but what if this is the first time you fetch the data (first use of the app) meaning that there is literally no cache at all. I think the sample above did not handle that.

Comments are closed.

Generic selectors
Exact matches only
Search in title
Search in content