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
Gradle
Add the dependency below into your module's build.gradle file:
dependencies {
implementation "com.github.skydoves:sandwich:1.3.7"
}SNAPSHOT
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
- ApiResponse
- onSuccess, onError, onException
- ApiResponse for coroutines
- ApiResponse Extensions for Coroutines
- Retrieve success data
- Mapper
- Run and Retry
- Operator, Operator for coroutines, Global Operator
- Merge
- toFlow
- ResponseDataSource
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.headersApiResponse.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.headersApiResponse.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
ApiResponseisApiResponse.Success. You can access body data directly in this scope. - onError: Executes if the
ApiResponseisApiResponse.Failure.Error. You can accessmessage()anderrorBodyin this scope. - onException: Executes if the
ApiResponseisApiResponse.Failure.Exception. You can accessmessage()in this scope. - onFailure: Executes if the
ApiResponseisApiResponse.Failure.ErrororApiResponse.Failure.Exception. You can accessmessage()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
ApiResponseisApiResponse.Success. You can access body data directly in this scope. - suspendOnError: Executes if the
ApiResponseisApiResponse.Failure.Error. You can accessmessage()anderrorBodyin this scope. - suspendOnException: Executes if the
ApiResponseisApiResponse.Failure.Exception. You can accessmessage()in this scope. - suspendOnFailure: Executes if the
ApiResponseisApiResponse.Failure.ErrororApiResponse.Failure.Exception. You can accessmessage()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
onSuccesscase.
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.
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.
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
DataSourceand 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.



