Android & Kotlin

Retrofit Error Handling Android in Single Place

Pinterest LinkedIn Tumblr

In the previous article, I have discussed the way to handling for NullPointerException, HttpException during REST API calls. In this post, I’m going to explain errors handling android in a single place.

Every developer wants to ensure our app will never crash, even end-user also want same. So we will catch all non-success responses from the server and handled the single place.

Retrofit is a very powerful library for networking in Android these days. While integrating REST APIs doesn’t guarantee the APIs response will be always expected. So how to manage these kinds of Exception?

Problem Use Case

Suppose we are integrating a REST API that gets the user profile from server and displaying our app and JSON like below.

User API response JSON

{
  "error": false,
  "message": "User Profile Details Found",
  "statusCode": 200,
  "data": {
    "userId": "dpPnxRI3n",
    "userName": "monika.sharma",
    "firstName": "Monika",
    "lastName": "Sharma",
    "bio": "Tech Lead/Architect",
    "mobileNumber": "91 9527169942",
    "location": [
      {
        "city": "Agra",
        "country": "India",
        "geoLocation": "",
        "state": "UP"
      }
    ],
    "profilePicUrl": "http://35.197.65.22/wp-content/uploads/2019/01/profile_pic.jpg",
    "designation": "Assistent Manager",
    "workAt": "Unify System Pvt. Ltd.",
    "about": " I enjoy working on projects for innovation, profitability and scalability",
    "followersCounter": 110,
    "followingCount": 110
  },
  "authToken": ""
}

Now you have create a POJO for parsing this JSON object.

package com.androidwave.errorhandling.network.pojo;

import com.google.gson.annotations.SerializedName;

import java.util.List;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public class UserProfile {

    @SerializedName("about")
    private String mAbout;
    @SerializedName("bio")
    private String mBio;
    @SerializedName("channelCount")
    private Long mChannelCount;
    @SerializedName("designation")
    private String mDesignation;
    @SerializedName("firstName")
    private String mFirstName;
    @SerializedName("followersCounter")
    private Long mFollowersCounter;
    @SerializedName("followingCount")
    private Long mFollowingCount;
    @SerializedName("lastName")
    private String mLastName;
    @SerializedName("location")
    private List<Place> mLocation;
    @SerializedName("mobileNumber")
    private String mMobileNumber;
    @SerializedName("profilePicUrl")
    private String mProfilePicUrl;
    @SerializedName("studiedAt")
    private String mStudiedAt;
    @SerializedName("userId")
    private String mUserId;
    @SerializedName("userName")
    private String mUserName;
    @SerializedName("workAt")
    private String mWorkAt;

    public String getAbout() {
        return mAbout;
    }

    public void setAbout(String about) {
        mAbout = about;
    }

    public String getBio() {
        return mBio;
    }

    public void setBio(String bio) {
        mBio = bio;
    }

    public Long getChannelCount() {
        return mChannelCount;
    }

    public void setChannelCount(Long channelCount) {
        mChannelCount = channelCount;
    }

    public String getDesignation() {
        return mDesignation;
    }

    public void setDesignation(String designation) {
        mDesignation = designation;
    }

    public String getFirstName() {
        return mFirstName;
    }

    public void setFirstName(String firstName) {
        mFirstName = firstName;
    }

    public Long getFollowersCounter() {
        return mFollowersCounter;
    }

    public void setFollowersCounter(Long followersCounter) {
        mFollowersCounter = followersCounter;
    }

    public Long getFollowingCount() {
        return mFollowingCount;
    }

    public void setFollowingCount(Long followingCount) {
        mFollowingCount = followingCount;
    }


    public String getLastName() {
        return mLastName;
    }

    public void setLastName(String lastName) {
        mLastName = lastName;
    }

    public List<Place> getLocation() {
        return mLocation;
    }

    public void setLocation(List<Place> location) {
        mLocation = location;
    }

    public String getMobileNumber() {
        return mMobileNumber;
    }

    public void setMobileNumber(String mobileNumber) {
        mMobileNumber = mobileNumber;
    }

    public String getProfilePicUrl() {
        return mProfilePicUrl;
    }

    public void setProfilePicUrl(String profilePicUrl) {
        mProfilePicUrl = profilePicUrl;
    }

    public String getStudiedAt() {
        return mStudiedAt;
    }

    public void setStudiedAt(String studiedAt) {
        mStudiedAt = studiedAt;
    }

    public String getUserId() {
        return mUserId;
    }

    public void setUserId(String userId) {
        mUserId = userId;
    }

    public String getUserName() {
        return mUserName;
    }

    public void setUserName(String userName) {
        mUserName = userName;
    }

    public String getWorkAt() {
        return mWorkAt;
    }

    public void setWorkAt(String workAt) {
        mWorkAt = workAt;
    }
}

