
Handling success data and error callback responses from a network for Android projects using Sandwich
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) - Using
request
extension on an instance of theCall
.
Using coroutines
We can use suspend
keyword in our Retrofit services and setApiResponse<T>
as a response type. Build your Retrofit using the ApiResponseCallAdapterFactory
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.