In the previous article, I have discussed the 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 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 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 , 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.