We are using GSON parsing with RxJava2CallAdapterFactory converter for the POJO, for now, API starts sending location Object as a string array in an instance of Place Object ( see below JSON ) resulted from app again start crashing because the converter is throwing JsonSyntaxException. how to resolve this problem so our app will never crash.

API response in case of location is string array

{
  "error": false,
  "message": "User Profile Details Found",
  "statusCode": 200,
  "data": {
    "userId": "dpPnxRI3n",
    "userName": "monika.sharma",
    "firstName": "Monika",
    "lastName": "Sharma",
    "bio": "Tech Lead/Architect",
    "mobileNumber": "91 9527169942",
    "location": [
      "Agra UP India "
    ],
    "profilePicUrl": "http://35.197.65.22/wp-content/uploads/2019/01/profile_pic.jpg",
    "designation": "Assistent Manager",
    "workAt": "Unify System Pvt. Ltd.",
    "about": " I enjoy working on projects for innovation, profitability and scalability",
    "followersCounter": 110,
    "followingCount": 110
  },
  "authToken": ""
}

Solution

When you integrate API using Retrofit you get to check error three times isSuccessful, IOException, and the response code. It increases the line of code unusually. Manage all this create a base wrapper class. I will tell you error handling android in a single place by using below wrapper class.

Create a response wrapper class

For parsing APIs response create a Wrapper class in src folder names WrapperResponse

package com.androidwave.errorhandling.network.pojo;

import com.google.gson.annotations.SerializedName;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public class WrapperResponse<T> {
    @SerializedName("data")
    private T mData;
    @SerializedName("error")
    private Boolean mError;
    @SerializedName("message")
    private String mMessage;
    @SerializedName("status")
    private String mStatus;
    @SerializedName("authToken")
    private String mAuthToken;

    public String getAuthToken() {
        return mAuthToken;
    }

    public void setAuthToken(String mAuthToken) {
        this.mAuthToken = mAuthToken;
    }

    public T getData() {
        return mData;
    }

    public void setData(T data) {
        mData = data;
    }

    public Boolean getError() {
        return mError;
    }

    public void setError(Boolean error) {
        mError = error;
    }

    public String getMessage() {
        return mMessage;
    }

    public void setMessage(String message) {
        mMessage = message;
    }

    public String getStatus() {
        return mStatus;
    }

    public void setStatus(String status) {
        mStatus = status;
    }
}
Here is T is TYPE of class that you want to parse in our case is UserProfile

Create a error wrapper class

In src folder a create a wrapper class with named

