Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrofit2 + OkHttp with RxJava: onNext() callback not being triggered when connected to a network that has no internet #3542

Open
ArcherEmiya05 opened this issue Apr 11, 2021 · 1 comment

Comments

@ArcherEmiya05
Copy link

ArcherEmiya05 commented Apr 11, 2021

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'
@ArcherEmiya05
Copy link
Author

For SO link here it is. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant