Let’s be honest, we’ve all been there — frantically refreshing our app, watching the loading spinner spin endlessly as we wait for that critical API response. It’s a dance we’ve perfected, isn’t it? The rhythmic tapping of our fingers, the subtle eye rolls, and the not-so-subtle muttering under our breath. “Come on, just give me the data already!”
When it comes to building applications that interact with external APIs, caching can be a game-changer. API calls can often be slow, resource-intensive, and subject to rate limiting by the API providers. By implementing a robust caching mechanism, you can dramatically reduce the number of API calls, lower response times, and provide a smoother overall user experience.
One of the ways to solve the problem of repeated API calls is to cache/store their responses locally. In this article, we’re going to take a look at how we can use OkHttp
library’s CacheControl
class to store API responses with a time validity.
https://github.com/ishanvohra2/findr?source=post_page—–1384a621c51f——————————–
Implementing the Cache
Defining the cache size and instance
To be able to store the responses for API calls locally in a cache, first, we need to define it and let our client know about the same. In the following snippet, we’re defining the cache by using the Cache
class present in okhttp
library. We’ve set the maximum size of this cache to 5 MB. Then we use the cache()
function while initializing our okhttpclient
parameter.
// Defining a cache of 5 MB size | |
val cacheSize = (5 * 1024 * 1024).toLong() | |
//Initializing instance of Cache class | |
val myCache = Cache(context.cacheDir, cacheSize) | |
//defining okhttpclient instance | |
val okHttpClient = OkHttpClient.Builder() | |
.cache(myCache) | |
.build() |
Defining rules for our cache
Let’s define some basic rules for network cache based on whether the device is connected to the internet or not as follows:
- If the device is connected to the internet: If the last API response was retrieved less than 30 minutes ago then show the cached response otherwise, get the new response and store it in the cache.
- If the device is offline: Use the API response up to 1 day old to keep the app functioning.
You can always define different and more nuanced rules for complex scenarios for your projects but for the sake of simplicity, we’ll use the aforementioned.
The first step is to check if the user has an internet connection or not. To do that we can use the ConnectivityManager
class to gather data and check if the user is connected to the internet or not.
Let’s define a function called hasNetwork()
as shown below:
fun hasNetwork(context: Context): Boolean { | |
val connectivityManager = context | |
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | |
val nw = connectivityManager.activeNetwork ?: return false | |
val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false | |
return when { | |
actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true | |
actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true | |
actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true | |
actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true | |
else -> false | |
} | |
} |
We check if the user is connected to a WiFi, cellular or Bluetooth network and return true or false based on the same.
To implement the rules, we have defined for our cache, we’ll be using a component of OkHttp
library called an Interceptor
. An interceptor is a powerful mechanism that allows you to intercept, process, and potentially modify HTTP requests and responses before they are sent or received by your application. They act like a middleman in the communication flow, giving you more control over how OkHttp
handles network requests.
Within the interceptor, we will provide instructions on when to use the cached response to our okHttpClient
instance.
Let us add the interceptor using the function called addInterceptor()
while building our client instance. We can modify our API request in this function. Within the lambda function, we can get the request object and modify it before finally sending it to the server. To add caching functionality, we use the function called cacheControl()
which takes in parameters belonging to theCacheControl
class.
The first rule we need to implement is if the device has internet, then use the response from 30 minutes ago (if there is one), otherwise get the new response. We use the maxAge()
function to implement the same.
The second rule we need to implement is if the device is disconnected, then use the response which is at most 1 day old. For this, we use maxStale()
function.
Job Offers
// Defining a cache of 5 MB size | |
val cacheSize = (5 * 1024 * 1024).toLong() | |
//Initializing instance of Cache class | |
val myCache = Cache(context.cacheDir, cacheSize) | |
//defining okhttpclient instance | |
val okHttpClient = OkHttpClient.Builder() | |
.cache(myCache) | |
.addInterceptor { chain -> | |
var request = chain.request() | |
request = if (hasNetwork(context)) | |
request | |
.newBuilder() | |
.cacheControl( | |
CacheControl.Builder() | |
.maxAge(30, TimeUnit.MINUTES) | |
.build() | |
) | |
.build() | |
else | |
request | |
.newBuilder() | |
.cacheControl( | |
CacheControl.Builder() | |
.maxStale(1, TimeUnit.DAYS) | |
.build() | |
) | |
.build() | |
chain.proceed(request) | |
} | |
.build() |
Difference between maxAge() and
maxStale()
Both max-stale
and max-age
are directives used in HTTP caching to control how fresh a cached response can be. However, they have distinct meanings:
- max-age: This directive specifies the maximum age of a cached response that a client considers to be fresh. It’s expressed in seconds. A response older than
max-age
seconds is considered stale by the client, and the client will request a fresh response from the server if available. - max-stale: This directive tells the client that it’s willing to accept a stale response, even if it’s older than the
max-age
specified by the server. It also specifies the maximum amount of time the response can be stale. It’s also expressed in seconds.
And that is it, we have now implemented the caching system in our OkHttpClient
implementation. Now, it’s time to build your project and provide a fast and reliable experience for your customers/users.
Don’t worry, if you’re not using OkHttp
library directly and instead using Retrofit library as a wrapper around it. Use the following snippet to implement caching in your project!
class RetrofitClient(private val context: Context) { | |
val cacheSize = (5 * 1024 * 1024).toLong() | |
val instance: Api by lazy { | |
val myCache = Cache(context.cacheDir, cacheSize) | |
val okHttpClient = OkHttpClient.Builder() | |
.cache(myCache) | |
.addInterceptor { chain -> | |
var request = chain.request() | |
request = if (hasNetwork(context)) | |
request | |
.newBuilder() | |
.cacheControl( | |
CacheControl.Builder() | |
.maxAge(30, TimeUnit.MINUTES) | |
.build() | |
) | |
.build() | |
else | |
request | |
.newBuilder() | |
.cacheControl( | |
CacheControl.Builder() | |
.maxStale(1, TimeUnit.DAYS) | |
.build() | |
) | |
.build() | |
chain.proceed(request) | |
} | |
.addInterceptor(HttpLoggingInterceptor().apply { | |
this.level = HttpLoggingInterceptor.Level.BODY } | |
) | |
.build() | |
val retrofit = Retrofit.Builder() | |
.baseUrl("BASE_URL") | |
.addConverterFactory(GsonConverterFactory.create()) | |
.client(okHttpClient) | |
.build() | |
retrofit.create(Api::class.java) | |
} | |
} |
Now, you can go ahead and run your project on a physical device or emulator. To inspect if the caching is indeed working, use the App Inspection tab in Android Studio or use OkHttp
’s logging interceptor to log all network calls.
Conclusion
For API responses that do not change over a long period can be cached via the above technique and would save a lot of resources. Software development always involves making decisions based on certain trade-offs between time and space. In this instance, we traded off for a little bit of space (for storing the API responses), to save time.
If you would like to read more on how to improve performance in your app. Check out this article on debouncing!
Thanks for reading!
This article is previously published on proandroiddev.com