Skip to content

Commit 99ebb48

Browse files
authored
Merge pull request #582 from skydoves/release/2.2.0
Prepare for release 2.2.0
2 parents 57ce4c3 + 6c97edc commit 99ebb48

4 files changed

Lines changed: 251 additions & 15 deletions

File tree

README.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ Add the dependency below into your **module**'s `build.gradle` file:
2828

2929
```gradle
3030
dependencies {
31-
implementation("com.github.skydoves:sandwich:2.1.3")
32-
implementation("com.github.skydoves:sandwich-retrofit:2.1.3") // For Retrofit (Android)
31+
implementation("com.github.skydoves:sandwich:2.2.0")
32+
implementation("com.github.skydoves:sandwich-retrofit:2.2.0") // For Retrofit (Android)
3333
}
3434
```
3535

@@ -182,12 +182,12 @@ val apiResponse = suspendApiResponseOf { service.request() }
182182
183183
#### ApiResponse Extensions
184184

185-
You can effectively handling `ApiResponse` using the following extensions:
185+
You can effectively handle `ApiResponse` using the following extensions:
186186

187187
- **onSuccess**: Executes when the `ApiResponse` is of type `ApiResponse.Success`. Within this scope, you can directly access the body data.
188-
- **onError**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Error`. Here, you can access the `messareOrNull` and `payload` here.
189-
- **onException**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Exception`. You can access the `messareOrNull` and `exception` here.
190-
- **onFailure**: Executes when the `ApiResponse` is either `ApiResponse.Failure.Error` or `ApiResponse.Failure.Exception`. You can access the `messareOrNull` here.
188+
- **onError**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Error`. You can access `messageOrNull` and `payload` here.
189+
- **onException**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Exception`. You can access `messageOrNull` and `exception` here.
190+
- **onFailure**: Executes when the `ApiResponse` is either `ApiResponse.Failure.Error` or `ApiResponse.Failure.Exception`. You can access `messageOrNull` here.
191191

192192
Each scope operates according to its corresponding `ApiResponse` type:
193193

@@ -270,6 +270,26 @@ response.toFlow { pokemons ->
270270
}.flowOn(Dispatchers.IO)
271271
```
272272

273+
#### Functional Extensions
274+
275+
Sandwich provides a variety of functional extensions for transforming and composing `ApiResponse`:
276+
277+
- **Recovery**: `recover`, `recoverWith` - Transform failures back into successes with fallback data
278+
- **Validation**: `validate`, `requireNotNull` - Validate success data and convert it to failure if invalid
279+
- **Filter**: `filter`, `filterNot` - Filter items in list data within a successful response
280+
- **Zip/Combine**: `zip`, `zip3` - Combine multiple `ApiResponse` instances into one
281+
- **Peek/Tap**: `peek`, `peekSuccess`, `peekFailure`, `peekError`, `peekException` - Observe responses without modifying them
282+
283+
```kotlin
284+
val response = disneyService.fetchDisneyPosterList()
285+
.validate({ it.isNotEmpty() }) { "List cannot be empty" } // Validate data
286+
.filter { poster -> poster.isActive } // Filter list items
287+
.recover(emptyList()) // Recover with fallback
288+
.peekSuccess { posters -> analytics.track(posters.size) } // Side effects
289+
```
290+
291+
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/).
292+
273293
### Retrieving
274294

275295
Sandwich provides effortless methods to directly extract the encapsulated body data from the `ApiResponse`. You can take advantage of the following functionalities:
@@ -540,7 +560,7 @@ interface NetworkEntryPoint {
540560

541561
##### 2. Provide Global Operator Dependency
542562

543-
Next, provide your global operator with Hilt like the exambple below:
563+
Next, provide your global operator with Hilt like the example below:
544564

545565
```kotlin
546566
@Module

buildSrc/src/main/kotlin/com/github/skydoves/sandwich/Configuration.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ object Configuration {
2222
const val minSdk = 21
2323
const val minSdkDemo = 21
2424
const val majorVersion = 2
25-
const val minorVersion = 1
26-
const val patchVersion = 3
25+
const val minorVersion = 2
26+
const val patchVersion = 0
2727
const val versionName = "$majorVersion.$minorVersion.$patchVersion"
28-
const val versionCode = 46
28+
const val versionCode = 47
2929
const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT"
3030
const val artifactGroup = "com.github.skydoves"
3131
}