package com.androidwave.errorhandling.network;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public class WrapperError extends RuntimeException {


    @Expose
    @SerializedName("status_code")
    private Long statusCode;

    @Expose
    @SerializedName("message")
    private String message;

    public WrapperError(Long statusCode, String message) {
        this.statusCode = statusCode;
        this.message = message;
    }


    public WrapperError(Long statusCode) {
        this.statusCode = statusCode;
    }


    public Long getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(Long statusCode) {
        this.statusCode = statusCode;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

Create a JSON Converter Factory

Gson provides powerful parsing of JSON object so I will be creating a dynamic type. Suppose you want to parse UserProfile I jut have to tell Gson to parse response as a WrapperResponse<UserProfile> and go from there. If you think Why would you create a custom JSON converter factory. I will clear your all doubt, just for Abstraction, I’m packing all of this API parsing and wrapping code into a single package and hide it from the rest of the application.

package com.androidwave.errorhandling.network;

import com.androidwave.errorhandling.network.pojo.WrapperResponse;

import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public class WrapperConverterFactory extends Converter.Factory {

    private GsonConverterFactory factory;

    public WrapperConverterFactory(GsonConverterFactory factory) {
        this.factory = factory;
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(final Type type,
                                                            Annotation[] annotations, Retrofit retrofit) {
        // e.g. WrapperResponse<UserProfile>
        Type wrappedType = new ParameterizedType() {
            @Override
            public Type[] getActualTypeArguments() {
                return new Type[]{type};
            }

            @Override
            public Type getOwnerType() {
                return null;
            }

            @Override
            public Type getRawType() {
                return WrapperResponse.class;
            }
        };
        Converter<ResponseBody, ?> gsonConverter = factory
                .responseBodyConverter(wrappedType, annotations, retrofit);
        return new WrapperResponseBodyConverter(gsonConverter);
    }
}

Create a response body converter

package com.androidwave.errorhandling.network;

import com.androidwave.errorhandling.network.pojo.WrapperResponse;

import java.io.IOException;

import okhttp3.ResponseBody;
import retrofit2.Converter;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public class WrapperResponseBodyConverter<T>
        implements Converter<ResponseBody, T> {
    private Converter<ResponseBody, WrapperResponse<T>> converter;

    public WrapperResponseBodyConverter(Converter<ResponseBody,
            WrapperResponse<T>> converter) {
        this.converter = converter;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        WrapperResponse<T> response = converter.convert(value);
        if (!response.getError()) {
            return response.getData();
        }
        // RxJava will call onError with this exception
        throw new WrapperError(response.getStatus(), response.getMessage());
    }
}

Set the WrapperConverterFactory in Retrofit client

Now we will user WrapperConverterFactory instance of GsonConverterFactory. As I explain In this demo we are using Dagger2 + RxJava Retrofit with MVP design pattern. So Just open application module and change below in Retrofit Client

 /**
     * provide Retrofit instances
     *
     * @param baseURL base url for api calling
     * @param client  OkHttp client
     * @return Retrofit instances
     */

    @Provides
    public Retrofit provideRetrofit(String baseURL, OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl(baseURL)
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(new WrapperConverterFactory(GsonConverterFactory.create()))
                .build();
    }
For better understanding I’m show full code of retrofit client
package com.androidwave.errorhandling.di.module;

import android.app.Application;
import android.content.Context;

import com.androidwave.errorhandling.BuildConfig;
import com.androidwave.errorhandling.di.ApplicationContext;
import com.androidwave.errorhandling.network.NetworkService;
import com.androidwave.errorhandling.network.WrapperConverterFactory;
import com.androidwave.errorhandling.ui.MainMvp;
import com.androidwave.errorhandling.ui.MainPresenter;

import dagger.Module;
import dagger.Provides;
import io.reactivex.disposables.CompositeDisposable;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
@Module
public class ApplicationModule {

    private final Application mApplication;

    public ApplicationModule(Application application) {
        mApplication = application;
    }

    @Provides
    @ApplicationContext
    Context provideContext() {
        return mApplication;
    }

    @Provides
    Application provideApplication() {
        return mApplication;
    }

    /**
     * @return HTTTP Client
     */
    @Provides
    public OkHttpClient provideClient() {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        return new OkHttpClient.Builder().addInterceptor(interceptor).addInterceptor(chain -> {
            Request request = chain.request();
            return chain.proceed(request);
        }).build();
    }

    /**
     * provide Retrofit instances
     *
     * @param baseURL base url for api calling
     * @param client  OkHttp client
     * @return Retrofit instances
     */

    @Provides
    public Retrofit provideRetrofit(String baseURL, OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl(baseURL)
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(new WrapperConverterFactory(GsonConverterFactory.create()))
                .build();
    }

    /**
     * Provide Api service
     *
     * @return ApiService instances
     */

    @Provides
    public NetworkService provideNetworkService() {
        return provideRetrofit(BuildConfig.BASE_URL, provideClient()).create(NetworkService.class);
    }

    @Provides
    CompositeDisposable provideCompositeDisposable() {
        return new CompositeDisposable();
    }

    @Provides
    public MainMvp.Presenter provideMainPresenter(NetworkService mService, CompositeDisposable disposable) {
        return new MainPresenter(mService, disposable);
    }
}
Create a interface for get user details like below
package com.androidwave.errorhandling.network;

import com.androidwave.errorhandling.network.pojo.UserProfile;
import com.androidwave.errorhandling.network.pojo.WrapperResponse;

import io.reactivex.Observable;
import retrofit2.http.GET;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public interface NetworkService {
    /**
     * @return Observable feed response
     */
    @GET("user.php")
    Observable<UserProfile> getUserProfile();
}

Let’s create MVP Contract for main activity, Normally create in MVP pattern

package com.androidwave.errorhandling.ui;

import com.androidwave.errorhandling.network.pojo.UserProfile;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 * Email    : info@androidwave.com
 */
public class MainMvp {
    interface View {

        void showLoading(boolean isLoading);

        void onSuccess(UserProfile mProfile);

        void onError(String message);
    }

    public interface Presenter {

        void getUserProfile();

        void detachView();

        void attachView(View view);

        void handleApiError(Throwable error);

    }
}

In UI package create Presenter which implements MainMvp.Presenter

You already aware of the uses of Presenter the more important part void handleApiError(Throwable error); For best practice, you should create BasePresenter for implementing handleApiError() method and all child class will extend BasePresenter.

package com.androidwave.errorhandling.ui;


import com.androidwave.errorhandling.network.NetworkService;
import com.androidwave.errorhandling.network.WrapperError;
import com.google.gson.JsonSyntaxException;

import javax.inject.Inject;
import javax.net.ssl.HttpsURLConnection;

import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import retrofit2.HttpException;

/**
 * Created on : Jan 19, 2019
 * Author     : AndroidWave
 */
public class MainPresenter implements MainMvp.Presenter {
    public static final int API_STATUS_CODE_LOCAL_ERROR = 0;
    private CompositeDisposable mDisposable;
    private NetworkService mService;
    private MainMvp.View mView;
    private static final String TAG = "MainPresenter";

    @Inject
    public MainPresenter(NetworkService service, CompositeDisposable disposable) {
        this.mService = service;
        this.mDisposable = disposable;
    }


    @Override
    public void getUserProfile() {
        if (mView != null) {
            mView.showLoading(true);
        }
        mDisposable.add(
                mService.getUserProfile()
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnTerminate(() -> {
                            if (mView != null) {
                                mView.showLoading(false);
                            }
                        })
                        .subscribe(response -> {
                            if (mView != null) {
                                mView.showLoading(false);
                                /**
                                 * Update view here
                                 */
                                mView.onSuccess(response);
                            }
                        }, error -> {
                            if (mView != null) {
                                mView.showLoading(false);
                                /**
                                 * manage all kind of error in single place
                                 */
                                handleApiError(error);
                            }
                        })
        );
    }

    @Override
    public void detachView() {
        mDisposable.clear();
    }

    @Override
    public void attachView(MainMvp.View view) {
        this.mView = view;
    }

    @Override
    public void handleApiError(Throwable error) {
        if (error instanceof HttpException) {
            switch (((HttpException) error).code()) {
                case HttpsURLConnection.HTTP_UNAUTHORIZED:
                    mView.onError("Unauthorised User ");
                    break;
                case HttpsURLConnection.HTTP_FORBIDDEN:
                    mView.onError("Forbidden");
                    break;
                case HttpsURLConnection.HTTP_INTERNAL_ERROR:
                    mView.onError("Internal Server Error");
                    break;
                case HttpsURLConnection.HTTP_BAD_REQUEST:
                    mView.onError("Bad Request");
                    break;
                case API_STATUS_CODE_LOCAL_ERROR:
                    mView.onError("No Internet Connection");
                    break;
                default:
                    mView.onError(error.getLocalizedMessage());

            }
        } else if (error instanceof WrapperError) {
            mView.onError(error.getMessage());
        } else if (error instanceof JsonSyntaxException) {
            mView.onError("Something Went Wrong API is not responding properly!");
        } else {
            mView.onError(error.getMessage());
        }

    }
}

Implement view in activity like below

package com.androidwave.errorhandling.ui;

import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import com.androidwave.errorhandling.R;
import com.androidwave.errorhandling.WaveApp;
import com.androidwave.errorhandling.network.pojo.Place;
import com.androidwave.errorhandling.network.pojo.UserProfile;
import com.androidwave.errorhandling.utils.CommonUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;

import javax.inject.Inject;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MainActivity extends AppCompatActivity implements MainMvp.View {

    //  ActivityComponent mActivityComponent;
    private static final String TAG = "MainActivity";
    @Inject
    MainMvp.Presenter mPresenter;
    @BindView(R.id.txtTitle)
    TextView txtTitle;
    @BindView(R.id.txtDesignation)
    TextView txtDesignation;
    @BindView(R.id.txtFollowers)
    TextView txtFollowers;
    @BindView(R.id.txtFollowing)
    TextView txtFollowing;
    @BindView(R.id.txtUsername)
    TextView txtUsername;
    @BindView(R.id.txtBio)
    TextView txtBio;
    @BindView(R.id.txtPhone)
    TextView txtPhone;
    @BindView(R.id.txtAddress)
    TextView txtAddress;
    @BindView(R.id.txtWorkAt)
    TextView txtWorkAt;
    @BindView(R.id.imageViewProfilePic)
    ImageView imageViewProfilePic;
    private ProgressDialog mDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ((WaveApp) getApplication()).getComponent().inject(this);
        mPresenter.attachView(this);
        mPresenter.getUserProfile();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void showLoading(boolean isLoading) {
        if (isLoading) {
            mDialog = CommonUtils.showLoadingDialog(this);
        } else {
            if (mDialog != null)
                mDialog.dismiss();
        }
    }

    @Override
    public void onSuccess(UserProfile mProfile) {
        txtTitle.setText(String.format("%s %s", mProfile.getFirstName(), mProfile.getLastName()));
        txtDesignation.setText(mProfile.getDesignation());
        txtFollowers.setText(String.valueOf(mProfile.getFollowersCounter()));
        txtFollowing.setText(String.valueOf(mProfile.getFollowingCount()));
        txtUsername.setText(mProfile.getUserName());
        txtPhone.setText(mProfile.getMobileNumber());
        txtWorkAt.setText(mProfile.getWorkAt());
        txtBio.setText(mProfile.getBio());
        Place mPlace = mProfile.getLocation().get(0);
        txtAddress.setText(String.format("%s %s %s", mPlace.getCity(), mPlace.getState(), mPlace.getCountry()));

        Glide.with(MainActivity.this).load(mProfile.getProfilePicUrl()).apply(new RequestOptions().centerCrop().circleCrop().placeholder(R.drawable.profile_pic)).into(imageViewProfilePic);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter.detachView();
    }

    @Override
    public void onError(String message) {
        Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content),
                message, Snackbar.LENGTH_SHORT);
        View sbView = snackbar.getView();
        TextView textView = sbView
                .findViewById(android.support.design.R.id.snackbar_text);
        textView.setTextColor(ContextCompat.getColor(this, R.color.white));
        snackbar.show();
    }
}

After following all above just RUN the project and use app, If you have any queries, feel free to ask them in the comment section below.

For the best android app architect read our article MVP Architect Android apps with Dagger 2, Retrofit & RxJava 2.

Write A Comment