• Stars
    star
    1,403
  • Rank 32,212 (Top 0.7 %)
  • Language
    Kotlin
  • License
    Apache License 2.0
  • Created almost 4 years ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🥪 Sandwich is an adaptable and lightweight sealed API library designed for handling API responses and exceptions in Kotlin for Retrofit, Ktor, and Kotlin Multiplatform.

sandwich

License API Build Status
Google Medium Profile Profile Dokka

Why Sandwich?

Sandwich was invented to construct standardized interfaces from the Retrofit network response on Kotlin. This library allows you to handle the body data, errors, and exceptional cases more concisely with functional operators in multi-layer architecture. You don't need to design and build wrapper classes such as Resource or Result, and it helps you to focus on your business codes. Sandwich supports global responses handling, Mapper, Operator, and great compatibilities, such as toFlow. You can also utilize Sandwich with coroutines and flow.

Download

Maven Central

🥪 Sandwich has been downloaded in more than 200k Android projects all over the globe!

Gradle

Add the dependency below into your module's build.gradle file:

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

SNAPSHOT

Sandwich

See how to import the snapshot

Including the SNAPSHOT

Snapshots of the current development version of Sandwich are available, which track the latest versions.

To import snapshot versions on your project, add the code snippet below on your gradle file:

repositories {
   maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}

Next, add the dependency below to your module's build.gradle file:

dependencies {
    implementation "com.github.skydoves:sandwich:1.3.8-SNAPSHOT"
}

R8 / ProGuard

The specific rules are already bundled into the JAR which can be interpreted by R8 automatically.

Use Cases

You can also check out nice use cases of this library in the repositories below:

  • Pokedex: 🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture.
  • ChatGPT Android: 📲 ChatGPT Android demonstrates OpenAI's ChatGPT on Android with Stream Chat SDK for Compose.
  • 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.
  • Neko: Free, open source, unofficial MangaDex reader for Android.
  • TheMovies2: 🎬 A demo project using The Movie DB based on Kotlin MVVM architecture and material design & animations.

Table of contents

Usage

ApiResponse

ApiResponse is an interface to construct standardized responses from Retrofit calls. It provides useful extensions for handling network payload such as body data and exceptional cases. You can get ApiResponse with the request scope extension from the Call. The example below shows how to get an ApiResponse from an instance of the Call.

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

val disneyService = retrofit.create(DisneyService::class.java)
// fetches a model list from the network and getting [ApiResponse] asynchronously.
disneyService.fetchDisneyPosterList().request { response ->
      when (response) {
        // handles the success case when the API request gets a successful response.
        is ApiResponse.Success -> {
          posterDao.insertPosterList(response.data)
          livedata.post(response.data)
        }
        // handles error cases when the API request gets an error response.
        // e.g., internal server error.
        is ApiResponse.Failure.Error -> {
          // stub error case
          Timber.d(message())

          // handles error cases depending on the status code.
          when (statusCode) {
            StatusCode.InternalServerError -> toastLiveData.postValue("InternalServerError")
            StatusCode.BadGateway -> toastLiveData.postValue("BadGateway")
            else -> toastLiveData.postValue("$statusCode(${statusCode.code}): ${message()}")
          }
        }
        // handles exceptional cases when the API request gets an exception response.
        // e.g., network connection error, timeout.
        is ApiResponse.Failure.Exception -> {
          // stub exception case
        }
      }
    }

ApiResponse has three types; Success, Failure.Error, and Failure.Exception.

ApiResponse.Success

This represents the network request has been successful. You can get the body data of the response, and additional information such as StatusCode, Headers, and more from the ApiResponse.Success.

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

ApiResponse.Failure.Error

This represents the network request has been failed with bad requests or internal server errors. You can get an error message and additional information such as StatusCode, Headers, and more from the ApiResponse.Failure.Error.

val message: String = response.message()
val errorBody: ResponseBody? = response.errorBody
val statusCode: StatusCode = response.statusCode
val headers: Headers = response.headers

ApiResponse.Failure.Exception

This represents the network request has been failed when unexpected exceptions occur while creating requests or processing a response from the client-side such as network connection failed. You can get an exception message from the ApiResponse.Failure.Exception.

ApiResponse Extensions

You can handle the ApiResponse with the extensions below:

  • onSuccess: Executes if the ApiResponse is ApiResponse.Success. You can access body data directly in this scope.
  • onError: Executes if the ApiResponse is ApiResponse.Failure.Error. You can access message() and errorBody in this scope.
  • onException: Executes if the ApiResponse is ApiResponse.Failure.Exception. You can access message() in this scope.
  • onFailure: Executes if the ApiResponse is ApiResponse.Failure.Error or ApiResponse.Failure.Exception. You can access message() in this scope.

