Handling success data and error callback responses from a network for Android projects using Sandwich
By: Jaewoong Eum
Tags: Http, Android, Retrofit, Okhttp, Network

In modern Android development, constructing network structure is an important factor and we can implement network communication structure using HTTP client tools like OkHttp, Retrofit, or Volley. Also, it is an important factor to design how to handle callback responses from the network. Many people standardize callback responses by creating wrapper classes like Resource , Result , or Response and handling success data or error messages depending on the response state like the below.

However, what if we want to handle error cases depending on its HTTP status code (401, 402. 403..) or handle each error response globally for logging/debugging error messages or showing toasts? Or what if we want to get directly typed wrapper classes from every different service without creating new instances of wrappers every time manually? Probably we have to write a bunch of codes and classes for building the response handling. Constructing the structure of the standardized response requires a lot of resources at the initial time. In this post, we will take a look at how to handle responses and reduce our setup time for standardizing callbacks using a library Sandwich.

Sandwich was invented for constructing a standardized interface of the response from the network callback communication. It helps to reduce our construction time and makes focus us on business logic. We can handle success data, error responses, and exceptional cases intuitively using useful extensions of the interface.

Moreover, Sandwich supports handling error responses globally, Mapper, Operator, and great compatibilities with modern Android like LiveData, coroutines, and flow.

Sandwich has been downloaded in more than 50K around the world!

Including

To use this library, we should add its dependency to our build.gradle file. And you can check out the recent version here.

ApiResponse

ApiResponse is an abstract for constructing standard responses from the response of the retrofit call. It provides useful extensions for handling success data, error responses, and exceptional cases. We can get ApiResponse using extensions from an instance of Call or we can just set it as a response type on the suspend function using coroutines. There are three types of the ApiResponse .

ApiResponse.Success

A standard success response class that inherits ApiResponse . We can get body data of the response, StatusCode , Headers , and more details about the network callback response.

ApiResponse.Failure.Error

A standard error response class that inherits ApiResponse . API communication conventions are not matched or applications need to handle errors. e.g., internal server error. We can get a ResposeBody , StatusCode , Headers , and more details about the error response.

ApiResponse.Failure.Exception

A standard exceptional response class that inherits ApiResponse . An unexpected exception occurs while creating requests or processing a response on the client-side. e.g., Network connection error. We can get an exception message from this response.

How to get ApiResponse

We can get ApiRespose from the Retrofit call in two ways.

Using Coroutines (set ApiResponse<T> as a return type of a suspend function)

as a return type of a suspend function) Using request extension on an instance of the Call .

Using coroutines

We can use suspend keyword in our Retrofit services and set ApiResponse<T> as a response type. Build your Retrofit using the CoroutinesResponseCallAdapterFactory call adapter factory.

We should make Retrofit service functions as suspension functions using the suspend keyword. And we can set ApiResponse<T> as a response type. So we can get the ApiResponse from the Retrofit service call, and handle them right away using extensions.

And we can get ApiResponse<MODEL> and handle responses depending on its state using extensions. (onSuccess, onError, onException) By using those scope extensions, we don’t need to check type using exhaustive when-statement or if-else statement. If we want to handle only the success case, we can omit the other extensions. (onError, onException) The opposite case is similar.

Or we can emit success data in Flow using suspendOnSuccess like the below. If we want to use suspension functions in the response handling scopes, we can use suspendOnSuccess , suspendOnError , suspendOnException instead. This is useful to use on repository pattern for emitting flow data to ViewModel.

Using Call instance

If you are not a coroutines user, we can get ApiResponse from an instance of Call using request extension.

Mapper

Mapper is useful when we want to transform ApiResponse.Success or ApiResponse.Failure.Error to our custom model in our ApiResponse extension scopes.

ApiSuccessModelMapper

We can map the ApiResponse.Success model to our custom model using the ApiSuccessModelMapper<T, R> and map extension. We should create a custom mapper that inherits ApiSuccessModelMapper for converting the original ApiResponse.Success model to our custom model.

