Doing network caching with Retrofit 2 in Android

The following example will walk through the steps for doing caching with Retrofit 2. The example here will be using an Github API for getting a Github user account information. The big picture is to make a request to an api, the first request will get the response from the server, the following requests within 1 minute will be getting the response from cached data.

1. gradle dependencies

// rxandroid
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

// Retrofit 2 for network tasks
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
implementation 'com.google.code.gson:gson:2.8.2'

2. Create a pojo object. This is to hold the Github account information which will be obtained when making the network request to Github api. GithubAccount.kt

import com.google.gson.annotations.SerializedName
data class GithubAccount(
        @SerializedName("login") var login : String = "",
        @SerializedName("id") var id : Int = 0,
        @SerializedName("avatar_url") var avatarUrl : String = "",
        @SerializedName("created_at") var createdAt : String = "",
        @SerializedName("updated_at") var updatedAt : String = "")

3. Create a Retrofit network interface. GithubApi.kt

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

interface GithubApi {
    @GET("/users/{username}")
    fun getGithubAccountObservable(@Path("username") username: String): Observable<Response<GithubAccount>>
}

4. Create the cache object. The cache size is set to 10MB, which means old cached data will be over written with newer data. getCacheDir() is a method from Activity class and it returns the Android’s default cache directory.

val cacheSize = 10 * 1024 * 1024 // 10 MB
val httpCacheDirectory = File(getCacheDir(), "http-cache")
val cache = Cache(httpCacheDirectory, cacheSize.toLong())

5. Create the network cache interceptor. Setting the max age to 1 minute, which means the cache will be valid for 1 minute. For example, the first request will be getting response from the server, and the following request made within 1 minute of the first request will be getting response from the cache.

val networkCacheInterceptor = Interceptor { chain ->
    val response = chain.proceed(chain.request())

    var cacheControl = CacheControl.Builder()
            .maxAge(1, TimeUnit.MINUTES)
            .build()

    response.newBuilder()
            .header("Cache-Control", cacheControl.toString())
            .build()
}

6. Create the logging interceptor. This is not required for the cache, but it’s nice to have some logging statements when debugging.

val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY

7. Create the httpClient. Configure it with cache, network cache interceptor and logging interceptor

val httpClient = OkHttpClient.Builder()
        .cache(cache)
        .addNetworkInterceptor(networkCacheInterceptor)
        .addInterceptor(loggingInterceptor)
        .build()

8. Create the Retrofit with the httpClient.

val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .client(httpClient)
        .build()

9. Build the gitHubApi with Retrofit and do the network request. The result will be shown on a toast message. There is also a logging telling you whether the response was from the server or cache.

val githubApi = retrofit.create(GithubApi::class.java)
githubApi.getGithubAccountObservable("google")
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeWith(object : DisposableObserver<Response<GithubAccount>>() {
            override fun onNext(response: Response<GithubAccount>) {

                if (response.raw().cacheResponse() != null) {
                    Log.d("Network", "response came from cache")
                }

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

                Toast.makeText(applicationContext, response.body().toString(), Toast.LENGTH_SHORT).show()
            }

            override fun onComplete() {}

            override fun onError(e: Throwable) {
                e.printStackTrace()
            }
        })

Step 4 to 9 in one Activity class.

import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.View
import android.widget.Toast
import com.example.myapplication.data.GithubAccount
import com.example.myapplication.data.GithubApi
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.observers.DisposableObserver
import io.reactivex.schedulers.Schedulers
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        loadGithubAccount()
    }


    fun loadGithubAccount() {

        // Create a cache object
        val cacheSize = 10 * 1024 * 1024 // 10 MB
        val httpCacheDirectory = File(getCacheDir(), "http-cache")
        val cache = Cache(httpCacheDirectory, cacheSize.toLong())

        // create a network cache interceptor, setting the max age to 1 minute
        val networkCacheInterceptor = Interceptor { chain ->
            val response = chain.proceed(chain.request())

            var cacheControl = CacheControl.Builder()
                    .maxAge(1, TimeUnit.MINUTES)
                    .build()

            response.newBuilder()
                    .header("Cache-Control", cacheControl.toString())
                    .build()
        }

        // Create the logging interceptor
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY


        // Create the httpClient, configure it
        // with cache, network cache interceptor and logging interceptor
        val httpClient = OkHttpClient.Builder()
                .cache(cache)
                .addNetworkInterceptor(networkCacheInterceptor)
                .addInterceptor(loggingInterceptor)
                .build()

        // Create the Retrofit with the httpClient
        val retrofit = Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(httpClient)
                .build()

        // Build the gitHubApi with Retrofit and do the network request
        val githubApi = retrofit.create(GithubApi::class.java)
        githubApi.getGithubAccountObservable("google")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeWith(object : DisposableObserver<Response<GithubAccount>>() {
                    override fun onNext(response: Response<GithubAccount>) {

                        if (response.raw().cacheResponse() != null) {
                            Log.d("Network", "response came from cache")
                        }

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

                        Toast.makeText(applicationContext, response.body().toString(), Toast.LENGTH_SHORT).show()
                    }

                    override fun onComplete() {}

                    override fun onError(e: Throwable) {
                        e.printStackTrace()
                    }
                })
    }
}

References:

https://futurestud.io/tutorials/retrofit-2-activate-response-caching-etag-last-modified
https://stackoverflow.com/questions/23429046/can-retrofit-with-okhttp-use-cache-data-when-offline
https://stackoverflow.com/questions/45904054/retrofit-2-okhttpclient-caching-not-working

Search within Codexpedia

Custom Search

Search the entire web

Custom Search