All Projects → skydoves → Sandwich

skydoves / Sandwich

Licence: apache-2.0
🥪 A lightweight and standardized Android network response interface for handling successful data and error responses.

Programming Languages

kotlin
9241 projects

Projects that are alternatives of or similar to Sandwich

Avenging
MVP pattern example on Android: no Dagger or RxJava example
Stars: ✭ 279 (-24.59%)
Mutual labels:  api, retrofit
Rxretrojsoup
A simple API-like from html website (scrapper) for Android, RxJava2 ready !
Stars: ✭ 492 (+32.97%)
Mutual labels:  api, retrofit
Easygo
基于Kotlin、OkHttp的声明式网络框架,像写HTML界面一样写网络调用代码
Stars: ✭ 40 (-89.19%)
Mutual labels:  network, retrofit
X
新生命X组件,数据中间件XCode、日志、网络、RPC、序列化、缓存、Windows服务
Stars: ✭ 1,322 (+257.3%)
Mutual labels:  api, network
Grafanajsondatasource
Grafana datasource to load JSON data over your arbitrary HTTP backend
Stars: ✭ 146 (-60.54%)
Mutual labels:  api, datasource
Reddit Detective
Play detective on Reddit: Discover political disinformation campaigns, secret influencers and more
Stars: ✭ 129 (-65.14%)
Mutual labels:  api, network
Offit
Simple but powerful API mocking library. Make mocks great again.
Stars: ✭ 25 (-93.24%)
Mutual labels:  api, retrofit
Component
🔥🔥🔥A powerful componentized framework.一个强大、100% 兼容、支持 AndroidX、支持 Kotlin并且灵活的组件化框架
Stars: ✭ 2,434 (+557.84%)
Mutual labels:  api, retrofit
Materialhome
一个基于 Material Design 风格设计的图书展示类App,豆瓣图书,在线电子书。
Stars: ✭ 331 (-10.54%)
Mutual labels:  api, retrofit
Software
DeepValueNetwork is a peer-to-peer database network managed and hosted by its community. It contains a browser to render 2D/3D content and allow the creation of scripted applications built on top of the p2p database network and managed by its creators, without intermediary platform.
Stars: ✭ 357 (-3.51%)
Mutual labels:  network
Wp Graphql Acf
WPGraphQL for Advanced Custom Fields
Stars: ✭ 358 (-3.24%)
Mutual labels:  api
Newton Api
➗ A really micro micro-service for advanced math.
Stars: ✭ 358 (-3.24%)
Mutual labels:  api
Ocrserver
A simple OCR API server, seriously easy to be deployed by Docker, on Heroku as well
Stars: ✭ 359 (-2.97%)
Mutual labels:  api
Vuex Rest Api
A utility to simplify the use of REST APIs with Vuex
Stars: ✭ 365 (-1.35%)
Mutual labels:  api
Diplomat
A HTTP Ruby API for Consul
Stars: ✭ 358 (-3.24%)
Mutual labels:  api
Django Api Domains
A pragmatic styleguide for Django API Projects
Stars: ✭ 365 (-1.35%)
Mutual labels:  api
Yarp
YARP - Yet Another Robot Platform
Stars: ✭ 358 (-3.24%)
Mutual labels:  network
Swagger Typescript Api
TypeScript API generator via Swagger scheme
Stars: ✭ 342 (-7.57%)
Mutual labels:  api
Stone
The Official API Spec Language for Dropbox API V2
Stars: ✭ 371 (+0.27%)
Mutual labels:  api
Bgpalerter
Software to monitor streams of BGP data. Pre-configured for real-time detection of visibility loss, RPKI invalid announcements, hijacks, and more.
Stars: ✭ 367 (-0.81%)
Mutual labels:  network

Sandwich

🥪 A lightweight and standardized Android network response interface for handling successful data and error responses.

License API Build Status Javadoc Medium Profile

Why Sandwich?

