Retrofit error handling

Future Studio provides on-demand learning & wants you to become a better Android (Retrofit, Gson, Glide, Picasso) and Node.js/hapi developer!

Two weeks ago, you’ve seen how to log requests and responses for debugging purposes. Requests might not finish successfully and you have to take care of failure situations. Most of the time, you need to manually apply the correct action like showing an error message as user feedback. If you get more than just the response status code, you can use the additional data to set the user in the right context and provide more information about the current error situation. That’s what this post is about: how to apply simple error handling using Retrofit 2.

Retrofit Series Overview

  • Retrofit
  • Requests
  • Responses
  • Converters
  • Error Handling
  • Logging
  • Calladapters
  • Pagination
  • File Upload & Download
  • Authentication
  • Caching
  • Testing & Mocking
  • Java Basics for Retrofit

>

Error Handling Preparations

Even though you want your app to always work like expected and there shouldn’t be any issues while executing requests. However, you’re not in control of when servers will fail or users will put wrong data which results in errors returned from the requested API. In those cases, you want to provide as much feedback to the user required to set him/her into the right context so that he/she understands what the issue is.

Before diving into the actual request which results in an error, we’re going to prepare classes to parse the response body which contains more information.

Error Object

At first, we create the error object representing the response you’re receiving from your requested API. Let’s assume your API sends a JSON error body like this:

{
    statusCode: 409,
    message: "Email address already registered"
}

If we would just show the user a generic error message like There went something wrong, he/she would immediately be upset about this stupid app which isn’t able to show what went wrong.

To avoid these bad user experiences, we’re mapping the response body to a Java object, represented by the following class.

public class APIError {

    private int statusCode;
    private String message;

    public APIError() {
    }

    public int status() {
        return statusCode;
    }

    public String message() {
        return message;
    }
}

We don’t actually need the status code inside the response body, it’s just for illustration purposes and this way you don’t need to extra fetch it from the response.

Simple Error Handler

We’ll make use of the following class only having one static method which returns an APIError object. The parseError method expects the response as parameter. Further, you need to make your Retrofit instance available to apply the appropriate response converter for the received JSON error response.

public class ErrorUtils {

    public static APIError parseError(Response<?> response) {
        Converter<ResponseBody, APIError> converter = 
                ServiceGenerator.retrofit()
                        .responseBodyConverter(APIError.class, new Annotation[0]);

        APIError error;

        try {
            error = converter.convert(response.errorBody());
        } catch (IOException e) {
            return new APIError();
        }

        return error;
    }
}

We’re exposing our Retrofit instance from ServiceGenerator via static method (if you’re not familiar with the ServiceGenerator, please read the introductory post of this series). Please bear with us that we’re using a kind of hacky style by exposing the Retrofit object via static method. The thing that is required to parse the JSON error is the response converter. And the response converter is available via our Retrofit object.

At first, we’re getting the error converter from the ServiceGenerator.retrofit() instance by additionally passing our APIError class as the parameter to the responseBodyConverter method. The responseConverter method will return the appropriate converter to parse the response body type. In our case, we’re expecting a JSON converter, because we’ve received JSON data.

Further, we call converter.convert to parse the received response body data into an APIError object. Afterwards, we’ll return the created object.

Error Handler in Action

Retrofit 2 has a different concept of handling «successful» requests than Retrofit 1. In Retrofit 2, all requests that can be executed (sent to the API) and for which you’re receiving a response are seen as «successful». That means, for these requests the onResponse callback is fired and you need to manually check whether the request is actual successful (status 200-299) or erroneous (status 400-599).

If the request finished successfully, we can use the response object and do whatever we wanted. In case the error actually failed (remember, status 400-599), we want to show the user appropriate information about the issue.

Call<User> call = service.me();  
call.enqueue(new Callback<User>() {  
    @Override
    public void onResponse(Call<User> call, Response<User> response) {
        if (response.isSuccessful()) {
            // use response data and do some fancy stuff :)
        } else {
            // parse the response body …
            APIError error = ErrorUtils.parseError(response);
            // … and use it to show error information

            // … or just log the issue like we’re doing :)
            Log.d("error message", error.message());
        }
    }

    @Override
    public void onFailure(Call<User> call, Throwable t) {
        // there is more than just a failing request (like: no internet connection)
    }
});

As you can see, we use the ErrorUtils class to parse the error body and get an APIError object. Use this object and the contained information to show a meaningful message instead of a generic error message.

Outlook

This article shows you a simple way to manage errors and extract information from the response body. Most APIs will send you specific information on what went wrong and you should make use of it.

This is just the tip of the iceberg when it comes to error handling. Within Retrofit 1, you had the opportunity to add a custom error handler. This option was removed from Retrofit 2 and we think it’s good the way it is. We’ll tell you about more advanced techniques on error handling with Retrofit 2 within a future blog post.

If you run into any issue or have a question, please let us know in the comments below or tweet us @futurestud_io.


Still Have Questions? Get Our Retrofit Book!

Retrofit Book

All modern Android apps need to do network requests. Retrofit offers you an extremely convenient way of creating and managing network requests. From asynchronous execution on a background thread, to automatic conversion of server responses to Java objects, Retrofit does almost everything for you. Once you’ve a deep understanding of Retrofit, writing complex requests (e.g., OAuth authentication) will be done in a few minutes.