docs/apiresponse.md

Lines changed: 220 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ val apiResponse = suspendApiResponseOf { service.request() }
111111

112112
## ApiResponse Extensions
113113

114-
You can effectively handling `ApiResponse` using the following extensions:
114+
You can effectively handle `ApiResponse` using the following extensions:
115115

116116
- **onSuccess**: Executes when the `ApiResponse` is of type `ApiResponse.Success`. Within this scope, you can directly access the body data.
117-
- **onError**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Error`. Here, you can access the `messareOrNull` and `payload` here.
118-
- **onException**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Exception`. You can access the `messareOrNull` and `exception` here.
119-
- **onFailure**: Executes when the `ApiResponse` is either `ApiResponse.Failure.Error` or `ApiResponse.Failure.Exception`. You can access the `messareOrNull` here.
117+
- **onError**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Error`. You can access `messageOrNull` and `payload` here.
118+
- **onException**: Executes when the `ApiResponse` is of type `ApiResponse.Failure.Exception`. You can access `messageOrNull` and `exception` here.
119+
- **onFailure**: Executes when the `ApiResponse` is either `ApiResponse.Failure.Error` or `ApiResponse.Failure.Exception`. You can access `messageOrNull` here.
120120

121121
Each scope operates according to its corresponding `ApiResponse` type:
122122

@@ -197,4 +197,220 @@ response.toFlow { pokemons ->
197197
pokemonDao.insertPokemonList(pokemons)
198198
pokemonDao.getAllPokemonList(page)
199199
}.flowOn(Dispatchers.IO)
200+
```
201+
202+
## Recovery
203+
204+
Sandwich provides recovery extensions to transform a failed `ApiResponse` back into a successful one with fallback data.
205+
206+
### recover
207+
208+
Returns an `ApiResponse.Success` with the fallback value if the response is a failure, otherwise returns the original success:
209+
210+
```kotlin
211+
val response = disneyService.fetchDisneyPosterList()
212+
.recover(emptyList()) // Returns empty list if the request fails
213+
214+
// With a lambda for lazy evaluation
215+
val response = disneyService.fetchDisneyPosterList()
216+
.recover { cachedPosters }
217+
```
218+
219+
### recoverWith
220+
221+
Recovers the failure by executing an alternative `ApiResponse`:
222+
223+
```kotlin
224+
val response = disneyService.fetchDisneyPosterList()
225+
.recoverWith { failure ->
226+
// Try an alternative data source on failure
227+
localDatabase.fetchCachedPosters()
228+
}
229+
```
230+
231+
For coroutines, use `suspendRecover` and `suspendRecoverWith`:
232+
233+
```kotlin
234+
val response = disneyService.fetchDisneyPosterList()
235+
.suspendRecoverWith { failure ->
236+
backupService.fetchPosters() // suspend function
237+
}
238+
```
239+
240+
## Validation
241+
242+
Validation extensions allow you to validate success data and convert it to a failure if the validation fails.
243+
244+
### validate
245+
246+
Validates the success data with a predicate. If the predicate returns false, the response is converted to `ApiResponse.Failure.Error`:
247+
248+
```kotlin
249+
val response = disneyService.fetchDisneyPosterList()
250+
.validate(
251+
predicate = { it.isNotEmpty() },
252+
errorMessage = { "Poster list cannot be empty" }
253+
)
254+
```
255+
256+
### requireNotNull
257+
258+
Requires a non-null value from the success data. If the selected value is null, the response is converted to `ApiResponse.Failure.Error`:
259+
260+
```kotlin
261+
val response = userService.fetchUser()
262+
.requireNotNull(
263+
selector = { it.profileImage },
264+
errorMessage = { "Profile image is required" }
265+
)
266+
```
267+
268+
For coroutines, use `suspendValidate` and `suspendRequireNotNull`:
269+
270+
```kotlin
271+
val response = userService.fetchUser()
272+
.suspendValidate { user ->
273+
userValidator.isValid(user) // suspend function
274+
}
275+
```
276+
277+
## Filter
278+
279+
Filter extensions allow you to filter items in list data within an `ApiResponse`.
280+
281+
### filter
282+
283+
Filters the items in the success data list, keeping only items that match the predicate:
284+
285+
```kotlin
286+
val response = disneyService.fetchDisneyPosterList()
287+
.filter { poster -> poster.isActive }
288+
```
289+
290+
### filterNot
291+
292+
Filters the items in the success data list, excluding items that match the predicate:
293+
294+
```kotlin
295+
val response = disneyService.fetchDisneyPosterList()
296+
.filterNot { poster -> poster.isDeprecated }
297+
```
298+
299+
For coroutines, use `suspendFilter` and `suspendFilterNot`:
300+
301+
```kotlin
302+
val response = disneyService.fetchDisneyPosterList()
303+
.suspendFilter { poster ->
304+
posterValidator.isValid(poster) // suspend function
305+
}
306+
```
307+
308+
## Zip / Combine
309+
310+
Zip extensions allow you to combine multiple `ApiResponse` instances into a single response.
311+
312+
### zip
313+
314+
Combines two `ApiResponse` instances. If both are successful, the transform function is applied. If either is a failure, the first failure is returned:
315+
316+
```kotlin
317+
val usersResponse = userService.fetchUsers()
318+
val postersResponse = disneyService.fetchPosters()
319+
320+
val combined = usersResponse.zip(postersResponse) { users, posters ->
321+
HomeData(users = users, posters = posters)
322+
}
323+
324+
// Or combine into a Pair
325+
val paired = usersResponse.zip(postersResponse) // Returns ApiResponse<Pair<Users, Posters>>
326+
```
327+
328+
### zip3
329+
330+
Combines three `ApiResponse` instances:
331+
332+
```kotlin
333+
val response1 = service.fetchUsers()
334+
val response2 = service.fetchPosters()
335+
val response3 = service.fetchSettings()
336+
337+
val combined = response1.zip3(response2, response3) { users, posters, settings ->
338+
AppData(users = users, posters = posters, settings = settings)
339+
}
340+
```
341+
342+
For coroutines, use `suspendZip` and `suspendZip3`:
343+
344+
```kotlin
345+
val combined = response1.suspendZip(response2) { data1, data2 ->
346+
processData(data1, data2) // suspend function
347+
}
348+
```
349+
350+
## Peek / Tap
351+
352+
Peek extensions allow you to observe the `ApiResponse` without modifying it. This is useful for logging, analytics, or side effects.
353+
354+
### peek
355+
356+
Performs an action on the `ApiResponse` regardless of its type:
357+
358+
```kotlin
359+
val response = disneyService.fetchDisneyPosterList()
360+
.peek { response ->
361+
logger.log("Response received: $response")
362+
}
363+
```
364+
365+
### peekSuccess
366+
367+
Performs an action only if the response is successful:
368+
369+
```kotlin
370+
val response = disneyService.fetchDisneyPosterList()
371+
.peekSuccess { posters ->
372+
analytics.trackPostersLoaded(posters.size)
373+
}
374+
```
375+
376+
### peekFailure
377+
378+
Performs an action only if the response is a failure (either error or exception):
379+
380+
```kotlin
381+
val response = disneyService.fetchDisneyPosterList()
382+
.peekFailure { failure ->
383+
logger.error("Request failed: ${failure.message()}")
384+
}
385+
```
386+
387+
### peekError
388+
389+
Performs an action only if the response is `ApiResponse.Failure.Error`:
390+
391+
```kotlin
392+
val response = disneyService.fetchDisneyPosterList()
393+
.peekError { error ->
394+
errorTracker.trackApiError(error.statusCode)
395+
}
396+
```
397+
398+
### peekException
399+
400+
Performs an action only if the response is `ApiResponse.Failure.Exception`:
401+
402+
```kotlin
403+
val response = disneyService.fetchDisneyPosterList()
404+
.peekException { exception ->
405+
crashReporter.recordException(exception.throwable)
406+
}
407+
```
408+
409+
For coroutines, use `suspendPeek`, `suspendPeekSuccess`, `suspendPeekFailure`, `suspendPeekError`, and `suspendPeekException`:
410+
411+
```kotlin
412+
val response = disneyService.fetchDisneyPosterList()
413+
.suspendPeekSuccess { posters ->
414+
cache.savePosters(posters) // suspend function
415+
}
200416
```

docs/operator.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ interface NetworkEntryPoint {
196196

197197
### 2. Provide Global Operator Dependency
198198

199-
Next, provide your global operator with Hilt like the exambple below:
199+
Next, provide your global operator with Hilt like the example below:
200200

201201
```kotlin
202202
@Module

0 commit comments

Comments
 (0)