1 Problem Scenario
A network request interface that is requested when the app starts, when MainActivity enters, and even every time onResume resumes. The actual situation may be different, but there are situations like this where an interface needs to be requested more than once. We need the data of the interface, but the actual data doesn’t change that often. Requesting the network each time may result in wasted network resources and waiting.
2. Solution to the problem
2.1 No actual network request is executed and no data is returned
For example, through the data update interface function, data is the same as there is no need to update the interface. You can directly not call the request interface method during a certain period of time to reduce invalid refresh of the interface and achieve the effect of limiting the request interval
2.2 Caching requested data results
- Cache to memory (variable or LRU)
- Cache to database
- Cache to disk
If the same request is made within the same period of time, the cached data is returned.
This time around, I’m going to look at OkHttp’s built-in cache-to-disk capabilities.
3. Problem solving
3.1 Enabling OkHttp Caching
3.1.1 Initial configuration of OkHttp cache
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
// Set the cache function for OkHttp
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
.build()
}
Copy the code
Okhttpclient.builder (). Cache(cache(File(app.externalcachedir, “net”), diskCacheSize) Whether or not caching is actually triggered depends on the configuration of the request header that returns the data.
3.1.2 Manually setting an effective cache duration for the request header that returns data
Interceptor set for OkHttp
private fun Request.isCachePostRequest(a): Boolean = run {
url.toString().contains(APP_INFO_URL, true)}class CachePostResponseInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// Initiate a request
var response = chain.proceed(request)
// Get the request result
// Set the cache for this interface
if (response.request.isCachePostRequest()) {
response = response.newBuilder()
.removeHeader("Pragma")
// Cache for 60 seconds
.addHeader("Cache-Control"."max-age=60")
.build()
}
return response
}
}
Copy the code
There are several ways to set the Cache for the interface data. Here, the Cache is set by setting the request header parameter cache-control. The Cache duration is 60 seconds.
Then add the interceptor to OkHttp
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
// Set the cache function for OkHttp
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
// Add an interceptor to the return data Settings cache
.addNetworkInterceptor(CachePostResponseInterceptor())
.build()
}
Copy the code
This looks like it should be done, but to verify whether the cache needs to add a log or go through an interceptor, add a temporary interceptor for printing.
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
// Set the cache function for OkHttp
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
// A temporary interceptor for printing logs
.addInterceptor {
val request = it.request()
val response = it.proceed(request)
Timber.e("cacheResponse: ${response.cacheResponse} networkResponse: ${response.networkResponse}")
response
}
// Add an interceptor to the return data Settings cache
.addNetworkInterceptor(CachePostResponseInterceptor())
.build()
}
Copy the code
Here why CachePostResponseInterceptor interceptor is addNetworkInterceptor way to add, and log print interceptor is by adding the first addInterceptor method does not explain, Explaining how the OkHttp interceptor works and how the chain of responsibility design pattern works can be covered in several other articles.
Let’s run it and see what happens.
E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78) cacheResponse: null networkResponse: The Response {= HTTP / 1.1 protocol, code = 200, message =, url =... } E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78) cacheResponse: Null networkResponse: Response{protocol= HTTP /1.1, code=200, message=, url=... }Copy the code
If the interval between two requests is 60 seconds, cacheResponse is null and networkResponse has a value.
In this case, the request data is not cached successfully. Normally, the first request for cacheResponse is null and networkResponse has a value. The second request for cacheResponse has a value and networkResponse is null.
Why didn’t OkHttp cache our interface data? Let’s take a look at how OkHttp caches data.
3.1.3 OkHttp cache data working logic
The job of caching data in OkHttp is left to the CacheInterceptor interceptor
Looking at the code of the CacheInterceptor class, you can see that the Cache is saved when the network request data is returned and the Cache object reference is present. Cache(Cache(File(app.externalCachedir, “net”), diskCacheSize))).
.valresponse = networkResponse!! .newBuilder() .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build()if(cache ! =null) {
if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
// A critical point
// Offer this request to the cache.
val cacheRequest = cache.put(response)
return cacheWritingResponse(cacheRequest, response).also {
if(cacheResponse ! =null) {
// This will log a conditional cache miss only.
listener.cacheMiss(call)
}
}
}
}
...
Copy the code
Take a look at the Cache put method
internal fun put(response: Response): CacheRequest? {
val requestMethod = response.request.method
if (HttpMethod.invalidatesCache(response.request.method)) {
try {
remove(response.request)
} catch (_: IOException) {
// The cache cannot be written.
}
return null
}
// Only GET requests are supported
if(requestMethod ! ="GET") {
// Don't cache non-GET responses. We're technically allowed to cache HEAD requests and some
// POST requests, but the complexity of doing so is high and the benefit is low.
return null
}
if (response.hasVaryAll()) {
return null
}
val entry = Entry(response)
var editor: DiskLruCache.Editor? = null
try{ editor = cache.edit(key(response.request.url)) ? :return null
entry.writeTo(editor)
return RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
return null}}Copy the code
As you can see, only cached GET requests are supported, not GET requests that return NULL directly. Look at our request interface, it’s a POST request!
@POST(APP_INFO_URL)
suspend fun appInfo(@Body map: Map<String, String? >): Response<AppInfo>
Copy the code
OkHttp caches data only for GET requests, which is reasonable, but we sometimes encounter POST requests that need to cache data. Such a situation may indicate that the back-end write interface request mode is not appropriate, should the backend change? Not really.
The Cache class cannot inherit.
Change it yourself. How? There are two ideas
- copy
OkHttp
cachedCache
Classes andCacheInterceptor
Class, modifyCache
theput
Method support cachingPOST
Request and then copy inCacheInterceptor
The classCache
Class declaration reference to copy modifiedCache
Class object, will be modifiedCacheInterceptor
Class to add objects toOkHttp
List of interceptors.
This is something you can find on the web, but I think there’s too much duplication of code, adding classes with similar functionality (old Cache class and new Cache class, old CacheInterceptor class and new CacheInterceptor class).
- in
OkHttp
The cache interceptor processes data that needs to be cached before it worksPOST
The request toGET
First pass the cache level (if there is a valid cache data directly returned to the cache data), and then restore to before making the actual network requestPOST
Request to request data correctly, wait for the request data back againPOST
The request toGET
(to cache data).
This approach, which requires only two interceptors, is the approach I took.
3.2 Let OkHttp cache Post requests
3.2.1 inOkHttp
The cache interceptor processes data that needs to be cached before it worksPOST
The request toGET
request
The problem with this interceptor is to tell the CacheInterceptor that the interface data is cacheable and that if there is a valid cache data, it will return the cache data directly.
/** * POST converts to GET */
class TransformPostRequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
/ / cache
if (request.isCachePostRequest()) {
val builder = request.newBuilder()
// Change POST to GET
.method("GET".null)
.cacheControl(
CacheControl.Builder()
.maxAge(60, TimeUnit.SECONDS)
.build()
)
/ / save the body
saveRequestBody(builder, request.body)
request = builder.build()
}
return chain.proceed(request)
}
}
Copy the code
NewBuilder ().method(“GET”, request.body) to construct a new request is a GET request, but when you run it, you will find that the program crashes
Looking at the Method method, he doesn’t let us set the RequestBody for GET requests
open fun method(method: String, body: RequestBody?).: Builder = apply {
require(method.isNotEmpty()) {
"method.isEmpty() == true"
}
if (body == null) { require(! HttpMethod.requiresRequestBody(method)) {"method $method must have a request body."}}else {
require(HttpMethod.permitsRequestBody(method)) {
"method $method must not have a request body."}}this.method = method
this.body = body
}
Copy the code
@kotlin.internal.InlineOnly
public inline fun require(value: Boolean, lazyMessage: () -> Any): Unit {
contract {
returns() implies value
}
if(! value) {val message = lazyMessage()
throw IllegalArgumentException(message.toString())
}
}
Copy the code
@JvmStatic // Despite being 'internal', this method is called by popular 3rd party SDKs.
fun permitsRequestBody(method: String): Boolean = !(method == "GET" || method == "HEAD")
Copy the code
The RequestBody is our request parameter information and must be saved otherwise the request parameters will be lost. What do we do? We can only reflect it to him.
private fun saveRequestBody(builder: Request.Builder, body: RequestBody?). {
val bodyField = builder.javaClass.getDeclaredField("body")
bodyField.isAccessible = true
bodyField.set(builder, body)
}
Copy the code
3.2.2 reduction forPOST
Request to make the actual request, wait for the request data to come back againPOST
The request toGET
To cache data
The interceptor needs to handle the problem of making the network request correctly and telling the CacheInterceptor when the network request data comes back that the interface data needs to be cached.
/** * cache of Response */
class CachePostResponseInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
// Before the actual request
if (request.isCachePostRequest()) {
request = request.newBuilder()
.method("POST", request.body)
.build()
}
// Initiate a request
var response = chain.proceed(request)
// Get the request result
// Cache for this interface
if (response.request.isCachePostRequest()) {
val builder = response.request.newBuilder()
// Change POST to GET
.method("GET".null)
/ / save the body
saveRequestBody(builder, request.body)
response = response.newBuilder()
.request(builder.build())
.removeHeader("Pragma")
// Cache for 60 seconds
.addHeader("Cache-Control"."max-age=60")
.build()
}
return response
}
}
Copy the code
Set up an interceptor for OkHttp
@Singleton
@Provides
fun provideOkHttpClient(app: Application): OkHttpClient {
// 10m
val diskCacheSize = 10L shl 20
return OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
// Set the cache function for OkHttp
.cache(Cache(File(app.externalCacheDir, "net"), diskCacheSize))
.addInterceptor(TransformPostRequestInterceptor())
.addInterceptor {
val request = it.request()
val response = it.proceed(request)
Timber.e("cacheResponse: ${response.cacheResponse} networkResponse: ${response.networkResponse}")
response
}
// Add an interceptor to the return data Settings cache
.addNetworkInterceptor(CachePostResponseInterceptor())
.build()
}
Copy the code
Run it and see what happens.
E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78) cacheResponse: null networkResponse: The Response {= HTTP / 1.1 protocol, code = 200, message =, url =... } E/NetWorkModule$provideOkHttpClient$$inlined$-addInterceptor: (Interceptor.kt:78) cacheResponse: The Response {= HTTP / 1.1 protocol, code = 200, message =, url =... } networkResponse: nullCopy the code
If the interval between two requests is 60 seconds, the first request does not cache data, and the actual network request is sent, and the data returned should be cached. CacheResponse is null and networkResponse has a value.
The second request has cached data and returns the cached data directly without making the actual network request. CacheResponse has a value and networkResponse is null.
The actual log print was as expected, and the interface data was successfully cached and returned.
The final conclusion
The idea of having OkHttp cache POST requests using interceptors
The first step is to tell the CacheInterceptor that the interface data is cacheable before the CacheInterceptor works. If there is a valid cache, it will return the cache data directly.
The second step is to revert to a POST request before making the actual network request
Third, when the network request data comes back, tell the CacheInterceptor that the interface data needs to be cached.
All relevant codes have been posted in this article.