Thanks to visit codestin.com
Credit goes to github.com

Skip to content
forked from skydoves/sandwich

🥪 A lightweight Android network response API for handling data and error response with transformation extensions.

License

smarus/Sandwich

 
 

Repository files navigation

Sandwich


🥪 A lightweight Android network response API for handling data and error response with
transformation extensions using Retrofit.


License API Build Status Javadoc Medium Profile

Download

Download Maven Central Jitpack

🥪 Sandwich has been downloaded in more than 30k Android projects all over the world!

screenshot122228908

Gradle

Add a dependency code to your module's build.gradle file.

dependencies {
    implementation "com.github.skydoves:sandwich:1.0.7"
}

Usecase

You can reference the use cases of this library in the below repositories.

  • Pokedex - 🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture.
  • 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.

Usage

ApiResponse

ApiResponse is an interface for constructing standard responses from the response of the retrofit call. It provides useful extensions for handling data from the successful and error response.
We can get ApiResponse using the scope extension request from the Call. The below example is the basic of getting an ApiResponse from an instance of the Call.

interface DisneyService {
  @GET("/")
  fun fetchDisneyPosterList(): Call<List<Poster>>
}

val disneyService = retrofit.create(DisneyService::class.java)
// Request REST call asynchronously 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
          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()}")
          }
        }
        is ApiResponse.Failure.Exception -> {
          // stub exception case
        }
      }
    }

ApiResponse.Success

A standard API Success response class from OkHttp request calls.
We can get the body data of the response, StatusCode, Headers and etc from the ApiResponse.Success.

val data: List<Poster>? = response.data
val statusCode: StatusCode = response.statusCode
val headers: Headers = response.headers

ApiResponse.Failure.Error

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

ApiResponse.Failure.Exception

An unexpected exception occurs while creating the request or processing the response in the client. e.g., Network connection error.

ApiResponse Extensions

We can handle response cases conveniently using extensions.

onSuccess, onError, onException

We can use these scope functions to the ApiResponse, we can reduce 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
      }
    }

suspendOnSuccess, suspendOnError, suspendOnException

We can use suspension extensions for using suspend functions inside the scope.
In this case, we can use with CoroutinesResponseCallAdapterFactory.

flow {
  val response = disneyService.fetchDisneyPosterList()
  response.suspendOnSuccess {
    emit(data)
  }.suspendOnError {
    // stub error case
  }.suspendOnFailure {
    // stub exception case
  }
}.flowOn()

Mapper

Mapper can be used useful if we want to map the ApiResponse.Success or ApiResponse.Failure.Error to our custom model in our ApiResponse extension scopes.

ApiSuccessModelMapper

We can map the ApiResponse.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.
}

ApiErrorModelMapper

We can map the ApiResponse.Failure.Error model to our custom error model using the mapper extension.

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())
  }
}

// Maps the error response.
response.onError {
  // Maps the ApiResponse.Failure.Error to a custom error model using the mapper.
  map(ErrorEnvelopeMapper) {
     val code = this.code
     val message = this.message
  }
}

ApiResponse with coroutines

We can use the suspend keyword in our service.
Build your retrofit using with CoroutinesResponseCallAdapterFactory call adapter factory.

.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())

And use the suspend keyword in our service functions. The response type must be ApiResponse.

interface DisneyCoroutinesService {

  @GET("DisneyPosters.json")
  suspend fun fetchDisneyPosterList(): ApiResponse<List<Poster>>
}

We can use like this; An example of using toLiveData.

class MainCoroutinesViewModel constructor(disneyService: DisneyCoroutinesService) : ViewModel() {

  val posterListLiveData: LiveData<List<Poster>>

  init {
    posterListLiveData = liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
      emitSource(disneyService.fetchDisneyPosterList()
        .onSuccess {
          // stub success case
          livedata.post(response.data)
        }.onError {
          // stub error case
        }.onException {
          // stub exception case
        }.toLiveData()) // returns an observable LiveData
    }
  }
}

Disposable

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()
    }
  }
}

Merge

We can merge multiple ApiResponses 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.

ResponseDataSource

ResponseDataSource is an implementation of the DataSource interface.

  • Asynchronously send requests.
  • A temporarily 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.
  • Disposable of executing works.

Combine

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

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

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)

ObserveResponse

Observes every response data ApiResponse from the API call request.

dataSource.observeResponse {
   Timber.d("observeResponse: $it")
}

RetainPolicy

We can limit the policy for retaining data on the temporarily internal storage.
The default policy is no retaining any fetched data from the network, but we can set the policy using dataRetainPolicy method.

// Retain fetched data on the memory storage temporarily.
// If request again, returns the retained data instead of re-fetching from the network.
dataSource.dataRetainPolicy(DataRetainPolicy.RETAIN)

Invalidate

Invalidate a cached (holding) data and re-fetching the API request.

dataSource.invalidate()

Concat

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.

asLiveData

we can observe fetched data via DataSource as a LiveData.

val posterListLiveData: LiveData<List<Poster>>

init {
    posterListLiveData = disneyService.fetchDisneyPosterList().toResponseDataSource()
      .retry(3, 5000L)
      .dataRetainPolicy(DataRetainPolicy.RETAIN)
      .request {
        // ... //
      }.asLiveData()
}

Disposable

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()
    }
  }

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>()
  private val disposable = CompositeDisposable()

  /** 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)
      // joins onto CompositeDisposable as a disposable and dispose onCleared().
      .joinDisposable(disposable)
      // 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()
  }

  override fun onCleared() {
    super.onCleared()
    if (!disposable.disposed) {
      disposable.clear()
    }
  }
}

DataSourceCallAdapterFactory

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 an example 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()

CoroutinesDataSourceCallAdapterFactory

We can get the DataSource directly from the Retrofit service using with suspend.

Retrofit.Builder()
    ...
    .addCallAdapterFactory(CoroutinesDataSourceCallAdapterFactory())
    .build()

interface DisneyService {
  @GET("DisneyPosters.json")
  fun fetchDisneyPosterList(): DataSource<List<Poster>>
}

Here is an exmaple of the DataSource in the MainViewModel.

class MainCoroutinesViewModel constructor(disneyService: DisneyCoroutinesService) : ViewModel() {

  val posterListLiveData: LiveData<List<Poster>>

  init {
    Timber.d("initialized MainViewModel.")

    posterListLiveData = liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
      emitSource(disneyService.fetchDisneyPosterList().toResponseDataSource()
        // retry fetching data 3 times with 5000L interval when the request gets failure.
        .retry(3, 5000L)
        // a retain policy for retaining data on the internal storage
        .dataRetainPolicy(DataRetainPolicy.RETAIN)
        // request API network call asynchronously.
        .request {
          // handle the case when the API request gets a success response.
          onSuccess {
            Timber.d("$data")
          }.onError { // handle the case when the API request gets a error response.
              Timber.d(message())
            }.onException {  // handle the case when the API request gets a exception response.
              Timber.d(message())
            }
        }.asLiveData())
    }
  }
}

toResponseDataSource

We can change DataSource to ResponseDataSource after getting instance from network call using the below method.

private val dataSource: ResponseDataSource<List<Poster>>

  init {
    dataSource = disneyService.fetchDisneyPosterList().toResponseDataSource()

    //...
  }

Find this library useful? ❤️

Support it by joining stargazers for this repository. ⭐
And follow me for my next creations! 🤩

License

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.

About

🥪 A lightweight Android network response API for handling data and error response with transformation extensions.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Kotlin 100.0%