Sandwich was invented to construct standardized interfaces from the Retrofit network response. Sandwich allows you to handle the body data, errors, and exceptional cases obviously with useful 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 like toLiveData or toFlow. Also, you can utilize Sandwich with coroutines and flow.
🥪 Sandwich has been downloaded in more than 200k Android projects all over the globe!
Add the dependency below into your module's build.gradle
file:
dependencies {
implementation "com.github.skydoves:sandwich:1.2.6"
}
See how to import 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.2.7-SNAPSHOT"
}
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.
- 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.
- ApiResponse
- onSuccess, onError, onException
- ApiResponse for coroutines
- ApiResponse Extensions for Coroutines
- Retrieve success data
- Mapper
- Operator, Operator for coroutines, Global Operator
- Merge
- toLiveData, toFlow
- ResponseDataSource
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.
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
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
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
.
You can handle the ApiResponse
with the extensions below:
- onSuccess: Executes if the
ApiResponse
isApiResponse.Success
. You can access body data directly in this scope. - onError: Executes if the
ApiResponse
isApiResponse.Failure.Error
. You can accessmessage()
anderrorBody
in this scope. - onException: Executes if the
ApiResponse
isApiResponse.Failure.Exception
. You can accessmessage()
in this scope. - onFailure: Executes if the
ApiResponse
isApiResponse.Failure.Error
orApiResponse.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
}
}
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.
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
isApiResponse.Success
. You can access body data directly in this scope. - suspendOnError: Executes if the
ApiResponse
isApiResponse.Failure.Error
. You can accessmessage()
anderrorBody
in this scope. - suspendOnException: Executes if the
ApiResponse
isApiResponse.Failure.Exception
. You can accessmessage()
in this scope. - suspendOnFailure: Executes if the
ApiResponse
isApiResponse.Failure.Error
orApiResponse.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)
If you want to retrieve the encapsulated body data from the ApiResponse
directly, you can use the functionalities below.
Returns the encapsulated data if this instance represents ApiResponse.Success
or returns null if this is failed.
val data: List<Poster>? = disneyService.fetchDisneyPosterList().getOrNull()
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())
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 is useful when you want to transform the ApiResponse.Success
or ApiResponse.Failure.Error
to your custom model in ApiResponse
extension scopes.
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
}
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
}
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:
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()
)
)
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())
}
}
}
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:
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)
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)
// ... //
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
.
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.sandwichOperator = GlobalResponseOperator<Any>(this)
// ... //
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()
}
}
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()
You can merge multiple ApiResponse
s as a single ApiResponse
depending on policies. The example below shows how to merge three ApiResponse
as a single one if each three ApiResponse
s 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
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.
You can get a LiveData
that contains body data if the response is an ApiResponse.Success
. This is very useful if your goal is only getting a LiveData
from the ApiResponse
which holds successful data as the examples below:
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 you want to transform the original data and take a LiveData
that contains transformed data, you can follow as the examples below:
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
}
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)
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()
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 a Call
and lambda scope for constructing the DataSource.
val disneyService = retrofit.create(DisneyService::class.java)
val dataSource = ResponseDataSource<List<Poster>>()
dataSource.combine(disneyService.fetchDisneyPosterList()) { response ->
// stubs
}
Request API network call asynchronously.
If the request is successful, this data source will hold the success response model.
In the next request after the success, request() returns the cached API response.
If we need to fetch a new response data or refresh, we can use invalidate()
.
dataSource.request()
Retry fetching data (re-request) if your request got failure.
// retry fetching data 3 times with 5000 milli-seconds time interval when the request gets failure.
dataSource.retry(3, 5000L)
Observes every response data ApiResponse
from the API call request.
dataSource.observeResponse {
Timber.d("observeResponse: $it")
}
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 a cached (holding) data and re-fetching the API request.
dataSource.invalidate()
Concat an another DataSource
and request API call sequentially if the API call getting successful.
val dataSource2 = ResponseDataSource<List<PosterDetails>>()
dataSource2.retry(3, 5000L).combine(disneyService.fetchDetails()) {
// stubs handling dataSource2 response
}
dataSource1
.request() // request() must be called before concat.
.concat(dataSource2) // request dataSource2's API call after the success of the dataSource1.
.concat(dataSource3) // request dataSource3's API call after the success of the dataSource2.
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()
}
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()
}
}
}
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()
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())
}
}
}
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()
//...
}
Support it by joining stargazers for this repository. ⭐
And follow me for my next creations! 🤩
Copyright 2020 skydoves (Jaewoong Eum)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.