| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| 2.4.0 source code.tar.gz | 2026-07-27 | 450.1 kB | |
| 2.4.0 source code.zip | 2026-07-27 | 649.2 kB | |
| README.md | 2026-07-27 | 5.0 kB | |
| Totals: 3 Items | 1.1 MB | 0 | |
New artifacts
sandwich-bom: a Bill of Materials that aligns the versions of every Sandwich artifact.-
sandwich-ktor-serialization:deserializeErrorBodyandonErrorDeserializefor Ktor, which previously existed only for Retrofit on the JVM.:::kotlin implementation(platform("com.github.skydoves:sandwich-bom:2.4.0"))
implementation("com.github.skydoves:sandwich") implementation("com.github.skydoves:sandwich-retrofit") testImplementation("com.github.skydoves:sandwich-test")
Behaviour changes to read before upgrading
Global configuration now works on Kotlin/Native. SandwichInitializer was annotated with @ThreadLocal, which gave every Native thread its own copy of the object state. An operator registered during startup on the main thread was invisible to a request completing on a background thread, so global operators, failure mappers, successCodeRange and sandwichTimeout silently did nothing on iOS and macOS. They now run. If your app registers a global operator such as a token refresher, it will start firing on those platforms for the first time.
SandwichInitializer is now shared across threads. Write it once during application startup and only read it afterwards. Mutating it while requests are in flight is not synchronized.
Global operators no longer re-run on every transformation. mapSuccess, suspendMapSuccess, mapFailure, suspendMapFailure and merge created their result through ApiResponse.of, which re-entered the global pipeline. A three step mapSuccess chain fired a global operator four times, and a global failure mapper applied twice. Each now runs exactly once per request.
mapSuccess no longer captures CancellationException. It rethrows it, so coroutine cancellation propagates instead of being turned into a failed response.
Envelope support
Many backends answer HTTP 200 while the body encodes a business failure. Implementing ApiEnvelope lets Sandwich classify that as ApiResponse.Failure.Error, and unwrap flattens the payload.
:::kotlin
data class BaseResponse<T>(val code: Int, val message: String, val data: T?) :
ApiEnvelope<T?, String> {
override val isEnvelopeSuccessful: Boolean get() = code == 0
override val envelopeBody: T? get() = data
override val envelopeError: String get() = message
}
suspend fun posters(): ApiResponse<List<Poster>?> = service.fetchPosters().unwrap()
ApiEnvelopeSpec covers models that cannot be modified. A body that does not implement ApiEnvelope is untouched, so this is inert for existing models. Resolves the pattern reported in [#30], [#38], [#85] and [#138].
Flow operators
:::kotlin
fun posters(): Flow<ApiResponse<List<Poster>>> = apiResponseFlow {
service.fetchPosters()
}.flowOn(Dispatchers.IO)
posters()
.onSuccess { posterDao.insert(data) }
.onError { logger.warn(message()) }
.mapSuccess { map(Poster::toUiModel) }
.foldToFlow(
onSuccess = { UiState.Content(it) },
onFailure = { UiState.Error(it) },
)
Also onException, onFailure, mapToDataOrNull and filterSuccessData.
Retry policies
RetryPolicy had no implementations, so everyone wrote their own.
:::kotlin
RetryPolicies.none()
RetryPolicies.fixedDelay(maxAttempts = 3, delayMillis = 1_000)
RetryPolicies.linear(maxAttempts = 3, delayMillis = 1_000, maxDelayMillis = 10_000)
RetryPolicies.exponentialBackoff(maxAttempts = 4, factor = 2.0, jitter = 0.5)
runAndRetry gains a retryOn predicate that receives the failure, so a 400 is no longer retried like a timeout. Both integrations expose the Retry-After header as retryAfterMillis.
Exception classification
Telling a timeout apart from a connectivity failure meant inspecting the raw throwable, which is platform specific.
:::kotlin
SandwichInitializer.sandwichExceptionClassifiers += KtorExceptionClassifier
// or RetrofitExceptionClassifier
response.onException {
when (sandwichException) {
is SandwichTimeoutException -> retryLater()
is SandwichNetworkException -> showOfflineBanner()
else -> report(throwable)
}
}
isTimeout, isNetworkFailure and isSerializationFailure cover the common checks. Nothing is registered by default.
Platforms
Adds watchos, tvos, linuxX64, linuxArm64 and mingwX64. The klib ABI dump goes from 9 targets to 17. sandwich-ktorfit omits macosX64, watchosX64 and tvosX64 because ktorfit-lib-light does not publish those variants.
Build and verification
CI ran no tests at all before this release. A test job now runs the 415 unit tests across JVM, Android, macosArm64 and iosSimulatorArm64. klib ABI validation is enabled, so the Native, JS and Wasm surface is guarded. sandwich-test also could not compile for Native, and now does.
Compatibility
No public signature was removed or changed. Every API addition is additive.