Sandwich was invented for constructing the standardized response interface from the network response. We can handle successful data, error response, and an exceptional case intuitively using useful extensions of the interface. So we don't need to design and implement wrapper classes like Resource or Result, and it helps to reduce our work time and makes focus on only business codes. Sandwich supports handling error responses globally, Mapper, Operator, and great compatibilities like toLiveData or toFlow. Also, we can implement great harmony with coroutines and flow in our projects using this library.

Download

Maven Central Jitpack

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

Gradle

Add below codes to your root build.gradle file (not your module build.gradle file).

allprojects {
    repositories {
        mavenCentral()
    }
}

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

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

Usecase

You can reference the good 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.

Table of contents

Usage

ApiResponse

ApiResponse is an interface for constructing standard responses from the response of the retrofit call. It provides useful extensions for handling successful data and error responses. 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)
// 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.Success

A standard Success response interface from Retrofit network responses.
We can get the successful 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

A standard failure response interface from Retrofit network responses.
API communication conventions do 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 requests or processing an response in the client side. 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 handle the response cases without using the if-else/when clause.
Each scope will be executed or not depending on the type of the ApiResponse. (success, error, exception)

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

ApiResponse for coroutines

We can use the suspend keyword in our Retrofit services and gets ApiResponse<*> as a response type.
Build your Retrofit using the CoroutinesResponseCallAdapterFactory call adapter factory.

.addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())

We should make normal service functions as suspension functions using the suspend keyword. And we can get the ApiResponse<*> as a response type. So we can get the ApiResponse from the Retrofit service call, and handle them right away using extensions.

interface DisneyCoroutinesService {

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

We can use like the 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.
       posterDao.insertPosterList(data)
       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.
      }
    }
  }
}

suspendOnSuccess, suspendOnError, suspendOnException

We can use suspension extensions for invoking suspension related functions inside scopes. These extensions are not functionally different from the onSuccess, onError, and onException extensions.
Generally, we can use this way on the repository pattern.

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

Mapper

Mapper is useful when we want to transform 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 SuccessPosterMapper<T, R> and map 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)

We can use the map extension with a lambda.

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

If we want to get the transformed data from the start in the lambda, we can give the mapper as a parameter for the onSuccess or suspendOnSuccess.

.suspendOnSuccess(SuccessPosterMapper) {
    val poster = this
}

ApiErrorModelMapper

We can map the ApiResponse.Failure.Error model to our custom error model using the ApiErrorModelMapper<T> and map extension.

// 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 we want to get the transformed data from the start in the lambda, we can give the mapper as a parameter for the onError or suspendOnError.

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

Operator

We can delegate the onSuccess, onError, onException using the operator extension and ApiResponseOperator. Operator is very useful if we want to handle ApiResponses 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 = {
          data?.let { emit(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>() {

  // 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 we want to operate and delegate a suspension lambda to the operator, we can use the suspendOperator extension and ApiResponseSuspendOperator class.

ViewModel

We can use suspension functions like emit in the success lambda.

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

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

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

  // ... //

Global operator

We can operate an operator globally all ApiResponses in our application 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 a global operator about the ApiResponse.Failure.Error and ApiResponse.Failure.Exception. In this example, 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 exceptional cases,
    // so we don't need to mind the generic type of the operator.
    SandwichInitializer.sandwichOperator = GlobalResponseOperator<Any>(this)

    // ... //

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

  // 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

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

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 { 
  // 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 order, ignores failure responses in the responses.
  • PREFERRED_FAILURE (default): Regardless of the merging order, prefers failure responses in the responses.

toLiveData

We can get a LiveData that contains successful data if the response is an ApiResponse.Success. If our goal is only getting a LiveData that holds successful data, we can emit the onSuccess extension.

posterListLiveData = liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
  emitSource(
    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.
     }.toLiveData()) // returns an observable LiveData
}

If we want to transform the original data and get a LiveData that contains transformed data using successful data if the response is an ApiResponse.Success.

posterListLiveData = liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
  emitSource(
   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.
    }.toLiveData {
      this.onEach { poster -> poster.date = SystemClock.currentThreadTimeMillis() }
    }) // returns an observable LiveData
    }

toFlow

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

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

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.
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].