Table of contents
- Too long didn't read. Give me the code!!!
- The problem I am trying to solve
- What is a Interceptor
- How to monitor the Network
- Creating the Interceptor
- Adding Interceptor to Retrofit
- Catching the exception
- 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
}
}
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 ofprogram 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
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")
}
}
}
- 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)
- 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 calladdInterceptor()
. 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)
}
}
- 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")))
}
}
}
}
- You can look at the
catch{}
on our flow. This will catch ourNoNetworkException
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.
Top comments (0)