Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Good design for common success / failure / error handling for multiple APIs using Retrofit Android

I want to design API calls in such a way that it will be easy to handle success and failure responses easily from one place (instead of writing same code of call function for all APIs)

Here are the scenarios which I want to consider.

  1. Handle success / failure and error responses like 4xx, 5xx etc of all APIs at one central place.
  2. Want to cancel enqueue requests and also stop processing response if request is already sent in case of logout (because response parsing will modify some global data of app)
  3. If access token has expired and 401 response received from cloud, it should get new token and then call API again automatically with new token.

My current implementation is not satisfying above requirements. Is there any way to implement API calls which satisfy above requirements using Retrofit ? Please suggest me a good design for this.

Here is my current implementation :

  1. ApiInterface.java - It is an interface which contains different API calls definitions.
  2. ApiClient.java - To get retrofit client object to call APIs.
  3. ApiManager.java - It has methods to call APIs and parse their responses.

ApiInterface.java

public interface ApiInterface {

    // Get Devices
    @GET("https://example-base-url.com" + "/devices")
    Call<ResponseBody> getDevices(@Header("Authorization) String token);

    // Other APIs......
}

ApiClient.java

public class ApiClient {
    
    private static Retrofit retrofitClient = null;
    
    static Retrofit getClient(Context context) {

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .sslSocketFactory(sslContext.getSocketFactory(), systemDefaultTrustManager())
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .writeTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .build();

        retrofitClient = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(okHttpClient)
                .build();
    }
}

ApiManager.java

public class ApiManager {

private static ApiManager apiManager;

    public static ApiManager getInstance(Context context) {
        if (apiManager == null) {
            apiManager = new ApiManager(context);
        }
        return apiManager;
    }

    private ApiManager(Context context) {
        this.context = context;
        apiInterface = ApiClient.getClient(context).create(ApiInterface.class);   
    }

    public void getDevices(ResponseListener listener) {
        // API call and response handling
    }
    // Other API implementation
}

Update :

For 1st point, interceptor will be helpful to handle 4xx, 5xx responses globally according to this. But interceptor will be in the ApiClient file and to inform UI or API caller component, need to pass success or failure result in callback I mean response listener. How can I do that ? Any idea ?

For 3rd point, I know little bit about Retrofit Authenticator. I think for this point it is suitable but it requires synchronous call to get new token using refresh token. How can I make asynchronous call to synchronous ? (Note : this call is not retrofit call)

like image 335
Khushbu Shah Avatar asked May 19 '21 07:05

Khushbu Shah


People also ask

What is retrofit API in Android?

What is Retrofit? Retrofit is a REST Client library (Helper Library) used in Android and Java to create an HTTP request and also to process the HTTP response from a REST API. It was created by Square, you can also use retrofit to receive data structures other than JSON, for example SimpleXML and Jackson.

What is retrofit in API?

Retrofit automatically serialises the JSON response using a POJO(Plain Old Java Object) which must be defined in advanced for the JSON Structure. To serialise JSON we need a converter to convert it into Gson first. We need to add the following dependencies in our build.


2 Answers

By handling the success/failure responses at a central place I'll assume you want to get rid of repeated boilerplate based on the error parsing logic and how it may create UI side-effects for your app.

I'd probably suggest keeping things really simple by creating a custom abstraction for Callback which invokes your APIs for success/failure according to your domain logic.

Here's something fairly simple implementation for use-case (1) :

abstract class CustomCallback<T> implements Callback<T> {

    abstract void onSuccess(T response);
    abstract void onFailure(Throwable throwable);
    
    @Override
    public void onResponse(Call<T> call, Response<T> response) {
        if (response.isSuccessful()) {
            onSuccess(response.body());
        } else {
            onFailure(new HttpException(response));
        }
    }

    @Override
    public void onFailure(Call<T> call, Throwable t) {
        onFailure(t);
    }
}

For use-case (2), to be able to cancel all enqueued calls upon a global event like logout you'd have to keep a reference to all such objects. Fortunately, Retrofit supports plugging in a custom call factory okhttp3.Call.Factory

You could use your implementation as a singleton to hold a collection of calls and in the event of a logout notify it to cancel all requests in-flight. Word of caution, do use weak references of such calls in the collection to avoid leaks/references to dead calls. (also you might want to brainstorm on the right collection to use or a periodic cleanup of weak references based on the transactions)

For use-case (3), Authenticator should work out fine since you've already figured out the usage there are 2 options -

  1. Migrate the refresh token call to OkHttp/Retrofit and fire it synchronously
  2. Use a count-down latch to make the authenticator wait for the async call to finish (with a timeout set to connection/read/write timeout for the refresh token API call)

Here's a sample implementation:

abstract class NetworkAuthenticator implements Authenticator {

    private final SessionRepository sessionRepository;

    public NetworkAuthenticator(SessionRepository repository) {
        this.sessionRepository = repository;    
    }

    public Request authenticate(@Nullable Route route, @NonNull Response response) {
        String latestToken = getLatestToken(response);

        // Refresh token failed, trigger a logout for the user
        if (latestToken == null) {
            logout();
            return null;
        }

        return response
                .request()
                .newBuilder()
                .header("AUTHORIZATION", latestToken)
                .build();
    }

    private synchronized String getLatestToken(Response response) {
        String currentToken = sessionRepository.getAccessToken();

        // For a signed out user latest token would be empty
        if (currentToken.isEmpty()) return null;

        // If other calls received a 401 and landed here, pass them through with updated token
        if (!getAuthToken(response.request()).equals(currentToken)) {
            return currentToken;
        } else {
            return refreshToken();
        }
    }

    private String getAuthToken(Request request) {
        return request.header("AUTHORIZATION");
    }

    @Nullable
    private String refreshToken() {
        String result = null;
        CountDownLatch countDownLatch = new CountDownLatch(1);

        // Make async call to fetch token and update result in the callback
    
        // Wait up to 10 seconds for the refresh token to succeed
        try {
            countDownLatch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        return result;
    }

    abstract void logout();
}

I hope this helps with your network layer implementation

like image 160
rahul.taicho Avatar answered Oct 11 '22 19:10

rahul.taicho


So, With the help of official sample in the retrofit github repository here: https://github.com/square/retrofit/blob/fbf1225e28e2094bec35f587b8933748b705d167/samples/src/main/java/com/example/retrofit/ErrorHandlingAdapter.java

The ErrorHandlingAdapter is the closest you can get to your requirement because it lets you control enqueuing of the call, creating the error callbacks, calling error callbacks on your own. Whether you want the caller to do some action or you want to handle it yourself in one place or just both.

So this is how you can create it. Do read the inline comments to understand.

public final class ErrorHandlingAdapter {

    /**
     * Here you'll decide how many methods you want the caller to have.
     */
    interface MyCallback<T> {
        void success(Response<T> response);

        void error(String s);
    }

    /**
     * This is your call type
     */
    interface MyCall<T> {
        void cancel();

        void enqueue(MyCallback<T> callback);

        @NotNull
        MyCall<T> clone();
    }

    public static class ErrorHandlingCallAdapterFactory extends CallAdapter.Factory {
        @Override
        public @Nullable
        CallAdapter<?, ?> get(
                @NotNull Type returnType, @NotNull Annotation[] annotations, @NotNull Retrofit retrofit) {
            if (getRawType(returnType) != MyCall.class) {
                return null;
            }
            if (!(returnType instanceof ParameterizedType)) {
                throw new IllegalStateException(
                        "MyCall must have generic type (e.g., MyCall<ResponseBody>)");
            }
            Type responseType = getParameterUpperBound(0, (ParameterizedType) returnType);
            Executor callbackExecutor = retrofit.callbackExecutor();
            return new ErrorHandlingCallAdapter<>(responseType, callbackExecutor);
        }

        private static final class ErrorHandlingCallAdapter<R> implements CallAdapter<R, MyCall<R>> {
            private final Type responseType;
            private final Executor callbackExecutor;

            ErrorHandlingCallAdapter(Type responseType, Executor callbackExecutor) {
                this.responseType = responseType;
                this.callbackExecutor = callbackExecutor;
            }

            @NotNull
            @Override
            public Type responseType() {
                return responseType;
            }

            @Override
            public MyCall<R> adapt(@NotNull Call<R> call) {
                return new MyCallAdapter<>(call, callbackExecutor);
            }
        }
    }

    static class MyCallAdapter<T> implements MyCall<T> {
        private final Call<T> call;
        private final Executor callbackExecutor;

        MyCallAdapter(Call<T> call, Executor callbackExecutor) {
            this.call = call;
            this.callbackExecutor = callbackExecutor;
        }

        @Override
        public void cancel() {
            call.cancel();
        }

        @Override
        public void enqueue(final MyCallback<T> callback) {
            if (!SomeCondition.myCondition) {
                // Don't enqueue the call if my condition doesn't satisfy
                // it could be a flag in preferences like user isn't logged in or
                // some static flag where you don't want to allow calls
                return;
            }
            call.clone().enqueue(
                    new Callback<T>() {
                        @Override
                        public void onResponse(@NotNull Call<T> call, @NotNull Response<T> response) {
                            callbackExecutor.execute(() -> {
                                int code = response.code();
                                if (code >= 200 && code < 300) {
                                    //success response
                                    callback.success(response);
                                } else if (code == 401) {
                                    // Unauthenticated so fetch the token again
                                    getTheTokenAgain(callback);
                                } else if (code >= 400 && code < 500) {
                                    //handle error the way you want
                                    callback.error("Client error");
                                } else if (code >= 500 && code < 600) {
                                    //handle error the way you want
                                    callback.error("Server error");
                                } else {
                                    //handle error the way you want
                                    callback.error("Something went wrong");
                                }
                            });
                        }

                        @Override
                        public void onFailure(@NotNull Call<T> call, @NotNull Throwable t) {
                            callbackExecutor.execute(() -> {
                                if (t instanceof IOException) {
                                    callback.error("IOException");
                                } else {
                                    callback.error("Some exception");
                                }
                            });
                        }
                    });
        }

        private void getTheTokenAgain(MyCallback<T> callback) {
            // Make the call to get the token & when token arrives enqueue it again
            // Don't forget to put termination condition like 3 times, if still not successful
            // then just log user out or show error

            // This is just dummy callback, you'll need to make a
            // call to fetch token
            new MyTokenCallback() {
                @Override
                public void onTokenArrived(String token) {
                    //enqueue(callback); here
                }

                @Override
                public void onTokenFetchFailed() {
                    callbackExecutor.execute(() -> {
                        callback.error("Counld't fetch token");
                    });
                }
            };

           // This is for demo you should put it in success callback
            SomeCondition.callCount++;
            Log.d("MG-getTheTokenAgain", "Method called");
            if (SomeCondition.callCount < 3) {
                enqueue(callback);
            } else {
                callbackExecutor.execute(() -> {
                    callback.error("Counld't fetch token");
                });
            }
        }

        @NotNull
        @Override
        public MyCall<T> clone() {
            return new MyCallAdapter<>(call.clone(), callbackExecutor);
        }
    }
}

This is how you'll plug this adapter:

private void makeApiCall() {
        //This is just for demo to generate 401 error you won't need this
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        httpClient.addInterceptor(chain -> {
            Request request = chain.request().newBuilder()
                    .addHeader("Accept","application/json")
                    .addHeader("Authorization", "cdsc").build();
            return chain.proceed(request);
        });

        Retrofit retrofit =
                new Retrofit.Builder()
                        .baseUrl("http://httpbin.org/")
                        .addCallAdapterFactory(new ErrorHandlingAdapter.ErrorHandlingCallAdapterFactory())
                        .addConverterFactory(GsonConverterFactory.create())
                        .client(httpClient.build())
                        .build();

        HttpBinService service = retrofit.create(HttpBinService.class);
        ErrorHandlingAdapter.MyCall<Ip> ip = service.getIp();
        ip.enqueue(
                new ErrorHandlingAdapter.MyCallback<Ip>() {
                    @Override
                    public void success(Response<Ip> response) {
                        Log.d("MG-success", response.toString());
                    }

                    @Override
                    public void error(String s) {
                        Log.d("MG-error", s);
                    }
                });
    }

You might have to bend some things to your needs, but I think this could be good reference because it's in the official sample.

like image 2
Mayur Gajra Avatar answered Oct 11 '22 17:10

Mayur Gajra