Each scope runs depending on its dedicated ApiResponse type:

disneyService.fetchDisneyPosterList().request { response ->
    response.onSuccess {
     // this scope will be executed if the request successful.
     // handle the success case
    }.onError {
      // this scope will be executed when the request failed with errors.
      // handle the error case
    }.onException {
     // this scope will be executed when the request failed with exceptions.
     // handle the exception case
    }
  }

ApiResponse for Coroutines

You can use the suspend keyword in your Retrofit services with ApiResponse<*> as a response type. First, build your Retrofit with the ApiResponseCallAdapterFactory call adapter factory:

.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())

Next, you should define the service interface with the suspend keyword and ApiResponse<*> as a response type. So eventually you will get the ApiResponse from the Retrofit service call like the examples below:

interface DisneyCoroutinesService {

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

Finally, you can execute the defined service like the examples below:

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

  val posterListLiveData: MutableLiveData<List<Poster>>

  init {
     val response = disneyService.fetchDisneyPosterList()
     response.onSuccess {
       // handles the success case when the API request gets a successful response.
       posterListLiveData.post(data)
      }.onError {
       // handles error cases when the API request gets an error response.
      }.onException {
       // handles exceptional cases when the API request gets an exception response.
      }
   }
}

Note: If you're interested in injecting your own coroutine scope and unit testing with a test coroutine scope, check out the Injecting a custom CoroutineScope and Unit Tests.

ApiResponse Extensions for Coroutines

You can handle the ApiResponse with coroutines extensions below, which allows you can launch your suspend functions on the scopes.

  • suspendOnSuccess: Executes if the ApiResponse is ApiResponse.Success. You can access body data directly in this scope.
  • suspendOnError: Executes if the ApiResponse is ApiResponse.Failure.Error. You can access message() and errorBody in this scope.
  • suspendOnException: Executes if the ApiResponse is ApiResponse.Failure.Exception. You can access message() in this scope.
  • suspendOnFailure: Executes if the ApiResponse is ApiResponse.Failure.Error or ApiResponse.Failure.Exception. You can access message() in this scope.

Each scope runs depending on the its dedicated ApiResponse type:

flow {
  val response = disneyService.fetchDisneyPosterList()
  response.suspendOnSuccess {
    posterDao.insertPosterList(data) // insertPosterList(data) is a suspend function.
    emit(data)
  }.suspendOnError {
    // handles error cases
  }.suspendOnException {
    // handles exceptional cases
  }
}.flowOn(Dispatchers.IO)

Retrieve success data

If you want to retrieve the encapsulated body data from the ApiResponse directly, you can use the functionalities below.

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

Mapper

Mapper is useful when you want to transform the ApiResponse.Success or ApiResponse.Failure.Error to your custom model in ApiResponse extension scopes.

ApiSuccessModelMapper

You can map the ApiResponse.Success model to your custom model with the SuccessPosterMapper<T, R> and map extension like the examples below:

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)

You can use the map extension with a lambda like the examples below:

// Maps the success response data using a lambda.
map(SuccessPosterMapper) { poster ->
  emit(poster) // you can use the `this` keyword instead of the poster.
}

If you want to receive transformed body data in the scope, you can use the mapper as a parameter with the onSuccess or suspendOnSuccess extensions like the examples below:

.suspendOnSuccess(SuccessPosterMapper) {
    val poster = this
}

ApiErrorModelMapper

You can map the ApiResponse.Failure.Error model to your custom error model using the ApiErrorModelMapper<T> and map extension as the examples bleow:

// Create your custom error model.
data class ErrorEnvelope(
  val code: Int,
  val message: String
)

// An error response mapper.
// Create an instance of your custom model using the `ApiResponse.Failure.Error` in the `map`.
object ErrorEnvelopeMapper : ApiErrorModelMapper<ErrorEnvelope> {

  override fun map(apiErrorResponse: ApiResponse.Failure.Error<*>): ErrorEnvelope {
    return ErrorEnvelope(apiErrorResponse.statusCode.code, apiErrorResponse.message())
  }
}

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

If you want to receive transformed data from in the scope, you can use the mapper as a parameter with the onError or suspendOnError extensions as the examples below:

.suspendOnError(ErrorEnvelopeMapper) {
    val message = this.message
}

