Tag

JsonSyntaxException

Browsing

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.

During app development, We have always faced errors and exceptions and crashes due to APIs failures. As a native mobile developer, it very important to ensure the app never crashes at the end-user. You guys also aware Retrofit with RxJava is mostly used for calling external APIs In this tutorials, I’m going to write a solution to retrofit handle error in android while integrating external APIs in a single place.

Basically, the mobile application is dependent on the API development process and doesn’t guarantee the APIs response will be always expected. If the problem is backend then they prepare a patch of containing bug fix and deployed. But in the mobile app development, it’s not happened. You depend on review times of Google Play/App Store if you need to quickly deploy a patch containing a bug fix. In this case, you also need your users to update the app after the approval to get it fixed.

The following API instability occur while integrating Rest APIs

  1. NullPointerException
  2. Manage HttpException
    • Forbidden Request
    • Internal Server Error
    • Unauthorized Exception
    • Check for “Bad Request” and throw IncorrectLoginPasswordException
    • ConnectionException – No Internet
    • Any other error is just NetworkException
  3. While APIs response format is not expected. eg, JsonSyntaxException BEGIN_OBJECT but was BEGIN_ARRAY, Expected BEGIN_OBJECT but was STRING

1. NullPointerException

NullPointerException is most common exception occurs when you performing operation of null object. Let’s suppose our app has a screen containing RecyclerView that showing feed data from a model class. The model class is simple POJO that containing the following attributes described by the API.

public class Row {

    @SerializedName("title")
    @Expose
    private String title;
    @SerializedName("description")
    @Expose
    private String description;
    @SerializedName("imageHref")
    @Expose
    private String imageHref;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getImageHref() {
        return imageHref;
    }

    public void setImageHref(String imageHref) {
        this.imageHref = imageHref;
    }
}

On the screen, we have decided to show the title with uppercase letters. like below

TextView titleView = (TextView) findViewById(R.id.tvTitle);
titleView.setText(row.getTitle().toUpperCase());

Problem Statement

The app now is working fine as expected, you submitted at Google Play and user are happy to use it. Now let’s suppose APIs stop the sending title parameter. then what happens.? The app will crash when trying to show the title because we are applying toUpperCase () method on null object.

Proposed Solution

The proposed solution validates the model before it’s sent to the view, and somehow validation failed, show the error message to the user. But runtime just checking every property against null, not a good practice. It’s affected your app performance. then how to do .?

We need to find out the way so we tell compiler at runtime which properties we want to be verified. We can do using annotations.

Annotations, a form of metadata, provide data about a program that is not part of the program itself.

Create model validator script

Go to src and create a file with IsDefined name

package com.androidwave.utils;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created on : Jan 13, 2019
 * Author     : Morris
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IsDefined {
}

In this class, we are using two annotations

  • Target(ElementType.FIELD) – That means the IsDefined annotation should be available at runtime
  • Retention(RetentionPolicy.RUNTIME) – It means the IsDefined will annotate class attributes.

Create a POJO validator class

Let’s create a POJO validator class that will receive a model object, search each attribute that annotated by IsDefined and if it has null value will throw an exception.

package com.androidwave.utils;

import android.support.annotation.NonNull;

import java.lang.reflect.Field;

/**
 * Created on : Jan 13, 2019
 * Author     : Morris
 */
public class PojoValidator {
    private Object model;

    public PojoValidator(@NonNull Object model)  {
        this.model = model;
    }

    public void validate() throws IllegalArgumentException {

        if (model == null) {
            throw new IllegalArgumentException("Model cannot be null");
        }

        final Field[] modelFields = model.getClass().getDeclaredFields();

        for (Field modelField : modelFields) {
            validateIsDefinedField(modelField);
        }
    }

    private void validateIsDefinedField(Field field) {

        if (field.isAnnotationPresent(IsDefined.class)) {

            Object attributeValue = null;
            field.setAccessible(true);

            try {
                attributeValue = field.get(model);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            if (attributeValue == null) {
                throw new IllegalArgumentException(field + " is required");
            }

        }
    }
}

2. Manage HttpException

 subscription.add(
                service.getFeedResponse()
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnTerminate(() -> {
                            if (view != null) {
                                view.onFetchDataCompleted();
                            }
                        })
                        .subscribe(feedResponse -> {
                            if (view != null) {
                                /**
                                 * Update view here
                                 */
                                view.onFetchDataSuccess(feedResponse);
                            }
                        }, error -> {
                           handleApiError(error);
                        })
        );
Definition of handle API error
 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());

            }
        }
    }

3. The APIs response format is not expected

This problem mostly occurs during development going on, So I have written a separate article with complete source code, Read here Retrofit Globally Error Handling 🙂