🥪 A lightweight Android network response API for handling data and error response with
transformation extensions using Retrofit.
Add a dependency code to your module's build.gradle
file.
dependencies {
implementation "com.github.skydoves:sandwich:1.0.1"
}
You can reference the use cases of this library in the below repositories.
- 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.
ApiResponse
is an interface of the retrofit response for handling data and error response with useful extensions.
We can get ApiResponse
using the scope extension request
from the Call
.
interface DisneyService {
@GET("/")
fun fetchDisneyPosterList(): Call<List<Poster>>
}
val disneyService = retrofit.create(DisneyService::class.java)
// Asynchronously request REST call and get an ApiResponse model.
disneyService.fetchDisneyPosterList().request { response ->
when (response) {
is ApiResponse.Success -> {
// stub success case
livedata.post(response.data)
}
is ApiResponse.Failure.Error -> {
// stub error case
}
is ApiResponse.Failure.Exception -> {
// stub exception case
}
}
}
API success response class from retrofit.
We can get the response body data, StatusCode
, Headers
and etc from the ApiResponse.Success
.
val data: List<Poster>? = response.data
val statusCode: StatusCode = response.statusCode
val headers: Headers = response.headers
API format does not match or applications need to handle errors. e.g. Internal server error.
val errorBody: ResponseBody? = response.errorBody
val statusCode: StatusCode = response.statusCode
val headers: Headers = response.headers
Gets called when an unexpected exception occurs while creating the request or processing
the response in client. e.g. Network connection error.
We can handle response cases more handy using extensions.
We can use these scope functions to the ApiResponse
, it reduces the usage of the if/when clause.
disneyService.fetchDisneyPosterList().request { response ->
response.onSuccess {
// stub success case
livedata.post(response.data)
}.onError {
// stub error case
}.onException {
// stub exception case
}
}
We can map ApiResponse.Failure.Error
model to our customized error model using the mapper.
data class ErrorEnvelope(
val code: Int,
val message: String
)
object ErrorEnvelopeMapper : ApiErrorModelMapper<ErrorEnvelope> {
override fun map(apiErrorResponse: ApiResponse.Failure.Error<*>): ErrorEnvelope {
return ErrorEnvelope(apiErrorResponse.statusCode.code, apiErrorResponse.message())
}
}
// hadling the error response case.
response.onError {
// map the ApiResponse.Failure.Error to a customized error model using the mapper.
map(ErrorEnvelopeMapper) {
val code = this.code
val message = this.message
}
}
ResponseDataSource is an implementation of the DataSource
interface.
- Asynchronously send requests.
- A 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.
Combine a Call
and lambda scope for constructing the DataSource.
val disneyService = retrofit.create(DisneyService::class.java)
val dataSource = ResponseDataSource<List<Poster>>()
dataSource.combine(disneyService.fetchDisneyPosterList()) { response ->
// stubs
}
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()
.
dataSource.request()
Retry fetching data (re-request) if your request got failure.
// retry fetching data 3 times with 5000 milli-seconds time interval when the request gets failure.
dataSource.retry(3, 5000L)
Observes every response data ApiResponse
from the API call request.
dataSource.observeResponse {
Timber.d("observeResponse: $it")
}
Invalidate a cached (holding) data and re-fetching the API request.
dataSource.invalidate()
Concat an another DataSource
and request API call sequentially if the API call getting successful.
val dataSource2 = ResponseDataSource<List<PosterDetails>>()
dataSource2.retry(3, 5000L).combine(disneyService.fetchDetails()) {
// stubs handling dataSource2 response
}
dataSource1
.request() // request() must be called before concat.
.concat(dataSource2) // request dataSource2's API call after the success of the dataSource1.
.concat(dataSource3) // request dataSource3's API call after the success of the dataSource2.
Here is the example of the ResponseDataSource
in the MainViewModel
.
class MainViewModel constructor(
private val disneyService: DisneyService
) : ViewModel() {
// request API call Asynchronously and holding successful response data.
private val dataSource = ResponseDataSource<List<Poster>>()
val posterListLiveData = MutableLiveData<List<Poster>>()
val toastLiveData = MutableLiveData<String>()
/** fetch poster list data from the network. */
fun fetchDisneyPosters() {
dataSource
// retry fetching data 3 times with 5000 time interval when the request gets failure.
.retry(3, 5000L)
// combine network service to the data source.
.combine(disneyService.fetchDisneyPosterList()) { response ->
// handle the case when the API request gets a success response.
response.onSuccess {
Timber.d("$data")
posterListLiveData.postValue(data)
}
// handle the case when the API request gets a 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(this.toString())
}
}
// handle the case when the API request gets a exception response.
// e.g. network connection error.
.onException {
Timber.d(message())
toastLiveData.postValue(message())
}
}
// observe every API request responses.
.observeResponse {
Timber.d("observeResponse: $it")
}
// request API network call asynchronously.
// if the request is successful, the data source will hold the success data.
// in the next request after success, returns the cached API response.
// if you want to fetch a new response data, use invalidate().
.request()
}
}
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
.
Retrofit.Builder()
...
.addCallAdapterFactory(DataSourceCallAdapterFactory())
.build()
interface DisneyService {
@GET("DisneyPosters.json")
fun fetchDisneyPosterList(): DataSource<List<Poster>>
}
Here is the exmaple of the DataSource
in the MainViewModel.
class MainViewModel constructor(disneyService: DisneyService) : ViewModel() {
// request API call Asynchronously and holding successful response data.
private val dataSource: DataSource<List<Poster>>
init {
Timber.d("initialized MainViewModel.")
dataSource = disneyService.fetchDisneyPosterList()
// retry fetching data 3 times with 5000L interval when the request gets failure.
.retry(3, 5000L)
.observeResponse(object : ResponseObserver<List<Poster>> {
override fun observe(response: ApiResponse<List<Poster>>) {
// handle the case when the API request gets a success response.
response.onSuccess {
Timber.d("$data")
posterListLiveData.postValue(data)
}
}
})
.request() // must call request()
Support it by joining stargazers for this repository. ⭐
And follow me for my next creations! 🤩
Copyright 2020 skydoves (Jaewoong Eum)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.