Run and Retry

You can run and retry network requests by using RetryPolicy interface and runAndRetry extension like the code below:

val retryPolicy = object : RetryPolicy {
  override fun shouldRetry(attempt: Int, message: String?): Boolean = attempt <= 3

  override fun retryTimeout(attempt: Int, message: String?): Int = 3000
}

val apiResponse = runAndRetry(retryPolicy) { attempt, reason ->
  mainRepository.fetchPosters()
}.onSuccess {
  // handle a success case
}.onFailure {
  // handle failure cases
}

Operator

You can delegate the onSuccess, onError, and onException with the operator extension and ApiResponseOperator. Operator is very useful if you want to handle ApiResponse-s globally and reduce the boilerplates for your ViewModel and Repository classes. Here are some examples below:

ViewModel

We can delegate and operate the CommonResponseOperator using the operate extension.

disneyService.fetchDisneyPosterList().operator(
      CommonResponseOperator(
        success = {
          emit(data)
          Timber.d("success data: $data")
        },
        application = getApplication()
      )
    )

CommonResponseOperator

The CommonResponseOperator extends ApiResponseOperator with the onSuccess, onError, and onException override methods. They will be executed depending on 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>() {

  // handles error cases when the API request gets an error response.
  override fun onSuccess(apiResponse: ApiResponse.Success<T>) = success(apiResponse)

  // handles error cases depending on the status code.
  // 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")
      }
    }
  }

  // handles exceptional cases when the API request gets an exception response.
  // e.g., network connection error, timeout.
  override fun onException(apiResponse: ApiResponse.Failure.Exception<T>) {
    apiResponse.run {
      Timber.d(message())
      toast(message())
    }
  }
}

Operator for coroutines

If you want to operate and delegate a suspension lambda to the operator, you can use the suspendOperator extension and ApiResponseSuspendOperator class as the examples below:

ViewModel

You can use suspension functions like emit in the success scope.

flow {
  disneyService.fetchDisneyPosterList().suspendOperator(
      CommonResponseOperator(
        success = {
          emit(data)
          Timber.d("success data: $data")
        },
        application = getApplication()
      )
    )
}.flowOn(Dispatchers.IO)

CommonResponseOperator

The CommonResponseOperator extends ApiResponseSuspendOperator with suspend override methods as the examples below:

class CommonResponseOperator<T> constructor(
  private val success: suspend (ApiResponse.Success<T>) -> Unit,
  private val application: Application
) : ApiResponseSuspendOperator<T>() {

  // handles the success case when the API request gets a successful response.
  override suspend fun onSuccess(apiResponse: ApiResponse.Success<T>) = success(apiResponse)

  // ... //

Global operator

You can operate an operator globally whole ApiResponse-s in your application with the SandwichInitializer. So you don't need to create every instance of the Operators or use dependency injection for handling common operations. Here are some examples of handling a global operator for the ApiResponse.Failure.Error and ApiResponse.Failure.Exception.

Application class

First, you should initialize the global operator to the SandwichInitializer.sandwichOperator. It's highly recommended to initialize this in the Application class.

class SandwichDemoApp : Application() {

  override fun onCreate() {
    super.onCreate()
    
    // We will handle only the error and exceptional cases,
    // so we don't need to mind the generic type of the operator.
    SandwichInitializer.sandwichOperators += GlobalResponseOperator<Any>(this)

    // ... //

GlobalResponseOperator

Next, create your own GlobalResponseOperator, which extends operators such as ApiResponseSuspendOperator and ApiResponseOperator as the examples below:

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

  // handles error cases when the API request gets an 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")
        }
      }
    }
  }

  // handles exceptional cases when the API request gets an exception response.
  // e.g., network connection error, timeout.
  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

Finally, you don't need to use the operator expression anymore. The global operator will be operated, so you should handle only the ApiResponse.Success.

Note: This example didn't implement for the onSuccess case.

flow {
  disneyService.fetchDisneyPosterList().
    suspendOnSuccess {
      emit(data)
    }
}.flowOn(Dispatchers.IO).asLiveData()

Merge

You can merge multiple ApiResponses as a single ApiResponse depending on policies. The example below shows how to merge three ApiResponse as a single one if each three ApiResponses are successful.

disneyService.fetchDisneyPosterList(page = 0).merge(
   disneyService.fetchDisneyPosterList(page = 1),
   disneyService.fetchDisneyPosterList(page = 2),
   mergePolicy = ApiResponseMergePolicy.PREFERRED_FAILURE
).onSuccess { 
  // handles the success case when the API request gets a successful response.
}.onError { 
  // handles error cases when the API request gets an error response.
}

