Legacy Project Refactoring: Handling API Errors with Retrofit2 and RxJava

5.0 / 5.0
Article rating

Legacy projects have their own peculiarities, and working with them can be a challenge. Such projects are constantly expanding, and developers are adding new API requests, technologies, frameworks, libraries, and more. An architecture that was once great becomes outdated and can’t handle so many extra structures. This is when the time comes for refactoring.

I’m an Android developer at Mobindustry who is currently working on a five-year-old project that continues to evolve rapidly. Because it’s rather old, there are lots of APIs we use to make different features work and address the backend. Recently, the app struggled to sustain all these API requests, and I needed to find a solution.

For many developers including myself, Retrofit has become a standard tool for network interactions

I chose RxJava and Retrofit as my main tools to solve this issue. RxJava is a reactive programming framework that makes it easy to model real-world situations and manage asynchronous operations.

Retrofit is a flexible and convenient framework that helps to establish effective and fast API management. It’s a type-safe HTTP client for Android and Java. For many developers including myself, it has become a standard tool for network interactions. It makes sending requests and receiving responses extremely easy.

To receive API responses as objects, you can use converters. With the help of an additional adapter, you can receive not only a Call type of object but also reactive types of objects like these:

  • Observable, Observable<Response>, and Observable<Result>
  • Flowable, Flowable<Response>, and Flowable<Result>
  • Single, Single<Response>, and Single<Result>
  • Maybe, Maybe<Response>, and Maybe<Result>
  • Completable, for which reactive components are dismissed

T stands for the type of object we get.

In this article, I’ll explore how you can get an out-of-the-box Subscriber or Observer to process the data you get from Retrofit in RxJava. This will allow you to process all errors in one place: the app’s business logic. This means that you can:

  • save the data you receive in a response
  • calculate the next request
  • show the result of the request.

How to use Retrofit 2 for handling APIs

I’ll give you a detailed tutorial on handling APIs with Retrofit.

1. The first step is to add dependencies to your project:

implementation 'com.squareup.retrofit2:retrofit:version’
implementation 'com.squareup.retrofit2:converter-gson:version'
implementation 'com.squareup.retrofit2:adapter-rxjava2:version'

Note that my Retrofit and adapter version is 2.5.0. It’s important that they’re the same.

2. Initialize Retrofit:

public static Client getInstance() {
	return ourInstance;
}
Retrofit retrofit = new Retrofit.Builder()
	.baseUrl(BASE_URL)
	.client(okHttpClient)
	.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
	.build();

3. Describe the interface you’ll use to get the API requests to Retrofit:

 public interface Api {
@GET("users")
Call<List> getUserList();
}

4. To perform the same task in RxJava, you’ll need to use this code:

@GET("users")
Observable<List> getUserList();
}

5. The request itself should look like this:

public void getUsers (ResultCallback callback) {
Call<List> call = Api.getInstance().getUserList();
call.enqueue(callback)
}

The callback will store the server response in the form of an object, Response<List>. If the response is OK!, you’ll get the expected List in the onResponse(Call call, Response response) method with the help of the response.body() request.

6. Now you need to address Retrofit. It will then send a request to the server and return an RxJava data stream:

