Why Sandwich?
Sandwich was invented for constructing the standardized response interface from the network response. We can handle successful data, error response, and an exceptional case intuitively using useful extensions of the interface. So we don't need to design and implement wrapper classes like Resource
or Result
, and it helps to reduce our work time and makes focus on only business codes. Sandwich supports handling error responses globally, Mapper, Operator, and great compatibilities like toLiveData or toFlow. Also, we can implement great harmony with coroutines and flow in our projects using this library.
Download
? Sandwich has been downloaded in more than 70k Android projects all over the world!

Gradle
Add below codes to your root build.gradle
file (not your module build.gradle file).
And add a dependency code to your module's build.gradle
file.
SNAPSHOT
Snapshots of the current development version of Sandwich are available, which track the latest versions.
Usecase
You can reference the good use cases of this library in the below repositories.
- 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.
Table of contents
- ApiResponse
- onSuccess, onError, onException
- ApiResponse for coroutines
- suspendOnSuccess, suspendOnError, suspendOnException
- Retrieve success data
- Mapper
- Operator, Operator for coroutines, Global Operator
- Merge
- toLiveData. toFlow
- ResponseDataSource
Usage
ApiResponse
ApiResponse
is an interface for constructing standard responses from the response of the retrofit call. It provides useful extensions for handling successful data and error responses. We can get ApiResponse
using the scope extension request
from the Call
. The below example is the basic of getting an ApiResponse
from an instance of the Call
.
ApiResponse.Success
A standard Success response interface from Retrofit network responses.
We can get the successful body data of the response, StatusCode
, Headers
and etc from the ApiResponse.Success
.
ApiResponse.Failure.Error
A standard failure response interface from Retrofit network responses.
API communication conventions do not match or applications need to handle errors.
e.g., internal server error.
ApiResponse.Failure.Exception
An unexpected exception occurs while creating requests or processing an response in the client side. e.g., Network connection error.
ApiResponse Extensions
We can handle response cases conveniently using extensions.
onSuccess, onError, onException
We can use these scope functions to the ApiResponse
, we handle the response cases without using the if-else/when clause.
Each scope will be executed or not depending on the type of the ApiResponse
. (success, error, exception)
ApiResponse for coroutines
We can use the suspend
keyword in our Retrofit services and gets ApiResponse<*>
as a response type.
Build your Retrofit using the CoroutinesResponseCallAdapterFactory
call adapter factory.
We should make normal service functions as suspension functions using the suspend
keyword. And we can get the ApiResponse<*>
as a response type. So we can get the ApiResponse
from the Retrofit service call, and handle them right away using extensions.
We can use like the below.
suspendOnSuccess, suspendOnError, suspendOnException
We can use suspension extensions for invoking suspension related functions inside scopes. These extensions are not functionally different from the onSuccess
, onError
, and onException
extensions.
Generally, we can use this way on the repository pattern.
Retrieve success data
If we want to retrieve the encapsulated success data from the ApiResponse
directly, we can use the below functionalities.
getOrNull
Returns the encapsulated data if this instance represents ApiResponse.Success
or returns null if this is failed.
getOrElse
Returns the encapsulated data if this instance represents ApiResponse.Success
or returns a default value if this is failed.
getOrThrow
Returns the encapsulated data if this instance represents ApiResponse.Success
or throws the encapsulated Throwable
exception if this is failed.
Mapper
Mapper is useful when we want to transform the 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 SuccessPosterMapper<T, R>
and map
extension.
We can use the map
extension with a lambda.
If we want to get the transformed data from the start in the lambda, 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
. 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 handing.
ViewModel
We can delegate and operate the CommonResponseOperator
using the operate
extension.
CommonResponseOperator
The CommonResponseOperator
extends ApiResponseOperator
with the onSuccess
, onError
, onException
override methods. They will be executed depending on the type of the ApiResponse
.
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.
ViewModel
We can use suspension functions like emit
in the success
lambda.
CommonResponseOperator
The CommonResponseOperator
extends ApiResponseSuspendOperator
with suspend override methods.
Global operator
We can operate an operator globally all ApiResponse
s in our application using the SandwichInitializer
. So we don't need to create every instance of the Operators or use dependency injection for handling common operations. Here is an example of handling a global operator about the 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 (ApiResponseSuspendOperator
or ApiResponseOperator
)
ViewModel
We don't need to use the operator
expression. The global operator will be operated automatically, so we should handle only the ApiResponse.Success
.
Merge
We can merge multiple ApiResponse
s as one ApiResponse
depending on the policy.
The below example is merging three ApiResponse
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 successful data if the response is an ApiResponse.Success
. If our goal is only getting a LiveData that holds successful data, we can emit the onSuccess
extension.
If we want to transform the original data and get a LiveData
that contains transformed data using successful data if the response is an ApiResponse.Success
.
toFlow
We can get a Flow
that emits successful 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 successful data if the response is an ApiResponse.Success
and the data is not null.
ResponseDataSource
ResponseDataSource is an implementation of the DataSource
interface.
- Asynchronously send requests.
- A temporarily response data holder from the REST API call for caching data on memory.
- Observable for every response.
- Retry fetching data when the request gets failure.
- Concat another
DataSource
and request sequentially. - Disposable of executing works.
Combine
Combine a Call
and lambda scope for constructing the DataSource.
Request
Request API network call asynchronously.
If the request is successful, this data source will hold the success response model.
In the next request after the success, request() returns the cached API response.
If we need to fetch a new response data or refresh, we can use invalidate()
.
Retry
Retry fetching data (re-request) if your request got failure.
ObserveResponse
Observes every response data ApiResponse
from the API call request.
RetainPolicy
We can limit the policy for retaining data on the temporarily internal storage.
The default policy is no retaining any fetched data from the network, but we can set the policy using dataRetainPolicy
method.
Invalidate
Invalidate a cached (holding) data and re-fetching the API request.
Concat
Concat an another DataSource
and request API call sequentially if the API call getting successful.
asLiveData
we can observe fetched data via DataSource
as a LiveData
.
Disposable
We can make it joins onto CompositeDisposable
as a disposable using the joinDisposable
function. It must be called before request()
method. The below example is using in ViewModel. We can clear the CompositeDisposable
in the onCleared()
override method.
Here is the example of the ResponseDataSource
in the MainViewModel
.
DataSourceCallAdapterFactory
We can get the DataSource
directly from the Retrofit service.
Add a call adapter factory DataSourceCallAdapterFactory
to your Retrofit builder.
And change the return type of your service Call
to DataSource
.
Here is an example of the DataSource
in the MainViewModel.
CoroutinesDataSourceCallAdapterFactory
We can get the DataSource
directly from the Retrofit service using with suspend
.
Here is an exmaple of the DataSource
in the MainViewModel.
toResponseDataSource
We can change DataSource
to ResponseDataSource
after getting instance from network call using the below method.
GitHub
https://github.com/skydoves/Sandwich