Invest time to fully understand Retrofit’s principles. It’ll pay off multiple times in the future! Our book offers you a fast and easy way to get a full overview over Retrofit. You’ll learn how to create effective REST clients on Android in every detail.

Boost your productivity and enjoy working with complex APIs.

I want to handle error in Retrofit 2.0

Got e.g. code=404 and body=null, but errorBody() contains data in ErrorModel (Boolean status and String info).

This is errorBody().content: [text=n{"status":false,"info":"Provided email doesn't exist."}].

How can I get this data?

Thank for helping me!

This is my code for Retrofit request:

ResetPasswordApi.Factory.getInstance().resetPassword(loginEditText.getText().toString())
    .enqueue(new Callback<StatusInfoModel>() {
        @Override
        public void onResponse(Call<StatusInfoModel> call, Response<StatusInfoModel> response) {
            if (response.isSuccessful()) {
                showToast(getApplicationContext(), getString(R.string.new_password_sent));
            } else {
                showToast(getApplicationContext(), getString(R.string.email_not_exist));
            }
        }

        @Override
        public void onFailure(Call<StatusInfoModel> call, Throwable t) {
            showToast(getApplicationContext(), "Something went wrong...");
        }
    });

asked Jul 11, 2016 at 10:10

y07k2's user avatar

y07k2y07k2

1,8584 gold badges19 silver badges36 bronze badges

2

If you want to get data when error response comes (typically a response code except 200) you can do it like that in your onResponse() method:

if (response.code() == 404) {
    Gson gson = new GsonBuilder().create();
    YourErrorPojo pojo = new YourErrorPojo();
    try {
         pojo = gson.fromJson(response.errorBody().string(), YourErrorPojo.class);
         Toast.makeText(context, pojo.getInfo(), Toast.LENGTH_LONG).show();
    } catch (IOException e) { 
      // handle failure at error parse 
  }
}

When generating YourErrorPojo.class do following steps :

  1. Go to Json Schema 2 Pojo

  2. Paste your example Json, and select source type Json , annotation Gson

  3. Your example Json is : {"status":false,"info":"Provided email doesn't exist."}

  4. Click Preview and it will generate your Pojo class for you.

Add this to your build.gradle : compile 'com.google.code.gson:gson:2.7'

I used Gson in this solution but you can get your Json string using: response.errorBody().string()

Jamil Hasnine Tamim's user avatar

answered Jul 11, 2016 at 10:59

Yasin Kaçmaz's user avatar

Yasin KaçmazYasin Kaçmaz

6,4635 gold badges40 silver badges58 bronze badges

6

Retrofit doesn’t see 404 as a failure, so it will enter the onSuccess.

response.isSuccessful() is true if the response code is in the range of 200-300, so it will enter the else there.

if (response.isSuccessful()) {
    showToast(getApplicationContext(), getString(R.string.new_password_sent));
} else {
    // A 404 will go here

    showToast(getApplicationContext(), getString(R.string.email_not_exist));
}

However since your response was not successful, you do not get the response body with .body(), but with errorBody(), errorBody will filled when the request was a success, but response.isSuccessful() returns false (so in case of a status code that is not 200-300).

answered Jul 11, 2016 at 10:35

Tim's user avatar

TimTim

41k18 gold badges129 silver badges143 bronze badges

5

I’m using this library Retrobomb, you don’t have to serialize at that level.
it’s easy to use and customize. It supports annotation for each error type or error code.
If you prefer you can unwrap all errors and handle by your self.

@ErrorMapping(code = 401, errorType = Unauthorized.class)
@PATCH("/v1/widgets/{id}")
  Single<Widget> updateWidget(@Path("id") String id, @Body Widget widget);

answered Jul 23, 2018 at 15:27

schwertfisch's user avatar

schwertfischschwertfisch

4,5491 gold badge17 silver badges32 bronze badges

If you want to get data when error response comes (typically a response code except 200) you can do it like that in your onResponse() method:

override fun onResponse(call: Call<LoginData>?, response: Response<LoginData>?) {
    if (response != null) {
        if (response.code() == 200 && response.body() != null) {
            val loginData = response.body()
            if (loginData != null) {
                //Handle success case...
            }
        } else if (response.code() == 401) {
            val converter = ApiClient.getClient()?.responseBodyConverter<ErrorResponseData>(
                ErrorResponseData::class.java,
                arrayOfNulls<Annotation>(0))
            var errorResponse: ErrorResponseData? = null
            errorResponse = converter?.convert(response.errorBody())
            if (errorResponse != null) {
                //Handle Error case..
            }
        }
    }
}

Olcay Ertaş's user avatar

Olcay Ertaş

5,8178 gold badges77 silver badges110 bronze badges

answered Mar 14, 2018 at 11:03

Brijesh L.N's user avatar

For Kotlin:

Just follow this code to convert your errorBody to your response:

if(response.isSuccessful){
     val data = response.body()!!
               
 }else {
    val gson = GsonBuilder().create()

    try {
        var pojo = gson.fromJson(
            response.errorBody()!!.string(),
            CommentResponse::class.java)
            Log.e("ERROR_CHECK","here else is the error$pojo")

     } catch (e: IOException) {
           // handle failure at error parse
  }
}

