retrofit
retrofit copied to clipboard
Retrofit2 + OkHttp with RxJava: onNext() callback not being triggered when connected to a network that has no internet
Whenever there is an error like network related errors, we will use the last cached data from the client instead. It works but only when not connected to any network. If a client is connected to any network where internet service is not really available the callbacks is no longer working.
So far I do not know which side this error came from base on this 3 libraries.
Here we are using YouTube API as a sample.
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.
Dependencies
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
For SO link here it is. Thank you!