Releases: skydoves/sandwich
1.2.3
1.2.2
🥪 A new 1.2.2
stable has been released!
What's Changed
- Fix: receiver and validation of the
onFailure
andsuspendOnFailure
by @skydoves in #42 - Integrate: Kotlin Binary validator plugin by @skydoves in #31
- Update: GitHub Actions workflow by @skydoves in #32, #33, #35
- Update: APG to 7.0.2 internally by @skydoves in #37
Full Changelog: 1.2.1...1.2.2
1.2.1
🥪 Released a new version 1.2.1
! 🥪
What's New?
- Added new extensions
map
for theApiResponse.Success
andApiResponse.Failure.Error
using a lambda receiver. (#26) - Added new functions
suspendCombine
andsuspendRequest
for theDataSourceResponse
. (#27) - Added a
sandwichGlobalContext
for operating thesandwichOperator
when it extends the [ApiResponseSuspendOperator]. (#28) - Updated coroutines to
1.5.0
- Added explicit modifiers based on the strict Kotlin API mode internally.
1.2.0
🥪 Released a new version 1.2.0
! 🥪
You can check the migration codes here Pokedex(#35).
What's New?
Now the data property is a non-nullable type
The data
property in the ApiResponse.Success
is non-nullable from this release.
Previously, the data
property in the ApiResponse
would be null-able if the response has been succeeded but there is an empty-body response regardless of the status code. It will throw NoContentException
if we try to access the data
property for the 204
and 205
cases (succeeded but empty body). Thanks, @jakoss for discussing this (#20).
EmptyBodyInterceptor
If we want to bypass the NoContentException
and handle it as an empty body response, we can use the EmptyBodyInterceptor
. Then we will not get the NoContentException
if we try to access the data
property for the 204 and 205 response code.
OkHttpClient.Builder()
.addInterceptor(EmptyBodyInterceptor())
.build()
create factories
Now we should create the factory classes using the create()
method.
.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory.create())
getOrElse
We can get the data or default value based on the success or failed response.
Returns the encapsulated data if this instance represents ApiResponse.Success
or returns a default value if this is failed.
val data: List<Poster> = disneyService.fetchDisneyPosterList().getOrElse(emptyList())
1.1.0
🎉 Released a new version 1.1.0
! 🎉
What's New?
- Now we can retrieve the encapsulated success data from the
ApiResponse
directly using the below functionalities.
getOrNull
Returns the encapsulated data if this instance represents ApiResponse.Success
or returns null if this is failed.
val data: List<Poster>? = disneyService.fetchDisneyPosterList().getOrNull()
getOrElse
Returns the encapsulated data if this instance represents ApiResponse.Success
or returns a default value if this is failed.
val data: List<Poster>? = disneyService.fetchDisneyPosterList().getOrElse(emptyList())
getOrThrow
Returns the encapsulated data if this instance represents ApiResponse.Success
or throws the encapsulated Throwable
exception if this is failed.
try {
val data: List<Poster>? = disneyService.fetchDisneyPosterList().getOrThrow()
} catch (e: Exception) {
e.printStackTrace()
}
1.0.9
🎉 Released a new version 1.0.9
! 🎉
What's New?
- onSuccess
and suspendOnSuccess
can receive ApiSuccessModelMapper
as a parameter.
If we want to get the transformed data from the start in the lambda, we can pass the mapper as a parameter for the suspendOnSuccess.
.suspendOnSuccess(SuccessPosterMapper) {
val poster = this
}
- onError
and suspendOnError
can receive ApiErrorModelMapper
as a parameter.
// Maps the ApiResponse.Failure.Error to a custom error model using the mapper.
response.onError(ErrorEnvelopeMapper) {
val code = this.code
val message = this.message
}
- Added a new extension toLiveData
and toSuspendLiveData
with a transformer lambda.
If we want to transform the original data and get a LiveData which contains transformed data using successful data if the response is a ApiResponse.Success
.
posterListLiveData = liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emitSource(
disneyService.fetchDisneyPosterList()
.onError {
// handle the error case
}.onException {
// handle the exception case
}.toLiveData {
this.onEach { poster -> poster.date = SystemClock.currentThreadTimeMillis() }
}) // returns an observable LiveData
}
- Added a new extension toFlow
and toSsuspendFlow
with a transformer lambda.
We can get a Flow that emits successful data if the response is an ApiResponse.Success
and the data is not null.
disneyService.fetchDisneyPosterList()
.onError {
// stub error case
}.onException {
// stub exception case
}.toFlow() // returns a coroutines flow
.flowOn(Dispatchers.IO)
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.
val response = pokedexClient.fetchPokemonList(page = page)
response.toFlow { pokemons ->
pokemons.forEach { pokemon -> pokemon.page = page }
pokemonDao.insertPokemonList(pokemons)
pokemonDao.getAllPokemonList(page)
}.flowOn(Dispatchers.IO)
- Added a new transformer onProcedure
and suspendOnProcedure
expressions.
We can pass onSuccess
, onError
, and onException
as arguments.
.suspendOnProcedure(
// handle the case when the API request gets a successful response.
onSuccess = {
Timber.d("$data")
data?.let { emit(it) }
},
// handle the case when the API request gets an error response.
// e.g., internal server error.
onError = {
Timber.d(message())
// handling error based on status code.
when (statusCode) {
StatusCode.InternalServerError -> toastLiveData.postValue("InternalServerError")
StatusCode.BadGateway -> toastLiveData.postValue("BadGateway")
else -> toastLiveData.postValue("$statusCode(${statusCode.code}): ${message()}")
}
// map the ApiResponse.Failure.Error to a customized error model using the mapper.
map(ErrorEnvelopeMapper) {
Timber.d("[Code: $code]: $message")
}
},
// handle the case when the API request gets a exception response.
// e.g., network connection error.
onException = {
Timber.d(message())
toastLiveData.postValue(message())
}
)
1.0.8
🎉 Released a new version 1.0.8
! 🎉
What's New?
- Added
ApiResponseOperator
andApiResponseSuspendOperator
.
Operator
We can delegate the onSuccess
, onError
, onException
using the operator
extension and ApiResponseOperator
. Operators are very useful when 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.
disneyService.fetchDisneyPosterList().operator(
CommonResponseOperator(
success = { success ->
success.data?.let {
posterListLiveData.postValue(it)
}
Timber.d("$success.data")
},
application = getApplication()
)
)
CommonResponseOperator
The CommonResponseOperator
extends ApiResponseOperator
with the onSuccess
, onError
, onException
override methods. They will be executed depending on the type of the ApiResponse
.
/** A common response operator for handling [ApiResponse]s regardless of its type. */
class CommonResponseOperator<T> constructor(
private val success: suspend (ApiResponse.Success<T>) -> Unit,
private val application: Application
) : ApiResponseOperator<T>() {
// handle the case when the API request gets a success response.
override fun onSuccess(apiResponse: ApiResponse.Success<T>) = success(apiResponse)
// handle the case when the API request gets a error response.
// e.g., internal server error.
override fun onError(apiResponse: ApiResponse.Failure.Error<T>) {
apiResponse.run {
Timber.d(message())
// map the ApiResponse.Failure.Error to a customized error model using the mapper.
map(ErrorEnvelopeMapper) {
Timber.d("[Code: $code]: $message")
}
}
}
// handle the case when the API request gets a exception response.
// e.g., network connection error.
override fun onException(apiResponse: ApiResponse.Failure.Exception<T>) {
apiResponse.run {
Timber.d(message())
toast(message())
}
}
}
Operator with coroutines
If we want to operate and delegate a suspending lambda to the operator, we can use the suspendOperator
extension and ApiResponseSuspendOperator
class.
ViewModel
We can use suspending function like emit
in the success
lambda.
flow {
disneyService.fetchDisneyPosterList().suspendOperator(
CommonResponseOperator(
success = { success ->
success.data?.let { emit(it) }
Timber.d("$success.data")
},
application = getApplication()
)
)
}.flowOn(Dispatchers.IO).asLiveData()
CommonResponseOperator
The CommonResponseOperator
extends ApiResponseSuspendOperator
with suspend override methods.
class CommonResponseOperator<T> constructor(
private val success: suspend (ApiResponse.Success<T>) -> Unit,
private val application: Application
) : ApiResponseSuspendOperator<T>() {
// handle the case when the API request gets a success response.
override suspend fun onSuccess(apiResponse: ApiResponse.Success<T>) = success(apiResponse)
// skip //
Global operator
We can operate an operator globally on each ApiResponse
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 globally about the ApiResponse.Failure.Error
and ApiResponse.Failure.Exception
. 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.
class SandwichDemoApp : Application() {
override fun onCreate() {
super.onCreate()
// We will handle only the error and exception cases,
// so we don't need to mind the generic type of the operator.
SandwichInitializer.sandwichOperator = GlobalResponseOperator<Any>(this)
// skipp //
GlobalResponseOperator
The GlobalResponseOperator
can extend any operator (ApiResponseSuspendOperator
or ApiResponseOperator
)
class GlobalResponseOperator<T> constructor(
private val application: Application
) : ApiResponseSuspendOperator<T>() {
// The body is empty, because we will handle the success case manually.
override suspend fun onSuccess(apiResponse: ApiResponse.Success<T>) { }
// handle the case when the API request gets a error response.
// e.g., internal server error.
override suspend fun onError(apiResponse: ApiResponse.Failure.Error<T>) {
withContext(Dispatchers.Main) {
apiResponse.run {
Timber.d(message())
// handling error based on status code.
when (statusCode) {
StatusCode.InternalServerError -> toast("InternalServerError")
StatusCode.BadGateway -> toast("BadGateway")
else -> toast("$statusCode(${statusCode.code}): ${message()}")
}
// map the ApiResponse.Failure.Error to a customized error model using the mapper.
map(ErrorEnvelopeMapper) {
Timber.d("[Code: $code]: $message")
}
}
}
}
// handle the case when the API request gets a exception response.
// e.g., network connection error.
override suspend fun onException(apiResponse: ApiResponse.Failure.Exception<T>) {
withContext(Dispatchers.Main) {
apiResponse.run {
Timber.d(message())
toast(message())
}
}
}
private fun toast(message: String) {
Toast.makeText(application, message, Toast.LENGTH_SHORT).show()
}
}
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
.
flow {
disneyService.fetchDisneyPosterList().
suspendOnSuccess {
data?.let { emit(it) }
}
}.flowOn(Dispatchers.IO).asLiveData()
1.0.7
🎉 Released a new version 1.0.7
! 🎉
What's New?
- Changed non-inline functions to inline classes.
- Removed generating
BuildConfig
class. - Added
ApiSuccessModelMapper
for mapping data of theApiResponse.Success
to the custom model.
We can map theApiResponse.Success
model to our custom model using the mapper extension.
object SuccessPosterMapper : ApiSuccessModelMapper<List<Poster>, Poster?> {
override fun map(apiErrorResponse: ApiResponse.Success<List<Poster>>): Poster? {
return apiErrorResponse.data?.first()
}
}
// Maps the success response data.
val poster: Poster? = map(SuccessPosterMapper)
or
// Maps the success response data using a lambda.
map(SuccessPosterMapper) { poster ->
livedata.post(poster) // we can use the `this` keyword instead.
}
- Added
mapOnSuccess
andmapOnError
extensions for mapping success/error model to the custom model in their scope. - Added
merge
extension forApiResponse
for merging multipleApiResponses
as one ApiResponse depending on the policy.
The below example is merging three ApiResponse as one if every three ApiResponses are successful.
disneyService.fetchDisneyPosterList(page = 0).merge(
disneyService.fetchDisneyPosterList(page = 1),
disneyService.fetchDisneyPosterList(page = 2),
mergePolicy = ApiResponseMergePolicy.PREFERRED_FAILURE
).onSuccess {
// handle response data..
}.onError {
// handle error..
}
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.
1.0.6
🎉 Released a new version 1.0.6
! 🎉
What's New?
- Added a
Disposable
interface anddisposable()
extension for canceling tasks when we want. - Added
DisposableComposite
is that a disposable container that can hold onto multiple other disposables. - Added
joinDisposable
extensions toDataSource
andCall
for creating a disposable and add easily.
Disposable in Call
We can cancel the executing works using a disposable()
extension.
val disposable = call.request { response ->
// skip handling a response //
}.disposable()
// dispose the executing works
disposable.dispose()
And we can use CompositeDisposable
for canceling multiple resources at once.
class MainViewModel constructor(disneyService: DisneyService) : ViewModel() {
private val disposables = CompositeDisposable()
init {
disneyService.fetchDisneyPosterList()
.joinDisposable(disposables) // joins onto [CompositeDisposable] as a disposable.
.request {response ->
// skip handling a response //
}
}
override fun onCleared() {
super.onCleared()
if (!disposables.disposed) {
disposables.clear()
}
}
}
Disposable in DataSource
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.
private val disposable = CompositeDisposable()
init {
disneyService.fetchDisneyPosterList().toResponseDataSource()
// retry fetching data 3 times with 5000L interval when the request gets failure.
.retry(3, 5000L)
// joins onto CompositeDisposable as a disposable and dispose onCleared().
.joinDisposable(disposable)
.request {
// ... //
}
}
override fun onCleared() {
super.onCleared()
if (!disposable.disposed) {
disposable.clear()
}
}