answered Nov 13, 2021 at 18:11

Jamil Hasnine Tamim's user avatar

you can do in the following way

fun parseErrorBody(readText: String?): String {
    if (!readText.isNullOrEmpty()) {
        val result = Gson().fromJson(readText, AppError::class.java)//AppError can be your server error response model
        return result.errors?.get(0)?.message ?: Constants.EMPTY_STR
    }
    return ""
}

and calling code

    if(response.isSuccessful){}//handle success response
else{
    parseErrorBody(response.errorBody()?.charStream()?.readText())
}

answered Jul 11, 2022 at 13:56

Mohd Qasim's user avatar

Mohd QasimMohd Qasim

7408 silver badges19 bronze badges

Содержание

  1. Android — Retrofit 2 Custom Error Response Handling
  2. Preparation
  3. Parsing Error Response
  4. Retrofit 2 — Error Handling for Synchronous Requests
  5. Moonshoot
  6. Retrofit Series Overview
  7. Possible Retrofit Errors
  8. Reminder: Asynchronous Environment
  9. Synchronous Environment
  10. Outlook
  11. Still Have Questions? Get Our Retrofit Book!
  12. Get Notified on New Future Studio Content and Platform Updates
  13. Retrofit Error Handling Android in Single Place
  14. Problem Use Case
  15. User API response JSON
  16. Now you have create a POJO for parsing this JSON object.
  17. API response in case of location is string array
  18. Solution
  19. Create a response wrapper class
  20. Create a error wrapper class
  21. Create a JSON Converter Factory
  22. Create a response body converter
  23. Set the WrapperConverterFactory in Retrofit client
  24. Let’s create MVP Contract for main activity, Normally create in MVP pattern
  25. In UI package create Presenter which implements MainMvp.Presenter
  26. Implement view in activity like below

Android — Retrofit 2 Custom Error Response Handling

Usually, when using Retrofit 2, we have two callback listeners: onResponse and onFailure If onResponse is called, it doesn’t always mean that we get the success condition. Usually a response is considered success if the status scode is 2xx and Retrofit has already provides isSuccessful() method. The problem is how to parse the response body which most likely has different format. In this tutorial, I’m going to show you how to parse custom JSON response body in Retrofit 2.

Preparation

First, we create RetrofitClientInstance class for creating new instance of Retrofit client and the MyService interface which defines the endpoint we’re going to call.

RetrofitClientInstance.java

MyService.java

Let’s say we have an endpoint /api/items which in normal case, it returns the list of Item objects.

Item.java

Below is the standard way to call the endpoint.

Example.java

Parsing Error Response

The problem is how to parse the response if it’s not success. The idea is very simple. We need to create custom util for parsing the error. For example, we know that the server will return a JSON object with the following structure when an error occurs.

We need to create a util for parsing the error. It returns an Object containing parsed error body. First, we define the class which represents the parsed error.

APIError.java

And here’s the util for parsing the response body. If you have different response body format, you can adjust it to suit your case.

ErrorUtils.java

Finally, change the onResponse and onFailure methods. If response.isSuccessful() is false, we use the error parser. In addition, if the code go through onFailure which most likely we’re even unable to get the error response body, just return the default error.

Example.java

That’s how to parse error body in Retrofit 2. If you also need to define custom GSON converter factory, read this tutorial.

Источник

Retrofit 2 — Error Handling for Synchronous Requests

Moonshoot

Moonshoot is a
Student Feature.

When implementing network requests on mobile devices a lot of things can go wrong: no network connection or the server responds with an error. In either case you need to make sure you deal with the error and, if necessary, inform the user about the issue. In the tutorial on simple error handling we introduced the basics of dealing with those errors in Retrofit.

In this short tutorial, we’ll apply the same approach for synchronous requests. This is helpful when you’re doing synchronous requests, for example on background services.

Retrofit Series Overview

Getting Started and Creating an Android Client

Basics of API Description

Creating a Sustainable Android Client

URL Handling, Resolution and Parsing

How to Change API Base Url at Runtime

Multiple Server Environments (Develop, Staging, Production)

Share OkHttp Client and Converters between Retrofit Instances

Upgrade Guide from 1.9

Beyond Android: Retrofit for Java Projects

How to use OkHttp 3 with Retrofit 1

  1. Getting Started and Creating an Android Client
  2. Basics of API Description
  3. Creating a Sustainable Android Client
  4. URL Handling, Resolution and Parsing
  5. How to Change API Base Url at Runtime
  6. Multiple Server Environments (Develop, Staging, Production)
  7. Share OkHttp Client and Converters between Retrofit Instances
  8. Upgrade Guide from 1.9
  9. Beyond Android: Retrofit for Java Projects
  10. How to use OkHttp 3 with Retrofit 1

Synchronous and Asynchronous Requests

Send Objects in Request Body

Add Custom Request Header

Manage Request Headers in OkHttp Interceptor

Dynamic Request Headers with @HeaderMap

Multiple Query Parameters of Same Name

Optional Query Parameters

Send Data Form-Urlencoded

