Using domain exceptions in your app is an important step if you want to create abstractions over different 3rd party networking libraries like Retrofit or gRPC. Handling and mapping of these exceptions can quickly become a boilerplate that you and your colleagues have to think about and sooner or later you will probably forget to do it somewhere in your code. I want to explore how this task can be done automatically in a generic and clean way when using the Retrofit library.

Disclaimer: The reasoning about domain exceptions does not apply only to networking but to any layer in your code that accesses 3rd party libraries.

Recap

This post is a follow up to my previous post about mapping into domain exceptions. To quickly summarise what it’s all about — you should abstract all logic behind low level exceptions like IOException or Retrofit HttpException inside the M part of your MV* architecture and not handle these exceptions on the presentation layer. This gives you the opportunity to switch HTTP library or the logic behind detecting NoInternetException without the need to change your presentation layer.

Another disclaimer: I have mentioned that you should do this mapping inside the repository layer but well, we all evolve, and now my opinion is that the correct place to do that is inside your remote data source layer.

First implementation

I’ve created a sample project that will guide us through the process of improving exceptions mapping from manual tedious work to automatic mapping. 

The example is pretty simple: User can perform two actions, download a list of recipes or download a single recipe by its id. These exceptions are defined:

The first solution is based on the previous post — mapping is done in data source

As you can imagine, this approach works but you need to think about this every time you call your Retrofit method and it can be easily forgotten or a new developer on the project can easily miss that. Can we do better?

Custom CallAdapter

Retrofit has the support for custom CallAdapter that you register during initialisation. If you used RxJava with Retrofit you would need to use one so Retrofit can recognize Rx return types like Single or Completable. We use Coroutines now for asynchronous Retrofit calls and they do not require any special CallAdapter since version 2.6.0 but we can still register one and use it as a generic place where mapping of exceptions can be done.

First we need to create an instance of CallAdapter.Factory

We first check if the return type is really a Call (even though you do not specify it directly in Retrofit interface, it’s still a Call underground) and if it is, we ask Retrofit to give us the built-in CallAdapter which we pass to the custom ErrorsCallAdapter

We delegate all implementation of CallAdapter class to the retrieved CallAdapter and only override the adapt method where we intercept the call and wrap it with the CallWithErrorHandling where the actual magic happens.

 

The same pattern with delegating implementation of Call interface to the original Call is applied and only the enqueue method is overrode with the logic of mapping the exceptions.

At this point we have a generic mapping of ApiException or NoInternetException in one place! But.. there is still one issue. How would we solve the RecipeNotFoundException? The remote data source still needs to have try/catch to handle this case:

 

But it’s still an improvement — now we need to handle only special cases and in 90% of other cases it’s handled for us. But can we do even better?

Invocation with custom annotations

We can leverage another great feature of the Retrofit/OkHttp which is the pair of  okhttp3.Request.tag(Class<*>) method and retrofit2.Invocation class. Each OkHttp request can contain a tag which can be any object instance. When Retrofit uses OkHttp under the hood while performing the HTTP request it sets the instance of Invocation class as this tag. When you look how the class looks it’s only a wrapper over java.lang.reflect.Method and list of arguments of the interface function

Through Method we have access for example to the annotations that are defined in the Retrofit interface on the called method. Can you see where I’m heading to? We can create custom annotation

that contains a reference to the KClass of HttpExceptionMapper class

and now we can create this mapper instance for the RecipeNotFoundException

How to tie all these pieces together? We update the CallWithErrorHandling from previous section with this improvement:

In the method mapExceptionOfCall first the Invocation is retrieved, then the ExceptionsMapper annotation is queried and if it can be found, new instance is created and passed to the mapToDomainException helper function which we know from the beginning of the post

Now the only thing missing is to mark the method in Retrofit interface with the annotation

and voilá. The remote data source now may look like this

and it does not need to handle any exception mapping.

This approach has also one additional benefit — you can have a setup of your networking Retrofit code in one gradle library module and your mapping logic can be in a feature module that has a dependency on this module. You do not need to combine all of this together in one module.

Summary

We have leveraged a great mechanism of combination of Retrofit/OkHttp libraries to mitigate the need for handling remote call exceptions in the data sources/repositories. By passing all the logic to the low level CallAdapter and retrieving the Invocation instance of the Retrofit call we can keep our data sources clean. But even if you don’t want to mess with the annotations and Invocations you can still use CallAdapter for common error handling or mapping of the responses.

You can find sample code here. Feel free to comment or write to me on my Twitter. Thanks for your time!

Last disclaimer: The sample code is mainly for demonstration purposes, the code can be optimised by eg. caching instances of the ExceptionMapper s and so on.

Leave a Reply

Your email address will not be published. Required fields are marked *