We can use the map extension with a lambda.

If we want to get a converted data in the lambda scope, we can give the mapper as a parameter for the onSuccess or suspendOnSuccess .

ApiErrorModelMapper

We can map the ApiResponse.Failure.Error model to our custom error model using the ApiErrorModelMapper<T> and map extension.

If we want to get the transformed data from the start in the lambda, we can give the mapper as a parameter for the onError or suspendOnError .

Operator

We can delegate the onSuccess , onError , onException using the operator extension and ApiResponseOperator . The Operator is very useful if we want to handle ApiResponse s standardly or reduce the role of the ViewModel and Repository . Here is an example of standardized error and exception handling. For example, we can create an operator CommonResponseOperator for handling error cases and exceptional cases. We will handle success responses manually.

And we can use it on the ApiResponse using operator extension.

Operator for coroutines

If we want to operate and delegate a suspension lambda to the operator, we can use the suspendOperator extension and ApiResponseSuspendOperator class. For example, we can create an operator CommonResponseOperator extends ApiResponseSuspendOperator with suspend override methods.

And we can use suspension functions like emit in the success lambda.

Global operator

We can operate an operator globally all ApiResponse s in our application using SandwichInitializer . So we don't need to create every instance of Operators or use dependency injection for handling common operations. Here is an example of handling a global operator of ApiResponse.Failure.Error and ApiResponse.Failure.Exception . In this example, We will handle ApiResponse.Success manually.

Application class

We can initialize the global operator on the SandwichInitializer.sandwichOperator . It is recommended to initialize it in the Application class.

GlobalResponseOperator

The GlobalResponseOperator can extend any operator classes. ( ApiResponseSuspendOperator or ApiResponseOperator )

ViewModel/Repository

We don’t need to use the operator expression anymore. Because we set the global operator via the SandwichInitializer . The global operator will be operated automatically on each ApiResponse s, so we should handle only the successful case.

Merge

We can merge multiple ApiResponse s as one ApiResponse depending on the policy. The below example is merging three ApiResponse s as one if every three ApiResponse s are successful.

ApiResponseMergePolicy

ApiResponseMergePolicy is a policy for merging response data depend on the success or not.

IGNORE_FAILURE: Regardless of the merging order, ignores failure responses in the responses.

PREFERRED_FAILURE (default): Regardless of the merging order, prefers failure responses in the responses.

toLiveData

We can get a LiveData that contains success data if the response is an ApiResponse.Success . If our goal is only to get a LiveData that holds success data, we can emit the onSuccess extension.

If we want to transform the original data and get a LiveData that contains transformed data using success data if the response is an ApiResponse.Success .

toFlow

We can get a Flow that emits success data if the response is an ApiResponse.Success and the data is not null.

If we want to transform the original data and get a flow that contains transformed data using success data if the response is an ApiResponse.Success and the data is not null.

Conclusion

Designing and constructing the network response handling is important because it affects many factors in our project. If we failed to well-design the network structure, not only our business codes could not be constructed clearly but also UI logic. In this post, I’ve introduced the library Sandwich that constructs a standardized callback interface of the response from the network callback communication. By using this library, we can reduce our construction time on the initial project and we can handle the callback responses fastly depending on their state. And you can check out some use case repositories of this library in the below list. Thanks for reading and happy coding!

Ues cases

Pokedex — 🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture.

DisneyMotions — 🦁 A Disney app using transformation motions based on MVVM (ViewModel, Coroutines, LiveData, Room, Repository, Koin) architecture.

MarvelHeroes — ❤️ A sample Marvel heroes application based on MVVM (ViewModel, Coroutines, LiveData, Room, Repository, Koin) architecture.

TheMovies2 — 🎬 A demo project using The Movie DB based on Kotlin MVVM architecture and material design & animations.

Sandwich