Send Data Form-Urlencoded Using FieldMap

How to Add Query Parameters to Every Request

Add Multiple Query Parameter With QueryMap

How to Use Dynamic Urls for Requests

Constant, Default and Logic Values for POST and PUT Requests

Reuse and Analyze Requests

Optional Path Parameters

How to Send Plain Text Request Body

Customize Network Timeouts

How to Trust Unsafe SSL certificates (Self-signed, Expired)

Dynamic Endpoint-Dependent Interceptor Actions

How to Update Objects on the Server (PUT vs. PATCH)

How to Delete Objects on the Server

  1. Synchronous and Asynchronous Requests
  2. Send Objects in Request Body
  3. Add Custom Request Header
  4. Manage Request Headers in OkHttp Interceptor
  5. Dynamic Request Headers with @HeaderMap
  6. Multiple Query Parameters of Same Name
  7. Optional Query Parameters
  8. Send Data Form-Urlencoded
  9. Send Data Form-Urlencoded Using FieldMap
  10. How to Add Query Parameters to Every Request
  11. Add Multiple Query Parameter With QueryMap
  12. How to Use Dynamic Urls for Requests
  13. Constant, Default and Logic Values for POST and PUT Requests
  14. Cancel Requests
  15. Reuse and Analyze Requests
  16. Optional Path Parameters
  17. How to Send Plain Text Request Body
  18. Customize Network Timeouts
  19. How to Trust Unsafe SSL certificates (Self-signed, Expired)
  20. Dynamic Endpoint-Dependent Interceptor Actions
  21. How to Update Objects on the Server (PUT vs. PATCH)
  22. How to Delete Objects on the Server

Ignore Response Payload with Call

Receive Plain-String Responses

Crawl HTML Responses with jspoon (Wikipedia Example)

Loading Data into RecyclerView and CardView

  1. Ignore Response Payload with Call
  2. Receive Plain-String Responses
  3. Crawl HTML Responses with jspoon (Wikipedia Example)
  4. Loading Data into RecyclerView and CardView

Introduction to (Multiple) Converters

Adding & Customizing the Gson Converter

Implementing Custom Converters

How to Integrate XML Converter

Access Mapped Objects and Raw Response Payload

Supporting JSON and XML Responses Concurrently

Handling of Empty Server Responses with Custom Converter

Send JSON Requests and Receive XML Responses (or vice versa)

Unwrapping Envelope Responses with Custom Converter

Wrapping Requests in Envelope with Custom Converter

Define a Custom Response Converter

  1. Introduction to (Multiple) Converters
  2. Adding & Customizing the Gson Converter
  3. Implementing Custom Converters
  4. How to Integrate XML Converter
  5. Access Mapped Objects and Raw Response Payload
  6. Supporting JSON and XML Responses Concurrently
  7. Handling of Empty Server Responses with Custom Converter
  8. Send JSON Requests and Receive XML Responses (or vice versa)
  9. Unwrapping Envelope Responses with Custom Converter
  10. Wrapping Requests in Envelope with Custom Converter
  11. Define a Custom Response Converter

Simple Error Handling

Error Handling for Synchronous Requests

Catch Server Errors Globally with Response Interceptor

How to Detect Network and Conversion Errors in onFailure

  1. Simple Error Handling
  2. Error Handling for Synchronous Requests
  3. Catch Server Errors Globally with Response Interceptor
  4. How to Detect Network and Conversion Errors in onFailure

Log Requests and Responses

Enable Logging for Development Builds Only

Log Network Traffic with Stetho and Chrome Developer Tools

Using the Log Level to Debug Requests

Analyze Network Traffic with Android Studio Profiler

Debug and Compare Requests with RequestBin

  1. Log Requests and Responses
  2. Enable Logging for Development Builds Only
  3. Log Network Traffic with Stetho and Chrome Developer Tools
  4. Using the Log Level to Debug Requests
  5. Analyze Network Traffic with Android Studio Profiler
  6. Debug and Compare Requests with RequestBin

Introduction to Call Adapters

Custom Call Adapter to Separate OnResponse Callback

How to Integrate RxJava 1.x Call Adapter

How to Integrate RxJava 2.x Call Adapter

How to Integrate Guava Call Adapter

Custom Call Adapter to Separate Network and Gson Errors

  1. Introduction to Call Adapters
  2. Custom Call Adapter to Separate OnResponse Callback
  3. How to Integrate RxJava 1.x Call Adapter
  4. How to Integrate RxJava 2.x Call Adapter
  5. How to Integrate Guava Call Adapter
  6. Custom Call Adapter to Separate Network and Gson Errors

Pagination Using Query Parameter

Pagination Using Link Header and Dynamic Urls (Like GitHub)

Pagination Using Range Header Fields (Like Heroku)

  1. Pagination Using Query Parameter
  2. Pagination Using Link Header and Dynamic Urls (Like GitHub)
  3. Pagination Using Range Header Fields (Like Heroku)

How to Upload Files to Server

How to Upload Multiple Files to Server

How to Upload a Dynamic Amount of Files to Server

Upload Files with Progress

Passing Multiple Parts Along a File with @PartMap

How to Download Files from Server

Download Files with Progress Updates

