I need to implement a catch when an error occur like network related or the API limit has been reached. Now I've seen a lot of good samples yet it seems they are missing something.
This tutorial the handling of cache is based the network status like if a mobile data or Wi-Fi of the device is on/off. It is not the solution since one can be connected to a network but the network has no data or no internet at all.
The ideal flow on me would be
- Cache every time a fetch is success which repeats every 5 seconds.
- Use only the last cached data available if any of the performed fetch failed, if no error then use the fresh data set from online response.
- The cached data can be available for days or weeks and will only be updated again every new fetch is success.
- If no cache yet is available and the very first fetch was failed only then show the error.
My code
interface EndpointServices {
companion object {
private fun interceptor(): Interceptor {
return Interceptor { chain ->
var request: Request = chain.request()
val originalResponse: Response = chain.proceed(request)
val cacheControl: String? = originalResponse.header("Cache-Control")
if (cacheControl == null || cacheControl.contains("no-store") || cacheControl.contains("no-cache") ||
cacheControl.contains("must-revalidate") || cacheControl.contains("max-stale=0")
) {
Log.wtf("INTERCEPT", "SAVE A CACHE")
val cc: CacheControl = CacheControl.Builder()
.maxStale(1, TimeUnit.DAYS)
.build()
request = request.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public")
.cacheControl(cc)
.build()
chain.proceed(request)
} else {
Log.wtf("INTERCEPT", "ONLINE FETCH")
originalResponse.newBuilder()
.removeHeader("Pragma")
.build()
}
}
}
private fun onlineOfflineHandling(): Interceptor {
return Interceptor { chain ->
try {
Log.wtf("INTERCEPT", "TRY ONLINE")
chain.proceed(chain.request())
} catch (e: Exception) {
Log.wtf("INTERCEPT", "FALLBACK TO CACHE")
val cacheControl: CacheControl = CacheControl.Builder()
.maxStale(1, TimeUnit.DAYS)
.onlyIfCached() //Caching condition
.build()
val offlineRequest: Request = chain.request().newBuilder()
.cacheControl(cacheControl)
.build()
chain.proceed(offlineRequest)
}
}
}
fun create(baseUrl: String, context: Context): EndpointServices {
val cacheSize: Long = 10 * 1024 * 1024 // 10 MB
val cache = Cache(context.cacheDir, cacheSize)
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val httpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.callTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor())
.addInterceptor(onlineOfflineHandling())
.build()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(
RxJava2CallAdapterFactory.create()
)
.addConverterFactory(
MoshiConverterFactory.create()
)
.client(httpClient)
.baseUrl(baseUrl)
.build()
return retrofit.create(EndpointServices::class.java)
}
}
Main activity
intervalDisposable = Observable.interval(0L, 5L, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
Log.d("Interval", it.toString())
fetchAssets(UriUtil.assetField, "30")
}
private fun fetchAssets(field: String, limit: String) {
disposable = EndpointServices.create(url, requireContext()).getAssetItems(
field,
limit
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
//Our response here
},
{ error ->
//Error, offline and no cache has been found
Log.wtf("WTF", "${error.message}")
Toast.makeText(context, error.message, Toast.LENGTH_LONG).show()
}
)
}
I am working with @GET
UPDATES Apr 10 2021
I tried to work with Youtube API as an example and this is what the test result.
Mobile Data/Wi-Fi off
- INTERCEPT: TRY ONLINE
- INTERCEPT: FALLBACK TO CACHE
- The response was return (it works!)
Wi-Fi on and connected to a network but has no data/internet connection service
- INTERCEPT: TRY ONLINE
- Waiting for response to timeout?
- INTERCEPT: FALLBACK TO CACHE
- No response was return (WTF?)
Mobile data/Wi-Fi on and internet service is available (Online) works on Youtube API only so far
- INTERCEPT: TRY ONLINE
- INTERCEPT: ONLINE FETCH
- The response was return (it works!)
I tried working on other APIs too but no luck so far only YouTube API works but not as intended yet. I need an approach that could work on almost any API.
UPDATES Apr 11 2021
I updated the code and somewhat managed to make it work almost to what we needed.
interface EndpointServices {
companion object {
private fun interceptor(): Interceptor {
return Interceptor { chain ->
val request: Request = chain.request()
val originalResponse: Response = chain.proceed(request)
val cacheControlStatus: String? = originalResponse.header("Cache-Control")
if (cacheControlStatus == null || cacheControlStatus.contains("no-store") || cacheControlStatus.contains(
"no-cache") ||
cacheControlStatus.contains("must-revalidate") || cacheControlStatus.contains("max-stale=0")
) {
Log.wtf("INTERCEPT", "ORIGINAL CACHE-CONTROL: $cacheControlStatus")
} else {
Log.wtf("INTERCEPT",
"ORIGINAL : CACHE-CONTROL: $cacheControlStatus")
}
Log.wtf("INTERCEPT",
"OVERWRITE CACHE-CONTROL: ${request.cacheControl} | CACHEABLE? ${
CacheStrategy.isCacheable(originalResponse,
request)
}")
originalResponse.newBuilder()
.build()
}
}
private fun onlineOfflineHandling(): Interceptor {
return Interceptor { chain ->
try {
Log.wtf("INTERCEPT", "FETCH ONLINE")
val cacheControl = CacheControl.Builder()
.maxAge(5, TimeUnit.SECONDS)
.build()
val response = chain.proceed(chain.request().newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public, $cacheControl")
.build())
Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}")
response
} catch (e: Exception) {
Log.wtf("INTERCEPT", "FALLBACK TO CACHE ${e.message}")
val cacheControl: CacheControl = CacheControl.Builder()
.maxStale(1, TimeUnit.DAYS)
.onlyIfCached() // Use Cache if available
.build()
val offlineRequest: Request = chain.request().newBuilder()
.cacheControl(cacheControl)
.build()
val response = chain.proceed(offlineRequest)
Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}")
response
}
}
}
fun create(baseUrl: String, context: Context): EndpointServices {
// Inexact 150 MB of maximum cache size for a total of 4000 assets where about 1MB/30 assets
// The remaining available space will be use for other cacheable requests
val cacheSize: Long = 150 * 1024 * 1024
val cache = Cache(context.cacheDir, cacheSize)
Log.wtf("CACHE DIRECTORY", cache.directory.absolutePath)
for (cacheUrl in cache.urls())
Log.wtf("CACHE URLS", cacheUrl)
Log.wtf("CACHE OCCUPIED/TOTAL SIZE", "${cache.size()} ${cache.maxSize()}")
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val httpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.callTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor())
.addInterceptor(onlineOfflineHandling())
.build()
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(
RxJava2CallAdapterFactory.create()
)
.addConverterFactory(
MoshiConverterFactory.create()
)
.client(httpClient)
.baseUrl(baseUrl)
.build()
return retrofit.create(EndpointServices::class.java)
}
}
@GET("search")
fun getVideoItems(
@Query("key") key: String,
@Query("part") part: String,
@Query("maxResults") maxResults: String,
@Query("order") order: String,
@Query("type") type: String,
@Query("channelId") channelId: String,
):
Single<VideoItemModel>
}
MainActivity
EndpointServices.create(url, requireContext()).getVideoItems(
AppUtils.videoKey,
"id,snippet",
"20",
"date",
"video",
channelId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
Log.wtf("RESPONSE", result.toString())
adapter.submitList(result.videoData)
swipeRefreshLayout.isRefreshing = false
logTxt.text = null
},
{ error ->
Log.wtf("WTF", "${error.message}")
swipeRefreshLayout.isRefreshing = false
if (adapter.currentList.isEmpty() || (error is HttpException && error.code() == HttpURLConnection.HTTP_GATEWAY_TIMEOUT)){
adapter.submitList(mutableListOf())
logTxt.text = getString(R.string.swipeToRefresh)
}
}
)
FLOW BASED ON LOGS
WHEN ONLINE
A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: ORIGINAL : CACHE-CONTROL: private
A/INTERCEPT: OVERWRITE CACHE-CONTROL: public, max-age=5 | CACHEABLE? true
A/INTERCEPT: CACHE Response{protocol=http/1.1, code=200, message=, url=https://api.com} NETWORK Response{protocol=h2, code=304, message=, url=https://api.com}
A/RESPONSE: VideoItemModel(.....) WORKING!
COMPLETELY OFFLINE (Wi-Fi/Mobile Data OFF)
A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: FALLBACK TO CACHE Unable to resolve host "api.com": No address associated with hostname
A/INTERCEPT: CACHE Response{protocol=http/1.1, code=200, message=, url=https://api.com} NETWORK null
A/RESPONSE: VideoItemModel(.....) WORKING!
JUST CONNECTED TO A NETWORK BUT REALLY NO INTERNET SERVICE (Wi-Fi/Mobile Data ON)
A/CACHE DIRECTORY: /data/data/com.appname.app/cache
A/CACHE URLS: www.api.com
A/CACHE OCCUPIED/TOTAL SIZE: 228982 157286400
A/INTERCEPT: FETCH ONLINE
A/INTERCEPT: FALLBACK TO CACHE Unable to resolve host "api.com": No address associated with hostname
???WHERE IS THE CALLBACK JUST LIKE THE PREVIOUS ONE???
Also worth mentioning that neither of the line
Log.wtf("INTERCEPT", "CACHE ${response.cacheResponse} NETWORK ${response.networkResponse}")
is being called on this last scenario.