ApiResponseMergePolicy

ApiResponseMergePolicy is a policy for merging response data depend on the success or not.

  • IGNORE_FAILURE: Regardless of the merging sequences, ignores failure responses in the responses.
  • PREFERRED_FAILURE (default): Regardless of the merging sequences, prefers failure responses in the responses.

toFlow

You can get a Flow that emits body data if the response is an ApiResponse.Success and the data is not null.

disneyService.fetchDisneyPosterList()
  .onError {
    // handles error cases when the API request gets an error response.
  }.onException {
    // handles exceptional cases when the API request gets an exception response.
  }.toFlow() // returns a coroutines flow
  .flowOn(Dispatchers.IO)

If you want to transform the original data and take a flow that contains transformed data, you can follow as the examples below:

val response = pokedexClient.fetchPokemonList(page = page)
response.toFlow { pokemons ->
  pokemons.forEach { pokemon -> pokemon.page = page }
  pokemonDao.insertPokemonList(pokemons)
  pokemonDao.getAllPokemonList(page)
}.flowOn(Dispatchers.IO)

Injecting a custom CoroutineScope and Unit Tests

Sandwich uses an internal coroutine scope to execute network requests in the background thread, but can inject your custom CoroutineScope by setting your scope on your ApiResponseCallAdapterFactory like the below:

.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(
  coroutineScope = `Your Coroutine Scope`
))

You can apply your coroutine scope globally for the ApiResponseCallAdapterFactory by setting your scope on SandwichInitializer as the below:

SandwichInitializer.sandwichScope = `Your Coroutine Scope`

Also, you can inject a test coroutine scope into the ApiResponseCallAdapterFactory in your unit test cases.

val testScope = TestScope(coroutinesRule.testDispatcher)
val retrofit = Retrofit.Builder()
      .baseUrl(mockWebServer.url("/"))
      .addConverterFactory(MoshiConverterFactory.create())
      .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(testScope))
      .build()

Kotlin Serialization

This library allows you to deserialize your error body of the Retrofit response as your custom error class with Kotlin's Serialization.

For more information about setting up the plugin and dependency, check out Kotlin's Serialization.

Maven Central

Add the dependency below to your module's build.gradle file:

dependencies {
    implementation "com.github.skydoves:sandwich-serialization:<version>"
}

Deserialize Error Body

You can deserialize your error body with deserializeErrorBody extension and your custom error class. First, define your custom error class following your RESTful API formats as seen in the below:

@Serializable
public data class ErrorMessage(
  val code: Int,
  val message: String
)

Next, gets the result of the error class from the ApiResponse instance with the deserializeErrorBody extension like the below:

val apiResponse = pokemonService.fetchPokemonList()
val errorModel: ErrorMessage? = apiResponse.deserializeErrorBody()

Or you can get deserialized error response directly with onErrorDeserialize extension like the below:

val apiResponse = mainRepository.fetchPosters()
apiResponse.onErrorDeserialize<List<Poster>, ErrorMessage> { errorMessage ->
  ..
}

Sandwich DataSource

This library provides additional solutions DataSource for handing network responses.

Maven Central

Add the dependency below to your module's build.gradle file:

dependencies {
    implementation "com.github.skydoves:sandwich-datasource:<version>"
}

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 ->
        // handles the success case when the API request gets a successful response.
        response.onSuccess {
          Timber.d("$data")
          posterListLiveData.postValue(data)
        }
          // handles error cases 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(this.toString())
            }
          }
          // handles exceptional cases when the API request gets an exception response.
          // e.g. network connection error, timeout.
          .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

You can get the DataSource directly from the Retrofit service. Add the DataSourceCallAdapterFactory call adapter factory to your Retrofit builder. And change the return type of your Retrfot services Call to DataSource.

Retrofit.Builder()
    ...
    .addCallAdapterFactory(DataSourceCallAdapterFactory.create())
    .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()

DataSource with Coroutines

You can use the DataSource in Retrofit services with suspend keyword.

Retrofit.Builder()
    ...
    .addCallAdapterFactory(DataSourceCallAdapterFactory.create())
    .build()

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

This 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.

More Repositories

1

Pokedex

🗡️ Pokedex demonstrates modern Android development with Hilt, Material Motion, Coroutines, Flow, Jetpack (Room, ViewModel) based on MVVM architecture.
Kotlin
7,496
star
2