How to Upload Files to Server

  1. How to Upload Files to Server
  2. How to Upload Multiple Files to Server
  3. How to Upload a Dynamic Amount of Files to Server
  4. Upload Files with Progress
  5. Passing Multiple Parts Along a File with @PartMap
  6. How to Download Files from Server
  7. Download Files with Progress Updates
  8. How to Upload Files to Server

Basic Authentication on Android

Token Authentication on Android

OAuth on Android

Hawk Authentication on Android

How to Refresh an Access Token

  1. Basic Authentication on Android
  2. Token Authentication on Android
  3. OAuth on Android
  4. Hawk Authentication on Android
  5. How to Refresh an Access Token

Activate Response Caching (Etag, Last-Modified)

Check Response Origin (Network, Cache, or Both)

Force Server Cache Support with Response Interceptor

Support App Offline Mode by Accessing Response Caches

Analyze Cache Files

  1. Activate Response Caching (Etag, Last-Modified)
  2. Check Response Origin (Network, Cache, or Both)
  3. Force Server Cache Support with Response Interceptor
  4. Support App Offline Mode by Accessing Response Caches
  5. Analyze Cache Files

Basics of Mocking Server Responses

Customizing Network Behavior of Mocked Server Responses

Mock APIs with JsonServer

  1. Basics of Mocking Server Responses
  2. Customizing Network Behavior of Mocked Server Responses
  3. Mock APIs with JsonServer

Fluent Interface with Builders

Possible Retrofit Errors

Retrofit’s default response handling separates the results into three categories:

  • Request failed (e.g., no network connection)
  • Request succeeded, but server responded with error (status code 4xx or 5xx)
  • Request succeeded, and server responded with success (status code 2xx)

Reminder: Asynchronous Environment

In the asynchronous environment you’ll get a separate callback onFailure() for the first result case ( Request failed ). The second and third result case is combined in one asynchronous callback onResponse() . You can differentiate between these two cases by checking with response.isSuccessful() . If the request failed, we introduced an error parser to get a useful error object.

Synchronous Environment

The general approach when doing synchronous requests is very similar. Let’s look at the general way to make synchronous requests:

At this point, you won’t be able to deal with any errors. Let’s catch the case where the request itself fails by wrapping it into a try-catch clause:

When Retrofit throws an IOException , you know that the request failed. Deal with that according to your app requirements.

The next step would be to catch the problem when the server responds with an error. The approach is identical to the asynchronous environment:

When the response comes with data, we’ll display it in the app. If the response is an error response, we use the ErrorUtils to parse it into a useful error object.

Lastly, let’s combine everything together:

Outlook

In this tutorial you’ve learned how you can apply the general error handling approach to synchronous requests. Make sure to deal with the potential problems to make your app even better!

If you’ve feedback or a question, let us know in the comments or on twitter @futurestud_io.

Enjoy coding & make it rock!

Still Have Questions? Get Our Retrofit Book!

All modern Android apps need to do network requests. Retrofit offers you an extremely convenient way of creating and managing network requests. From asynchronous execution on a background thread, to automatic conversion of server responses to Java objects, Retrofit does almost everything for you. Once you’ve a deep understanding of Retrofit, writing complex requests (e.g., OAuth authentication) will be done in a few minutes.

Invest time to fully understand Retrofit’s principles. It’ll pay off multiple times in the future! Our book offers you a fast and easy way to get a full overview over Retrofit. You’ll learn how to create effective REST clients on Android in every detail.

Boost your productivity and enjoy working with complex APIs.

Get Notified on New Future Studio
Content and Platform Updates

Get your weekly push notification about new and trending
Future Studio content and recent platform enhancements

Источник

Retrofit Error Handling Android in Single Place

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

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

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

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

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

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 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.

Create a response body converter

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

For better understanding I’m show full code of retrofit client
Create a interface for get user details like below

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

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.

Implement view in activity like below

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.

Источник

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.

Android - Retrofit 2 Custom Error Response Handling

Usually, when using Retrofit 2, we have two callback listeners: onResponse and onFailure If onResponse is called, it doesn’t always mean that we get the success condition. Usually a response is considered success if the status scode is 2xx and Retrofit has already provides isSuccessful() method. The problem is how to parse the response body which most likely has different format. In this tutorial, I’m going to show you how to parse custom JSON response body in Retrofit 2.

Preparation

First, we create RetrofitClientInstance class for creating new instance of Retrofit client and the MyService interface which defines the endpoint we’re going to call.

RetrofitClientInstance.java

  public class RetrofitClientInstance {
      private static final String BASE_URL = "http://159.89.185.115:3500";

      public static Retrofit getRetrofitInstance() {
            return new retrofit2.Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
      }
  }

MyService.java

  public interface MyServie {
      @GET("/api/items")
      Call<List> getItems();
  }

Let’s say we have an endpoint /api/items which in normal case, it returns the list of Item objects.

Item.java

  public class Item {
      private String id;
      private String name;

      public Item(String id, String name) {
          this.id = id;
          this.name = name;
      }

     /* Getter and Setter here */
  }

Below is the standard way to call the endpoint.

