Using Retrofit Interceptors to check network connection in Android and testing it

theplebdev

Tristan Elliott

Posted on January 23, 2024

Using Retrofit Interceptors to check network connection in Android and testing it

Table of contents

  1. Too long didn't read. Give me the code!!!
  2. The problem I am trying to solve
  3. What is a Interceptor
  4. How to monitor the Network
  5. Creating the Interceptor
  6. Adding Interceptor to Retrofit
  7. Catching the exception
  8. Testing code

My app on the Google play store

GitHub code

Too long didn't read (TLDR)

The problem I am trying to solve

  • Currently I have no way to differentiate between types of network failures in my application. Being able to differentiate is a must. For example, our application should behave differently to a 401 response vs a complete absence of any network.
  • In this tutorial we are only going to talk about notifying the user if they make a request and no network is available at all. Not the most exciting, but it is a good starting point to get a better understanding of Interceptors

Moving forward

  • from this point on, I will assume, you have a basic understanding of Retrofit. To get the most out of this tutorial I would actually suggest you have a retrofit client already implemented in your application.

What is a Interceptor

  • As the documentation states: Interceptors are a powerful mechanism that can monitor, rewrite, and retry calls.
  • There are two types of interceptors: Application Interceptors and Network Interceptors. We are going to be using a Application Interceptor simply because, as the documentation states: Are always invoked once. So on every request our application interceptor will be able to check the network's availability.

How to monitor the Network

  • To do the actual checking of the network we are going to rely on the ConnectivityManager object. This will give us the necessary API to check the network before sending a request.
  • Our full code is going to look like this:
/**
 * LiveNetworkMonitor is a class meant to check the application's network connectivity
 *
 * @param context a [Context] object. This is what is used to get the system's connectivity manager
 * */
class LiveNetworkMonitor @Inject constructor(
    private val context: Context
):NetworkMonitor {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    override fun isConnected(): Boolean {
        val network =connectivityManager.activeNetwork
        return network != null
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Notice how I am using Hilt to inject the Context object. Also, I want to draw your attention to the .activeNetwork method call. Now I am not 100% sure if this is the proper way to check the networks availability. However, it does work and here is the documentation to support my case for using it, Full documentation,: This will return null when there is no default network, or when the default network is blocked.

  • If you are curious about the NetworkMonitor, it is just a simple interface I have created to adhere to the principle of program to an interface not an implementation and to make testing easier:

/**
 * NetworkMonitor is a interface meant to act as the API to all network monitoring related issues
 *
 * @param isConnected a function used to determine if the application is connected to a network or not
 * */
interface NetworkMonitor {
    fun isConnected():Boolean
}
- This might seem like a bunch of unnecessary Object oriented coding techniques. However, this will make testing 100000% easier. So please use the interface

Enter fullscreen mode Exit fullscreen mode

Creating the Interceptor

  • To create the an Interceptor we need to extend the Interceptor interface. Which will allow us to override the intercept() function and fill it with our network monitoring logic. So our full interceptor will look like this:
/**
 * NetworkMonitorInterceptor is a [application-interceptor](https://square.github.io/okhttp/features/interceptors/#application-interceptors)
 * meant to first check the status of the Network before sending the request
 *
 * @param liveNetworkMonitor a [NetworkMonitor] implementation that handles all of the actual network logic checking
 * */
class NetworkMonitorInterceptor @Inject constructor(
    private val liveNetworkMonitor:NetworkMonitor
): Interceptor {


    @Throws(NoNetworkException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val request: Request = chain.request()

        if(liveNetworkMonitor.isConnected()){
            return chain.proceed(request)
        }else{

            throw NoNetworkException("Network Error")
        }

    }
}

Enter fullscreen mode Exit fullscreen mode
  • I want to call your attention to chain.request(), because as the documentation states: A call to chain.proceed(request) is a critical part of each interceptor’s implementation. This simple-looking method is where all the HTTP work happens, producing a response to satisfy the request.
  • Also, you may notice the NoNetworkException. This is a custom exception that we will use to identify when there is no network. It's just a class that extends the IOException:
 class NoNetworkException(message:String): IOException(message)
Enter fullscreen mode Exit fullscreen mode
  • We have to extend IOException because that is the only type of exception allowed inside of the intercept() function.
  • Now we can move on to adding this interceptor to our Retrofit client

Adding Interceptor to Retrofit

  • To creating and add the interceptor to Retrofit is actually very straight forward. All we have to do is create a OkHttpClient(What Retrofit uses under the hood) and call addInterceptor(). Here is how I created and added the Interceptor in a Hilt module:
@Module
@InstallIn(SingletonComponent::class)
object SingletonModule {
   @Provides
    fun provideNetworkMonitor(
        @ApplicationContext appContext: Context
    ): NetworkMonitor{
        return LiveNetworkMonitor(appContext)
    }

    @Singleton //scope binding
    @Provides
    fun providesTwitchClient(
        liveNetworkMonitor: NetworkMonitor
    ): TwitchClient {
         val monitorClient = OkHttpClient.Builder()     
    .addInterceptor(NetworkMonitorInterceptor(liveNetworkMonitor))
            .build()
        return Retrofit.Builder()
            .baseUrl("https://api.twitch.tv/helix/")
            .addConverterFactory(GsonConverterFactory.create())
            .client(monitorClient)
            .build().create(TwitchClient::class.java)
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Notice that we simply call .client(monitorClient) which will add out custom Interceptor.

Catching the exception

  • In the Network layer of my application I have this code that reaches out the the Twitch servers and awaits a response:
class TwitchRepoImpl @Inject constructor(
    private val twitchClient: TwitchClient
) : TwitchRepo  {


    override suspend fun getFollowedLiveStreams(
        authorizationToken: String,
        clientId: String,
        userId: String
    ): Flow<Response<List<StreamInfo>>> = flow {
        emit(Response.Loading)

        val response = twitchClient.getFollowedStreams(
            authorization = "Bearer $authorizationToken",
            clientId = clientId,
            userId = userId
        )

        val emptyBody = FollowedLiveStreams(listOf<StreamData>())
        val body = response.body() ?: emptyBody
        val exported = body.data.map { it.toStreamInfo() }

        if (response.isSuccessful) {
            emit(Response.Success(exported))
        } else {
            emit(Response.Failure(Exception("Error!, code: {${response.code()}}")))
        }
    }.catch { cause ->
        when (cause) {
        is NoNetworkException -> {

            emit(Response.Failure(Exception("Network error, please try again later")))
        }

        else -> {
            emit(Response.Failure(Exception("Error! Please try again")))
        }
    }
    }
}

Enter fullscreen mode Exit fullscreen mode
  • You can look at the catch{} on our flow. This will catch our NoNetworkException when it is thrown by our interceptor.

Testing code

  • I ran out of time writing this blog post, meaning I can't go into detail about testing. So we will have to settle with the GitHub link to the testing code: HERE

Conclusion

  • Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
💖 💪 🙅 🚩
theplebdev
Tristan Elliott

Posted on January 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related