android-developer-roadmap

🗺 The Android Developer Roadmap offers comprehensive learning paths to help you understand Android ecosystems.
Kotlin
6,880
star
3

Balloon

🎈 Modernized and sophisticated tooltips, fully customizable with an arrow and animations for Android.
Kotlin
3,535
star
4

chatgpt-android

📲 ChatGPT Android demonstrates OpenAI's ChatGPT on Android with Stream Chat SDK for Compose.
Kotlin
3,460
star
5

TransformationLayout

🌠 Transform between two Views, Activities, and Fragments, or a View to a Fragment with container transform animations for Android.
Kotlin
2,279
star
6

landscapist

🌻 A pluggable, highly optimized Jetpack Compose and Kotlin Multiplatform image loading library that fetches and displays network images with Glide, Coil, and Fresco.
Kotlin
1,876
star
7

ColorPickerView

🎨 Android colorpicker for getting colors from any images by tapping on the desired color.
Java
1,503
star
8

DisneyMotions

🦁 A Disney app using transformation motions based on MVVM (ViewModel, Coroutines, Flow, Room, Repository, Koin) architecture.
Kotlin
1,482
star
9

AndroidVeil

🎭 An easy, flexible way to implement loading skeletons and shimmering effect for Android.
Kotlin
1,384
star
10

MarvelHeroes

❤️ A sample Marvel heroes application based on MVVM (ViewModel, Coroutines, Room, Repository, Koin) architecture.
Kotlin
1,224
star
11

PowerMenu

🔥 Powerful and modernized popup menu with fully customizable animations.
Java
1,163
star
12

PowerSpinner

🌀 A lightweight dropdown popup spinner, fully customizable with an arrow and animations for Android.
Kotlin
1,105
star
13

Orbital

🪐 Jetpack Compose Multiplatform library that allows you to implement dynamic transition animations such as shared element transitions.
Kotlin
989
star
14

DisneyCompose

🧸 A demo Disney app using Jetpack Compose and Hilt based on modern Android tech stacks and MVVM architecture.
Kotlin
929
star
15

WhatIf

☔ Fluent syntactic sugar of Kotlin for handling single if-else statements, nullable, collections, and booleans.
Kotlin
835
star
16

ExpandableLayout

🦚 An expandable layout that shows a two-level layout with an indicator.
Kotlin
809
star
17

ElasticViews

✨ An easy way to implement an elastic touch effect for Android.
Kotlin
784
star
18

ProgressView

🌊 A polished and flexible ProgressView, fully customizable with animations.
Kotlin
755
star
19

AndroidRibbon

🎀 A fancy and beautiful ribbon with shimmer effects for Android.
Kotlin
684
star
20

Cloudy

☁️ Jetpack Compose blur effect library, which falls back onto a CPU-based implementation to support older API levels.
Kotlin
639
star
21

Needs

🌂 An easy way to implement modern permission instructions popup.
Kotlin
616
star
22

Pokedex-AR

🦄 Pokedex-AR demonstrates ARCore, Sceneform, and modern Android tech stacks — such as Hilt, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture.
Kotlin
582
star
23

FlexibleBottomSheet

🐬 Advanced Compose Multiplatform bottom sheet for segmented sizing and non-modal type, similar to Google Maps.
Kotlin
541
star
24

Only

💐 An easy way to persist and run code block only as many times as necessary on Android.
Kotlin
485
star
25

TheMovies

🎬 A demo project for The Movie DB based on Kotlin MVVM architecture and material design & animations.
Kotlin
484
star
26

MovieCompose

🎞 A demo movie app using Jetpack Compose and Hilt based on modern Android tech stacks.
Kotlin
475
star
27

ColorPickerPreference

🎨 A library that lets you implement ColorPicker, ColorPickerDialog, ColorPickerPreference.
Kotlin
474
star
28

TheMovies2

🎬 A demo project using The Movie DB based on Kotlin MVVM architecture and material design & animations.
Kotlin
474
star
29

Submarine

🚤 Floating navigation view for displaying a list of items dynamically on Android.
Kotlin
471
star
30

retrofit-adapters

🚆 Retrofit call adapters for modeling network responses using Kotlin Result, Jetpack Paging3, and Arrow Either.
Kotlin
462
star
31

Rainbow

🌈 Fluent syntactic sugar of Android for applying gradations, shading, and tinting.
Kotlin
451
star
32

Orchestra