Example.java

  MyService myService = RetrofitClientInstance
          .getRetrofitInstance()
          .create(MyService.class);
  Call<List<Item>> call = retailService.getItems();

  call.enqueue(new Callback<List<Item>>() {
      @Override
      public void onResponse(final Call<List<Item>> call, final Response<List<Item>> response) {
          List<Item> items = response.body();
          Toast.makeText(getContext(), "Success").show();
      }

      @Override
      public void onFailure(final Call call, final Throwable t) {
          Toast.makeText(getContext(), "Failed").show();
      }
  });

Parsing Error Response

The problem is how to parse the response if it’s not success. The idea is very simple. We need to create custom util for parsing the error. For example, we know that the server will return a JSON object with the following structure when an error occurs.

  {
    "success": false,
    "errors": [
      "Error message 1",
      "Error message 2"
    ]
  }

We need to create a util for parsing the error. It returns an Object containing parsed error body. First, we define the class which represents the parsed error.

APIError.java

  public class APIError {
      private boolean success;
      private ArrayList messages;

      public static class Builder {
          public Builder() {}

          public Builder success(final boolean success) {
              this.success = success;
              return this;
          }

          public Builder messages(final ArrayList messages) {
              this.messages = messages;
              return this;
          }

          public Builder defaultError() {
              this.messages.add("Something error");
              return this;
          }

          public APIError build() { return new APIError(this); }
      }

      private APIError(final Builder builder) {
          success = builder.successs;
          messages = builder.messages;
      }
  }

And here’s the util for parsing the response body. If you have different response body format, you can adjust it to suit your case.

ErrorUtils.java

  public class ErrorUtils {
      public static APIError parseError(final Response<?> response) {
          JSONObject bodyObj = null;
          boolean success;
          ArrayList messages = new ArrayList<>();

          try {
              String errorBody = response.errorBody().string();

              if (errorBody != null) {
                  bodyObj = new JSONObject(errorBody);

                  success = bodyObj.getBoolean("success");
                  JSONArray errors = bodyObj.getJSONArray("errors");

                  for (int i = 0; i < errors.length(); i++) {
                      messages.add(errors.get(i));
                  }
              } else {
                  success = false;
                  messages.add("Unable to parse error");
              }
          } catch (Exception e) {
              e.printStackTrace();

              success = false;
              messages.add("Unable to parse error");
          }

          return new APIError.Builder()
                  .success(false)
                  .messages(messages)
                  .build();
      }
  }

Finally, change the onResponse and onFailure methods. If response.isSuccessful() is false, we use the error parser. In addition, if the code go through onFailure which most likely we’re even unable to get the error response body, just return the default error.

Example.java

  MyService myService = RetrofitClientInstance
          .getRetrofitInstance()
          .create(MyService.class);
  Call<List<Item>> call = retailService.getItems();

  call.enqueue(new Callback<List<Item>>() {
      @Override
      public void onResponse(final Call<List<Item>> call, final Response<List<Item>> response) {
          if (response.isSuccessful()) {
              List<Item> items = response.body();
              Toast.makeText(getContext(), "Success").show();
          } else {
              apiError = ErrorUtils.parseError(response);
              Toast.makeText(getContext(), R.string.cashier_create_failed,
                      Toast.LENGTH_LONG).show();
          }
      }

      @Override
      public void onFailure(final Call<Cashier> call, final Throwable t) {
          apiError = new APIError.Builder().defaultError().build();
          Toast.makeText(getContext(), "failed").show();
      }
  });

That’s how to parse error body in Retrofit 2. If you also need to define custom GSON converter factory, read this tutorial.

Using domain exceptions in your app is an important step if you want to create abstractions over different 3rd party networking libraries like Retrofit or gRPC. Handling and mapping of these exceptions can quickly become a boilerplate that you and your colleagues have to think about and sooner or later you will probably forget to do it somewhere in your code. I want to explore how this task can be done automatically in a generic and clean way when using the Retrofit library.

Disclaimer: The reasoning about domain exceptions does not apply only to networking but to any layer in your code that accesses 3rd party libraries.

Recap

This post is a follow up to my previous post about mapping into domain exceptions. To quickly summarise what it’s all about — you should abstract all logic behind low level exceptions like `IOException` or Retrofit `HttpException` inside the M part of your MV* architecture and not handle these exceptions on the presentation layer. This gives you the opportunity to switch HTTP library or the logic behind detecting `NoInternetException` without the need to change your presentation layer.

Another disclaimer: I have mentioned that you should do this mapping inside the repository layer but well, we all evolve, and now my opinion is that the correct place to do that is inside your remote data source layer.

First implementation

I’ve created a sample project that will guide us through the process of improving exceptions mapping from manual tedious work to automatic mapping. 

The example is pretty simple: User can perform two actions, download a list of recipes or download a single recipe by its id. These exceptions are defined:

/**
 * Exception when communicating with the remote api. Contains http [statusCode].
 */
data class ApiException(val statusCode: String) : Exception()

/**
 * Exception indicating that device is not connected to the internet
 */
class NoInternetException : Exception()

/**
 * Not handled unexpected exception
 */
class UnexpectedException(cause: Exception) : Exception(cause)

/**
 * Exception indicating that recipe was not found on the API
 */