public Observable<List> getUsers () {
return  Api.getInstance().getUserList();

7. To handle the Rx response, you’ll need to use this code:

getUserList()	
.observeOn(AndroidSchedulers.mainThread())
	.subscribe(users -> 
{//some action},
throwable->
{//error handling}
);

This is what you need to set up the process of handling APIs with Retrofit. What happens, however, if your app receives an error as a response to the request? This is a very important part of API handling. If you don’t provide your app with ways to handle different errors, it can lead to crashes.

Handling API errors in Retrofit: Solutions

There are two types of errors you can receive while working with API requests:

  • HTTP errors
  • Request processing errors

To make sure the app works consistently, you need to handle the way it processes and reacts to errors. There are several ways to do this:

1. Add a custom interface Interceptor to your okHttp
2. When using a Call object, you can use the following lines of code in the method realization callback:

  • For processing errors: void onResponse(Call call, Response response);
  • For HTTP errors: void onFailure(Call call, Throwable t);

3. When using RX, use this:

  • For processing errors: In the method onNext(T t), in case you find the error in the t object
  • For HTTP errors: In the method void onError(Throwable t), when you process the error depending on the type of exception

You can also create an Observable interface (or alternative interfaces such as Single, Maybe, and Completable) for the Subscribe function.

Handling API errors in Retrofit: Solution drawbacks

Each of the solutions I’ve mentioned have their drawbacks; some are bigger, some smaller. You need to take them into account when you handle API errors to choose the best solution for your project and make sure you know what to do if your solution doesn’t work as intended.

For example, implementing the Interceptor interface can seem like a perfect solution at first because you can handle errors at the early stages in OkHttp. However, this is where you can encounter some problems.

Each of the solutions I’ve mentioned have their drawbacks; some are bigger, some smaller. You need to take them into account when you handle API errors

The deal is that to implement error handling behavior in your app, you need to transfer the app’s Context into an OkHttpClient. Error handling behavior includes notifying the user about an error or making an additional or alternative request to the backend.

When you transfer the Context into an OkHttpClient, this can lead to unpredictable errors in the application if you change the Context. This can also result in memory leakage.

Using an RxJava2Adapter to process the errors directly in the Subscribe function is a great idea until there are more than two methods in which you need to process errors.

The best way to handle API errors

While experimenting with different solutions for API error handling, I came up with a perfect solution that involves processing errors in a reactive data flow. I achieved this using RxJava.

The idea is to get the response to our request in the form of an object with or without an error, or to receive an HTTP error as a certain type of exception. This way we can handle internal errors like we handle HTTP errors.

This is what we need to make it work.

1. First, create an object that will contain the Context and the app’s behavior in case of any errors. For this, you’ll need to implement an interface for creating different error processors:

public interface BaseView {
   Context getContext();
}

This is how I implemented the processor:

public class RxErrorHandling implements BaseView {
   private WeakReference context;

RxErrorHandling(Context context){
	this.context = new WeakReference<>(context);
}

   private Context getContext() {
       return context.get();
   }

public void exampleInnerErrorWithCode (int code){
	//in case you received an internal error code from your server, use this method to handle the error 
Log.e(“TAG”, “Inner error with code ” + code);
//You can display Toast as well, because we got Context
}

public void onTimeoutError (){
	//you process the error here if you got the TimeoutException 
Log.e(“TAG”, “TimeoutException”);
}

public void onNetworkError (){
	//if the NetworkErrorException was initialized, process the error here  
Log.e(“TAG”, “ NetworkErrorExeption”);
}
	...
}

2. At this point, we need to create a class with two methods. These methods will process the server responses and HTTP errors.

class ResponseWrapper{
	private RxErrorHandling baseView;
	
	ReponseWrapper(RxErrorHandling view){
		this.baseView = view;
	}
	//Method for handling internal errors in the response
	public handlingResponse(BaseResponse response){
		if (!resp.isSuccess()) {
		//In case there is an internal error in the response, send the error code to the view to get a reaction
			baseView.exampleInnerErrorWithCode(resp.getErrorCode);
		}
	//Here you initialize the method for generating own error
   	catchError(resp.getCode(), resp.getError());

	}

	//Method for handling HTTP errors
	public handlingException(Throwable e){
	if(e instanceof SocketTimeoutException){
		baseView.onTimeoutError ();
	}elseIf(e instanceof IOException){
		baseView.onNetworkError()
	}

//Method for generating your own exception
	private void catchError(String code, String error) {
		try {
			throw new ResponseException(error, code);
		} catch (ResponseException e) {
			throw Exceptions.propagate(e);
		}
	}
}

These are all the necessary things you need to do to get prepared for API error handling. The last thing to do is to make a request and add the following to subscribe():

  • map() – Used to check if there are any errors in the server response. If there are, this function processes them or generates an exception.
  • doOnError() – A processor that initializes with the onError method in subscribe(). This processor allows us to process the HTTP exceptions or exceptions that were created in catchError(String code, String error) in the ResponseWrapper class.
ResponseWrapper wrapper = new ResponseWrapper(this)
getUserList()	
.observeOn(AndroidSchedulers.mainThread())
// if there were no exceptions from HTTP, than map() will be initialized
.map(response -> {wrapper.testResponse(response);
   			return response;}
		)
		//if there was an HTTP or a .doOnError exception -> {
			wrapper.testExceptions(e);})
.subscribe(
users -> 
{//some actions},
throwable->
{//individual error processing}
);

This is all you need to effectively handle your API errors in the best way possible.

Conclusion

Handling API errors with Retrofit2 and RxJava is a great solution for a few reasons.

First, you get rid of excessive, repeating code. Usually an app reacts to errors in the same way, and the overall behavior to the similar errors has to be identical. For example, if you receive a 404 error, you need to show an error pop-up (e.g. Toast). In case of a 403 Forbidden error, an app should display the authorization screen.

The second reason to use this strategy is its flexibility. If you need another behavior, you can just add it to onError() in the subscribe() function.

The third reason, and the best one, is that you can treat the response like a data stream in reactive programming. Legacy AsyncTasks won’t bother you here.

I hope this article is useful for you. If you have any questions about handling API requests and errors, be sure to contact Mobindustry.

Rate the article!

🌕 Cool!
🌖 Good
🌗 So-so
🌘 Meh
🌑 …