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.
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 :
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)
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.
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.
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 -
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With