data class RecipeNotFoundException(val recipeId: RecipeId) : Exception()

The first solution is based on the previous post — mapping is done in data source

interface RemoteRecipesDataSource {

    suspend fun recipes(): List<Recipe>

    suspend fun recipe(id: RecipeId): Recipe
}

class RetrofitRecipesDataSource(
    private val recipesApiDescription: RecipesApiDescription
) : RemoteRecipesDataSource {

    override suspend fun recipes(): List<Recipe> {
        return try {
            recipesApiDescription.recipes().map { it.toRecipe() }
        } catch (e: Exception) {
            throw mapToDomainException(e)
        }
    }

    override suspend fun recipe(id: RecipeId): Recipe {
        return try {
            recipesApiDescription.recipeDetail(id.value).let {
                it.toRecipe()
            }
        } catch (e: Exception) {
            throw mapToDomainException(e) {
                if (it.code() == 404) {
                    RecipeNotFoundException(id)
                } else {
                    null
                }
            }
        }
    }
}

fun mapToDomainException(
    remoteException: Exception,
    httpExceptionsMapper: (HttpException) -> Exception? = { null }
): Exception {
    return when (remoteException) {
        is IOException -> NoInternetException()
        is HttpException -> httpExceptionsMapper(remoteException) ?: ApiException(remoteException.code().toString())
        else -> UnexpectedException(remoteException)
    }
}

As you can imagine, this approach works but you need to think about this every time you call your Retrofit method and it can be easily forgotten or a new developer on the project can easily miss that. Can we do better?

Custom CallAdapter

Retrofit has the support for custom `CallAdapter` that you register during initialisation. If you used RxJava with Retrofit you would need to use one so Retrofit can recognize Rx return types like `Single` or `Completable`. We use Coroutines now for asynchronous Retrofit calls and they do not require any special `CallAdapter` since version 2.6.0 but we can still register one and use it as a generic place where mapping of exceptions can be done.

First we need to create an instance of `CallAdapter.Factory`

class ErrorsCallAdapterFactory : CallAdapter.Factory() {

    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, Call<*>>? {
        if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType || returnType.actualTypeArguments.size != 1) {
            return null
        }

        val delegate = retrofit.nextCallAdapter(this, returnType, annotations)
        @Suppress("UNCHECKED_CAST")
        return ErrorsCallAdapter(
            delegateAdapter = delegate as CallAdapter<Any, Call<*>>
        )
    }
}

We first check if the return type is really a `Call` (even though you do not specify it directly in Retrofit interface, it’s still a `Call` underground) and if it is, we ask Retrofit to give us the built-in `CallAdapter` which we pass to the custom `ErrorsCallAdapter`

class ErrorsCallAdapter(
    private val delegateAdapter: CallAdapter<Any, Call<*>>
) : CallAdapter<Any, Call<*>> by delegateAdapter {

    override fun adapt(call: Call<Any>): Call<*> {
        return delegateAdapter.adapt(CallWithErrorHandling(call))
    }
}

We delegate all implementation of `CallAdapter` class to the retrieved `CallAdapter` and only override the `adapt` method where we intercept the call and wrap it with the `CallWithErrorHandling` where the actual magic happens.

class CallWithErrorHandling(
    private val delegate: Call<Any>
) : Call<Any> by delegate {

    override fun enqueue(callback: Callback<Any>) {
        delegate.enqueue(object : Callback<Any> {
            override fun onResponse(call: Call<Any>, response: Response<Any>) {
                if (response.isSuccessful) {
                    callback.onResponse(call, response)
                } else {
                    callback.onFailure(call, mapToDomainException(HttpException(response)))
                }
            }

            override fun onFailure(call: Call<Any>, t: Throwable) {
                callback.onFailure(call, mapToDomainException(t))
            }
        })
    }

    override fun clone() = CallWithErrorHandling(delegate.clone())
}

The same pattern with delegating implementation of `Call` interface to the original `Call` is applied and only the `enqueue` method is overrode with the logic of mapping the exceptions.

At this point we have a generic mapping of `ApiException` or `NoInternetException` in one place! But.. there is still one issue. How would we solve the `RecipeNotFoundException`? The remote data source still needs to have `try/catch` to handle this case:

class RetrofitRecipesDataSource(
    private val recipesApiDescription: RecipesApiDescription
) : RemoteRecipesDataSource {

    override suspend fun recipes(): List<Recipe> {
        return recipesApiDescription.recipes().map { it.toRecipe() }
    }

    override suspend fun recipe(id: RecipeId): Recipe {
        return try {
            recipesApiDescription.recipeDetail(id.value).let {
                it.toRecipe()
            }
        } catch (apiException: ApiException) {
            throw if (apiException.statusCode == "404") {
                RecipeNotFoundException(id)
            } else {
                apiException
            }
        }
    }
}

But it’s still an improvement — now we need to handle only special cases and in 90% of other cases it’s handled for us. But can we do even better?

Invocation with custom annotations

We can leverage another great feature of the Retrofit/OkHttp which is the pair of  `okhttp3.Request.tag(Class<*>)` method and `retrofit2.Invocation` class. Each OkHttp request can contain a `tag` which can be any object instance. When Retrofit uses OkHttp under the hood while performing the HTTP request it sets the instance of `Invocation` class as this tag. When you look how the class looks it’s only a wrapper over `java.lang.reflect.Method` and list of arguments of the interface function

