
### Gradle
Add the dependency below into your **module**'s `build.gradle` file:
```gradle
dependencies {
implementation(platform("com.github.skydoves:sandwich-bom:2.4.0"))
implementation("com.github.skydoves:sandwich")
implementation("com.github.skydoves:sandwich-retrofit") // For Retrofit (Android)
testImplementation("com.github.skydoves:sandwich-test") // For Testing
}
```
For Kotlin Multiplatform, add the dependency below to your module's `build.gradle.kts` file:
```kotlin
sourceSets {
val commonMain by getting {
dependencies {
implementation(project.dependencies.platform("com.github.skydoves:sandwich-bom:$version"))
implementation("com.github.skydoves:sandwich")
implementation("com.github.skydoves:sandwich-ktor")
implementation("com.github.skydoves:sandwich-ktor-serialization")
implementation("com.github.skydoves:sandwich-ktorfit")
}
}
val commonTest by getting {
dependencies {
implementation("com.github.skydoves:sandwich-test")
}
}
}
```
## R8 / ProGuard
The specific rules are [already bundled](sandwich/consumer-rules.pro) into the JAR which can be interpreted by R8 automatically.
## Documentation
For comprehensive details about Sandwich, please refer to the complete [documentation available here](https://skydoves.github.io/sandwich/).
## Use Cases
You can also check out nice use cases of this library in the repositories below:
- [Pokedex](https://github.com/skydoves/pokedex): 🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture.
- [ChatGPT Android](https://github.com/skydoves/chatgpt-android): 📲 ChatGPT Android demonstrates OpenAI's ChatGPT on Android with Stream Chat SDK for Compose.
- [DisneyMotions](https://github.com/skydoves/DisneyMotions): 🦁 A Disney app using transformation motions based on MVVM (ViewModel, Coroutines, LiveData, Room, Repository, Koin) architecture.
- [MarvelHeroes](https://github.com/skydoves/marvelheroes): ❤️ A sample Marvel heroes application based on MVVM (ViewModel, Coroutines, LiveData, Room, Repository, Koin) architecture.
- [Neko](https://github.com/CarlosEsco/Neko): Free, open source, unofficial MangaDex reader for Android.
- [TheMovies2](https://github.com/skydoves/TheMovies2): 🎬 A demo project using The Movie DB based on Kotlin MVVM architecture and material design & animations.
## Usage
For comprehensive details about Sandwich, please refer to the complete [documentation available here](https://skydoves.github.io/sandwich/).
- [Retrofit Integration](https://skydoves.github.io/sandwich/retrofit)
- [Ktor Integration](https://skydoves.github.io/sandwich/ktor)
- [Ktorfit Integration](https://skydoves.github.io/sandwich/ktorfit)
- [Testing](https://skydoves.github.io/sandwich/testing)
### ApiResponse
`ApiResponse` serves as an interface designed to create consistent responses from API or I/O calls, such as network, database, or whatever. It offers convenient extensions to manage your payloads, encompassing both body data and exceptional scenarios. `ApiResponse` encompasses three distinct types: **Success**, **Failure.Error**, and **Failure.Exception**.
#### ApiResponse.Success
This represents a successful response from API or I/O tasks. You can create an instance of [ApiResponse.Success] by giving the generic type and data.
```kotlin
val apiResponse = ApiResponse.Success(data = myData)
val data = apiResponse.data
```
Depending on your model designs, you can also utilize `tag` property. The `tag` is an additional value that can be held to distinguish the origin of the data or to facilitate post-processing of successful data.
```kotlin
val apiResponse = ApiResponse.Success(data = myData, tag = myTag)
val tag = apiResponse.tag
```
#### ApiResponse.Failure.Exception
This signals a failed tasks captured by unexpected exceptions during API request creation or response processing on the client side, such as a network connection failure. You can obtain exception details from the `ApiResponse.Failure.Exception`.
```kotlin
val apiResponse = ApiResponse.Failure.Exception(exception = HttpTimeoutException())
val exception = apiResponse.exception
val message = apiResponse.message
```
#### ApiResponse.Failure.Error
This denotes a failed API or I/O request, typically due to bad requests or internal server errors. You can additionally put an error payload that can contain detailed error information.
```kotlin
val apiResponse = ApiResponse.Failure.Error(payload = errorBody)
val payload = apiResponse.payload
```
You can also define custom error responses that extend `ApiResponse.Failure.Error` or `ApiResponse.Failure.Exception`, as demonstrated in the example below:
```kotlin
data object LimitedRequest : ApiResponse.Failure.Error(
payload = "your request is limited",
)
data object WrongArgument : ApiResponse.Failure.Error(
payload = "wrong argument",
)
data object HttpException : ApiResponse.Failure.Exception(
throwable = RuntimeException("http exception")
)
```
The custom error response is very useful when you want to explicitly define and handle error responses, especially when working with map extensions.
```kotlin
val apiResponse = service.fetchMovieList()
apiResponse.onSuccess {
// ..
}.flatMap {
// if the ApiResponse is Failure.Error and contains error body, then maps it to a custom error response.
if (this is ApiResponse.Failure.Error) {
val errorBody = (payload as? Response)?.body?.string()
if (errorBody != null) {
val errorMessage: ErrorMessage = Json.decodeFromString(errorBody)
when (errorMessage.code) {
10000 -> LimitedRequest
10001 -> WrongArgument
}
}
}
this
}
```
Then you can handle the errors based on your custom message in other layers:
```kotlin
val apiResponse = repository.fetchMovieList()
apiResponse.onError {
when (this) {
LimitedRequest -> // update your UI
WrongArgument -> // update your UI
}
}
```
You might not want to use the `flatMap` extension for all API requests. If you aim to standardize custom error types across all API requests, you can explore the [Global Failure Mapper](https://skydoves.github.io/sandwich/mapper/#global-failure-mapper).
#### Creation of ApiResponse
Sandwich provides convenient ways to create an `ApiResponse` using functions such as `ApiResponse.of` or `apiResponseOf`, as shown below:
```kotlin
val apiResponse = ApiResponse.of { service.request() }
val apiResponse = apiResponseOf { service.request() }
```
If you need to run suspend functions inside the lambda, you can use `ApiResponse.suspendOf` or `suspendApiResponseOf` instead:
```kotlin
val apiResponse = ApiResponse.suspendOf { service.request() }
val apiResponse = suspendApiResponseOf { service.request() }
```
> **Note**: If you intend to utilize the global operator or global ApiResponse mapper in Sandwich, you should create an `ApiResponse` using the `ApiResponse.of` or `ApiResponse.suspendOf` method to ensure the application of these global functions. If you're using `ApiResponseFailureSuspendMapper` or `ApiResponseSuspendOperator` (common with Ktor/Ktorfit), use `ApiResponse.suspendOf` to ensure suspend mappers and operators are properly awaited.
#### ApiResponse Extensions
You can effectively handle `ApiResponse` using the following extensions:
- **onSuccess**: Executes when the `ApiResponse` is of type `ApiResponse.Success`. Within this scope, you can directly access the body data.
- **onError**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Error`. You can access `messageOrNull` and `payload` here.
- **onException**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Exception`. You can access `messageOrNull` and `exception` here.
- **onFailure**: Executes when the `ApiResponse` is either `ApiResponse.Failure.Error` or `ApiResponse.Failure.Exception`. You can access `messageOrNull` here.
Each scope operates according to its corresponding `ApiResponse` type:
```kotlin
val response = disneyService.fetchDisneyPosterList()
response.onSuccess {
// this scope will be executed if the request successful.
// handle the success case
}.onError {
// this scope will be executed when the request failed with errors.
// handle the error case
}.onException {
// this scope will be executed when the request failed with exceptions.
// handle the exception case
}
```
If you don't want to specify each failure case, you can simplify it by using the `onFailure` extension:
```kotlin
val response = disneyService.fetchDisneyPosterList()
response.onSuccess {
// this scope will be executed if the request successful.
// handle the success case
}.onFailure {
}
```
#### ApiResponse Extensions With Coroutines
With the `ApiResponse` type, you can leverage [Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) extensions to handle responses seamlessly within coroutine scopes. These extensions provide a convenient way to process different response types. Here's how you can use them:
- **suspendOnSuccess**: This extension runs if the `ApiResponse` is of type `ApiResponse.Success`. You can access the body data directly within this scope.
- **suspendOnError**: This extension is executed if the `ApiResponse` is of type `ApiResponse.Failure.Error`. You can access the error message and the error body in this scope.
- **suspendOnException**: If the `ApiResponse` is of type `ApiResponse.Failure.Exception`, this extension is triggered. You can access the exception message in this scope.
- **suspendOnFailure**: This extension is executed if the `ApiResponse` is either `ApiResponse.Failure.Error` or `ApiResponse.Failure.Exception`. You can access the error message in this scope.
Each extension scope operates based on the corresponding `ApiResponse` type. By utilizing these extensions, you can handle responses effectively within different coroutine contexts.
```kotlin
flow {
val response = disneyService.fetchDisneyPosterList()
response.suspendOnSuccess {
posterDao.insertPosterList(data) // insertPosterList(data) is a suspend function.
emit(data)
}.suspendOnError {
// handles error cases
}.suspendOnException {
// handles exceptional cases
}
}.flowOn(Dispatchers.IO)
```
#### Flow
Sandwich offers some useful extensions to transform your `ApiResponse` into a [Flow](https://kotlinlang.org/docs/flow.html) by using the `toFlow` extension:
```kotlin
val flow = disneyService.fetchDisneyPosterList()
.onError {
// handles error cases when the API request gets an error response.
}.onException {
// handles exceptional cases when the API request gets an exception response.
}.toFlow() // returns a coroutines flow
.flowOn(Dispatchers.IO)
```
If you want to transform the original data and work with a `Flow` containing the transformed data, you can do so as shown in the examples below:
```kotlin
val response = pokedexClient.fetchPokemonList(page = page)
response.toFlow { pokemons ->
pokemons.forEach { pokemon -> pokemon.page = page }
pokemonDao.insertPokemonList(pokemons)
pokemonDao.getAllPokemonList(page)
}.flowOn(Dispatchers.IO)
```
#### Functional Extensions
Sandwich provides a variety of functional extensions for transforming and composing `ApiResponse`:
- **Recovery**: `recover`, `recoverWith` - Transform failures back into successes with fallback data
- **Validation**: `validate`, `requireNotNull` - Validate success data and convert it to failure if invalid
- **Filter**: `filter`, `filterNot` - Filter items in list data within a successful response
- **Zip/Combine**: `zip`, `zip3` - Combine multiple `ApiResponse` instances into one
- **Peek/Tap**: `peek`, `peekSuccess`, `peekFailure`, `peekError`, `peekException` - Observe responses without modifying them
```kotlin
val response = disneyService.fetchDisneyPosterList()
.validate({ it.isNotEmpty() }) { "List cannot be empty" } // Validate data
.filter { poster -> poster.isActive } // Filter list items
.recover(emptyList()) // Recover with fallback
.peekSuccess { posters -> analytics.track(posters.size) } // Side effects
```
All extensions have corresponding `suspend` variants (e.g., `suspendRecover`, `suspendValidate`) for coroutine support. For comprehensive details, refer to the [ApiResponse documentation](https://skydoves.github.io/sandwich/apiresponse/).
### Retrieving
Sandwich provides effortless methods to directly extract the encapsulated body data from the `ApiResponse`. You can take advantage of the following functionalities:
#### getOrNull
Returns the encapsulated data if this instance represents `ApiResponse.Success` or returns null if this is failed.
```kotlin
val data: List