🎺 A collection of Jetpack Compose libraries, which allows you to build tooltips, spinners, and color pickers.
Kotlin
447
star
33

IndicatorScrollView

🧀 A dynamic scroll view that animates indicators according to its scroll position.
Kotlin
415
star
34

PreferenceRoom

🚚 Android processing library for managing SharedPreferences persistence efficiently and structurally.
Java
378
star
35

colorpicker-compose

🎨 Jetpack Compose color picker library for getting colors from any images by tapping on the desired color.
Kotlin
370
star
36

DoubleLift

🦋 Expands and collapses a layout horizontally and vertically sequentially.
Kotlin
360
star
37

GoldMovies

👑 The GoldMovies is based on Kotlin, MVVM architecture, coroutines, dagger, koin, and material designs & animations.
Kotlin
354
star
38

lazybones

😴 A lazy and fluent syntactic sugar for observing Activity, Fragment, and ViewModel lifecycles with lifecycle-aware properties.
Kotlin
351
star
39

sealedx

🎲 Kotlin Symbol Processor that auto-generates extensive sealed classes and interfaces for Android and Kotlin.
Kotlin
316
star
40

Bindables

🧬 Android DataBinding kit for notifying data changes to UI layers with MVVM architecture.
Kotlin
304
star
41

AndroidBottomBar

🍫 A lightweight bottom navigation view, fully customizable with an indicator and animations.
Kotlin
297
star
42

GithubFollows

:octocat: A demo project based on MVVM architecture and material design & animations.
Kotlin
293
star
43

Bundler

🎁 Android Intent & Bundle extensions that insert and retrieve values elegantly.
Kotlin
262
star
44

gemini-android

✨ Gemini Android demonstrates Google's Generative AI on Android with Stream Chat SDK for Compose.
Kotlin
262
star
45

snitcher

🦉 Snitcher captures global crashes, enabling easy redirection to the exception tracing screen for swift recovery.
Kotlin
216
star
46

Chamber

⚖️ A lightweight Android lifecycle-aware and thread-safe pipeline for communicating between components with custom scopes.
Kotlin
185
star
47

twitch-clone-compose

🎮 Twitch clone project demonstrates modern Android development built with Jetpack Compose and Stream Chat/Video SDK for Compose.
Kotlin
180
star
48

Flourish

🎩 Flourish implements dynamic ways to show up and dismiss layouts with animations.
Kotlin
174
star
49

compose-stable-marker

✒️ Compose stable markers for KMP to tell stable/immutable guarantees to the compose compiler.
Kotlin
169
star
50

BaseRecyclerViewAdapter

⚡ Fast way to bind RecyclerView adapter and ViewHolder for implementing clean sections.
Kotlin
163
star
51

Multi-ColorPicker

Android multi colorpicker for getting colors from any images by tapping on the desired color.
Kotlin
125
star
52

All-In-One

👔 Health care application for reminding health-todo lists and making healthy habits every day.
Kotlin
118
star
53

Medal

🏅An easy way to implement medal effect for Android.
Kotlin
111
star
54

viewmodel-lifecycle

🌳 ViewModel Lifecycle allows you to track and observe Jetpack's ViewModel lifecycle changes.
Kotlin
104
star
55

WaterDrink

💧 Simple water drinking reminder application based on MVP architecture.
Kotlin
75
star
56

CameleonLayout

A library that let you implement double-layer-layout changing with slide animation.
Kotlin
71
star
57

Awesome-Android-Persistence

A curated list of awesome android persistence libraries about SQLite, ORM, Mobile Database, SharedPreferences, etc.
70
star
58

SyncMarket

Let managing your application version update more simply.
Java
41
star
59

MagicLight-Controller

This simple demo application is controlling MagicLight's smart bulbs by bluetooth-le
Java
35
star
60

MethodScope

Reduce repetitive inheritance works in OOP world using @MethodScope.
Java
33
star
61

MapEditor

You can draw your map using by this Map Editor project.
C#
22
star
62

skydoves

🕊 skydoves
14
star
63

seungmani

This simple project is cocos-2dx c++ multi-patform(win32, android, ios, linux) game in Jan 2015.
C++
8
star
64

soniaOnline

XNA C# win 32/64 patform MMO game in Jan 2016.
C#
5
star
65

NityLife

This simple project is cocos-2dx c++ multi-patform(win32, android, ios, linux) game in 2014.
C++
5
star
66

Rurimo-Camera

You can take some screenshots or save images at clipboard so easily like just one click on Windows with this application.
C#
1
star