public final class Invocation {

  private final Method method;
  private final List<?> arguments;
}

Through `Method` we have access for example to the annotations that are defined in the Retrofit interface on the called method. Can you see where I’m heading to? We can create custom annotation

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class ExceptionsMapper(val value: KClass<out HttpExceptionMapper>)

that contains a reference to the `KClass` of `HttpExceptionMapper` class

abstract class HttpExceptionMapper(protected val callArguments: List<String>) {

    abstract fun map(httpException: HttpException): Exception?
}

and now we can create this mapper instance for the `RecipeNotFoundException`

class RecipeDetailExceptionMapper(arguments: List<String>) : HttpExceptionMapper(arguments) {

    override fun map(httpException: HttpException): Exception? {
        return if (httpException.code() == 404) {
            RecipeNotFoundException(RecipeId(callArguments.first()))
        } else {
            null
        }
    }
}

How to tie all these pieces together? We update the `CallWithErrorHandling` from previous section with this improvement:

class CallWithErrorHandling(
    private val delegate: Call<Any>
) : Call<Any> by delegate {

    override fun enqueue(callback: Callback<Any>) {
        delegate.enqueue(object : Callback<Any> {
            override fun onResponse(call: Call<Any>, response: Response<Any>) {
                if (response.isSuccessful) {
                    callback.onResponse(call, response)
                } else {
                    callback.onFailure(call, mapExceptionOfCall(call, HttpException(response)))
                }
            }

            override fun onFailure(call: Call<Any>, t: Throwable) {
                callback.onFailure(call, mapExceptionOfCall(call, t))
            }
        })
    }

    fun mapExceptionOfCall(call: Call<Any>, t: Throwable): Exception {
        val retrofitInvocation = call.request().tag(Invocation::class.java)
        val annotation = retrofitInvocation?.method()?.getAnnotation(ExceptionsMapper::class.java)
        val mapper = try {
            annotation?.value?.java?.constructors?.first()
                ?.newInstance(retrofitInvocation.arguments()) as HttpExceptionMapper
        } catch (e: Exception) {
            null
        }
        return mapToDomainException(t, mapper)
    }

    override fun clone() = CallWithErrorHandling(delegate.clone())
}

In the method `mapExceptionOfCall` first the `Invocation` is retrieved, then the `ExceptionsMapper` annotation is queried and if it can be found, new instance is created and passed to the `mapToDomainException` helper function which we know from the beginning of the post

fun mapToDomainException(
    remoteException: Throwable,
    httpExceptionsMapper: HttpExceptionMapper? = null
): Exception {
    return when (remoteException) {
        is IOException -> NoInternetException()
        is HttpException -> httpExceptionsMapper?.map(remoteException) ?: ApiException(remoteException.code().toString())
        else -> UnexpectedException(remoteException)
    }
}

Now the only thing missing is to mark the method in Retrofit interface with the annotation

interface RecipesApiDescription {

    @GET("recipes")
    suspend fun recipes(): List<ApiRecipe>

    @GET("recipes/{recipeId}")
    @ExceptionsMapper(value = RecipeDetailExceptionMapper::class)
    suspend fun recipeDetail(@Path("recipeId") recipeId: String): ApiRecipe
}

and voilá. The remote data source now may look like this

class RetrofitRecipesDataSource(
    private val recipesApiDescription: RecipesApiDescription
) : RemoteRecipesDataSource {

    override suspend fun recipes(): List<Recipe> {
        return recipesApiDescription.recipes().map { it.toRecipe() }
    }

    override suspend fun recipe(id: RecipeId): Recipe {
        return recipesApiDescription.recipeDetail(id.value).let {
            it.toRecipe()
        }
    }
}

and it does not need to handle any exception mapping.

This approach has also one additional benefit — you can have a setup of your networking Retrofit code in one gradle library module and your mapping logic can be in a feature module that has a dependency on this module. You do not need to combine all of this together in one module.

Summary

We have leveraged a great mechanism of combination of Retrofit/OkHttp libraries to mitigate the need for handling remote call exceptions in the data sources/repositories. By passing all the logic to the low level `CallAdapter` and retrieving the `Invocation` instance of the Retrofit call we can keep our data sources clean. But even if you don’t want to mess with the annotations and `Invocation`s you can still use `CallAdapter` for common error handling or mapping of the responses.

You can find sample code here. Feel free to comment or write to me on my Twitter. Thanks for your time!

Last disclaimer: The sample code is mainly for demonstration purposes, the code can be optimised by eg. caching instances of the ExceptionMapper s and so on.

David Bilík

Android Team Lead

Bilda disassembles Android for more than 8 years. In his free time he loves to cook, bake, fry or perform any other sort of food preparation. Lately his new passion is fermenting. His kombucha “shrooms” are spreading through Ackee more and more.

Понравилась статья? Поделить с друзьями:
  • Retroarch как изменить язык
  • Retrieving info самп как исправить
  • Retracker local error
  • Retail 01 горит error
  • Ret s 04009 10 ошибка ман тга