Before we begin
When creating mobile apps you have many decisions to make that will have strong influence on final project’s shape. One of them is choosing the right libraries.
Currently, it is hard to imagine mobile app that doesn’t communicate with network services. Weather apps download weather forecasts from servers, sport & fitness apps store our training data, social apps let us communicate with other users – these are only few examples.
Here at Snowdog, Mobile Team Android uses Retrofit to communicate with APIs. Today I would like to show you a few tips on how to use it to make your code simpler, cleaner and more maintainable.
Basic steps
Adding library to your project
To start using Retrofit you have to add the library to your project by entering in dependencies section of your build.gradle file given entries:
compile 'com.squareup.retrofit:retrofit:1.8.0'
compile 'com.squareup.okhttp:okhttp:2.1.0'
compile 'com.squareup.okhttp:okhttp-urlconnection:2.1.0'
compile 'com.google.code.gson:gson:2.3'
okhttp libraries are strongly recommended, as they allow to bypass many native http client issues. Gson will also be needed for serializing and deserializing between Object and JSON format.
Preparing API interfaces
Next step is consulting with your API documentation and preparing interfaces for Retrofit. Example class below:
public class MyApi {
public static final String TAG = "MyApi";
public interface Users {
@GET("/user/{user_id}.json")
void getUser(@Path("user_id") String userId, Callback<User> cb);
@POST("/user.json")
void createUser(@Body User user, Callback<User> cb);
@PUT("/user/{user_id}.json")
void updateUser(@Path("user_id") String deviceId, @Body User user, Callback<User> cb);
}
public interface Messages {
@GET("/messages/{message_id}.json")
void getMessage(@Path("message_id") String messageId, Callback<Message> cb);
// etc
}
}
If there is something unclear here please refer to official Retrofit documentation.
Creating RestAdapter
It is advised to create single instance of RestAdapter per application (one of the best places to put it in would be your Application class). Make it public static so you can reference it from other parts of your project. You can also introduce public static fields of your API interfaces.
public static RestAdapter restAdapter;
public static MyApi.Users usersApi;
public static MyApi.Messages messagesApi;
Having added these fields initialize them:
@Override
public void onCreate() {
super.onCreate();
// your other code
restAdapter = new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.BASIC)
.setEndpoint(API_URL)
.build();
usersApi = restAdapter.create(MyApi.Users.class);
messagesApi = restAdapter.create(MyApi.Messages.class);
}
API_URL is a String containing url of api server (e.g. https://api.example.com, https://example.com/api/1.0, etc) without trailing slash. By means of setLogLevel() method you can adjust the amount of data being dumped to the log (RestAdapter.LogLevel).
Sending request
Having done this you can now access your API service easily from any part of your project by simply calling:
YourApplicationClass.usersApi.getUser("someid", new Callback<User>() {
@Override
public void success(User user, Response response) {
//success code
}
@Override
public void failure(RetrofitError error) {
//failure code
}
});
This sums up essential parts needed for communication with your api server, which is easy to implement but you can make it even better.
Modifications
Sometimes api requires you to provide authentication via key (tokens anyone?), which you might have to include in url or header. Of course you can do this by modifying your interfaces like this:
public interface Users {
@GET("/user/{user_id}.json")
void getUser(@Path("user_id") String userId, @Query("apiKey") String apiKey, Callback<User> cb);
@POST("/user.json")
void createUser(@Body User user, @Query("apiKey") String apiKey, Callback<User> cb);
@PUT("/user/{user_id}.json")
void updateUser(@Path("user_id") String deviceId, @Query("apiKey") String apiKey, @Body User user, Callback<User> cb);
}
It will work fine but there is a better way to do this.
RequestInterceptor
RequestInterceptor is Retrofit’s interface that allows us to make some modifications (add additional data) to the request before it is executed. You can add headers, query params, etc, for all requests that go through RestAdapter instance that your RequestInterceptor is bound to. To bind your interceptor call method setRequestInterceptor() on RestAdapter.Builder() when instantiating RestAdapter:
restAdapter = new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.BASIC)
.setEndpoint(API_URL)
.setRequestInterceptor(requestInterceptor)
.build();
RequestInterceptor has only one method to implement – intercept(), which allows you to manipulate request by means of RequestFacade object.
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
//manipulate request here
}
};
There are 3 methods (well, actually 5, but 2 of them only differ slightly from others, see javadoc) to call on RequestFacade:
- addHeader()
- addQueryParam()
- addPathParam()
Their functions are identical to those provided by annotations when declaring API interfaces (@Header, @Query, @Path).
@Override
public void intercept(RequestFacade request) {
//add api_key as query param to every api request
request.addQueryParam("api_key", PreferencesUtil.getUserApiKey(mContext));
//add api_key as header param
request.addHeader("X-Some-Service-API-Key", PreferencesUtil.getUserApiKey(mContext));
//api can return data in different languages
if(PreferencesUtil.getLocale(mContext)!=null) {
request.addQueryParam("lang", PreferencesUtil.getLocale(mContext));
} else {
request.addQueryParam("lang", "en");
}
}
This lets us remove the clutter and code duplication from our project. Useful, isn’t it?
But wait – there is more!
Error handling
Any request can end up returning error that’s why besides success method there is a failure method in Callback. It works as advertised, no objections, but imagine a project where you have many api calls in different places and now you have to handle the same errors all across the project’s code. By such errors I mean e.g. Unauthorized (error code 401), Forbidden (403) or Internal Server Error (50X). Do you often find yourself checking if network connection is available before sending request? Well, we can handle that too and make your life easier by the way.
ErrorHandler to the rescue!
Retrofit provides us with the ErrorHandler interface, which instantiated we can pass to RestAdapter while building one.
restAdapter = new RestAdapter.Builder()
.setLogLevel(RestAdapter.LogLevel.BASIC)
.setEndpoint(API_URL)
.setRequestInterceptor(requestInterceptor)
.setErrorHandler(errorHandler)
.build();
Implementing ErrorHandler is easy, just create new class and implement the interface (which has only one method – handleError(RetrofitError cause)). Basic example (code kept simple for the sake of this article’s clarity):
public class MyErrorHandler implements ErrorHandler {
public static final String TAG = "MyErrorHandler";
private Context mContext;
public MyErrorHandler(Context ctx) {
mContext = ctx;
}
@Override
public Throwable handleError(RetrofitError cause) {
if(cause != null) {
switch(cause.getKind()) {
case NETWORK:
return new NetworkBroadcastedException(mContext, cause);
default:
Response r = cause.getResponse();
if(r == null) return cause;
if (r.getStatus() == 401) {
return new UnauthorizedBroadcastedException(mContext, cause);
} else if(r.getStatus() == 403) {
return new ForbiddenBroadcastedException(mContext, cause);
} else if(r.getStatus() >= 500) {
return new InternalServerErrorBroadcastedException(mContext, cause);
} else if(r.getStatus() == 404) {
Log.e(TAG, "error 404");
return cause;
}
}
}
return cause;
}
}
BroadcastedException:
public abstract class BroadcastedException extends Throwable {
public static final String TAG = "BroadcastedException";
public static final String ACTION_BROADCASTED_EXCEPTION = "action_broadcasted_exception";
public static final String EXTRA_TYPE = "broadcasted_exception_type";
public static final String EXTRA_MESSAGE = "broadcasted_exception_message";
public static enum EXCEPTION_TYPE {
UNKNOWN, NETWORK, UNAUTHORIZED, FORBIDDEN, INTERNAL_SERVER;
public static EXCEPTION_TYPE getByOrdinal(int o) {
for(EXCEPTION_TYPE e : EXCEPTION_TYPE.values()) {
if(e.ordinal() == o) return e;
}
return UNKNOWN;
}
}
protected EXCEPTION_TYPE mExceptionType;
public BroadcastedException(Context ctx, RetrofitError cause, EXCEPTION_TYPE type) {
super(cause);
mExceptionType = type;
Intent bcast = new Intent(ACTION_BROADCASTED_EXCEPTION);
bcast.putExtra(EXTRA_TYPE, mExceptionType.ordinal());
bcast.putExtra(EXTRA_MESSAGE, cause.toString());
ctx.sendBroadcast(bcast);
}
}
NetworkBroadcastedException:
public class NetworkBroadcastedException extends BroadcastedException {
public static final String TAG = "NetworkBroadcastedException";
public NetworkBroadcastedException(Context ctx, RetrofitError cause) {
super(ctx, cause, EXCEPTION_TYPE.NETWORK);
}
}
UnauthorizedBroadcastedException:
public class UnauthorizedBroadcastedException extends BroadcastedException {
public static final String TAG = "UnauthorizedBroadcastedException";
public UnauthorizedBroadcastedException(Context ctx, RetrofitError cause) {
super(ctx, cause, EXCEPTION_TYPE.UNAUTHORIZED);
//process unauthorized error - logout user/force to login again?
// . . .
}
}
ForbiddenBroadcastedException:
public class ForbiddenBroadcastedException extends BroadcastedException {
public static final String TAG = "ForbiddenBroadcastedException";
public ForbiddenBroadcastedException(Context ctx, RetrofitError cause) {
super(ctx, cause, EXCEPTION_TYPE.FORBIDDEN);
//process forbidden error - inform user that (s)he can’t access this content?
// . . .
}
}
InternalServerErrorBroadcastedException:
public class InternalServerErrorBroadcastedException extends BroadcastedException {
public static final String TAG = "InternalServerErrorBroadcastedException";
public NetworkBroadcastedException(Context ctx, RetrofitError cause) {
super(ctx, cause, EXCEPTION_TYPE.INTERNAL_SERVER);
//process internal server error - inform user that server is unable to process request due to internal error?
// . . .
}
}
Provided exceptions are created when sufficient conditions are met when retrofit receives error from server. Please note that error handling is being processed before entering failure() method of Callback.
NetworkBroadcastedException is a convenient way to handle network issues (no Internet access, timeouts). UnauthorizedBroadcastedException lets you keep logout/relog user code (context reference helps with that) in one place when 401 Unauthorized is received. ForbiddenBroadcastedException gathers the code responsible for handling access forbidden errors. InternalServerErrorBroadcastedException serves the similar purpose but regarding internal server errors.
As you can see exceptions broadcast messages using intents. To receive them in your let’s say Fragments you have to register broadcast receiver for given exception. You can do it for every Fragment class that you create but this will make you duplicate code blocks and introduce clutter. I will show you how to do it better.
Finalizing
First, you need some interfaces like this:
public interface IBroadcastedExceptionListener {
public void processException(BroadcastedException.EXCEPTION_TYPE type, String message);
}
public interface INetworkExceptionListener {
void processNetworkException(String message);
}
Then, create new class that extends Fragment class and make it implement given interface:
public abstract class CoreFragment extends Fragment
implements IBroadcastedExceptionListener, INetworkExceptionListener {
public static final String TAG = "CoreFragment";
protected BroadcastedExceptionReceiver broadcastedExceptionReceiver;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
broadcastedExceptionReceiver = new BroadcastedExceptionReceiver();
}
@Override
public void onResume() {
super.onResume();
getActivity().registerReceiver(broadcastedExceptionReceiver,
new IntentFilter(BroadcastedException.ACTION_BROADCASTED_EXCEPTION));
}
@Override
public void onPause() {
super.onPause();
getActivity().unregisterReceiver(broadcastedExceptionReceiver);
}
@Override
public void processNetworkException(String message) {
Log.e(TAG, "Network exception: " + message);
}
@Override
public void processException(BroadcastedException.EXCEPTION_TYPE type, String message) {
switch(type) {
case NETWORK:
processNetworkException(message);
break;
case UNAUTHORIZED:
Log.d(TAG, "Unauthorized exception: " + message);
break;
default:
Log.d(TAG, "Exception: " + message);
break;
}
}
public class BroadcastedExceptionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if(intent!=null) {
String message = intent.getStringExtra(BroadcastedException.EXTRA_MESSAGE);
int nType = intent.getIntExtra(
BroadcastedException.EXTRA_TYPE,
BroadcastedException.EXCEPTION_TYPE.UNKNOWN.ordinal());
BroadcastedException.EXCEPTION_TYPE type =
BroadcastedException.EXCEPTION_TYPE.getByOrdinal(nType);
processException(type, message);
}
}
}
}
Now you can create your fragments by extending CoreFragment and handle received network exception broadcasts by overriding processNetworkException() method in your fragments. For other exceptions (like UnauthorizedException, ForbiddenException, … ) the code looks almost the same – just remember to add relevant interfaces and dispatch methods in processException()’s switch.
Summary
Retrofit is a great library that can help you build better, maintainable code and avoid code duplication. Presented code snippets show that you can easily do that while separating business logic from handling I/O (network) errors as well.
I hope simple hints I presented will make your developer’s life easier. Have fun and create great things!
Image used in the article by photoeverywhere.co.uk