How to get the response of another observable when error occurs? - android

I just want to ask if it is possible to get the response of another observable after encountering an error from the another observable?
for example I am calling a two api Avatar and Attachment using a combineLatest.
val avatar: Observable<ResponseBody> = api().getAvatar()
val attachment: Observable<ResponseBody> = api().getAttachment()
val obs = Observables.combineLatest(avatar, attachment)
.map { it ->
if (it.first is Exception) {
Log.e(TAG, "getAvatar failed")
} else {
updateAvatar()
}
if (it.second is Exception) {
Log.e(TAG, "getAttachment failed")
} else {
updateAttachment()
}
if (it.first !is Exception && it.second !is Exception) {
Log.i(TAG, "success first=${it.first}, second=${it.second}")
updateAll()
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturn { it }
.subscribe()
disposable.add(obs)
I just want to get the avatar response if the attachment error and I want to get the attachment response if the avatar error.
Thanks.

Yes, my friend. You can handle error for each observable that you combine by calling onErrorReturn() method. You can use empty ResponseBody for detecting error. Final code
val avatar: Observable<Optional<ResponseBody>> = api().getAvatar().onErrorReturn{ Optional.empty }
val attachment: Observable<Optional<ResponseBody>> = api().getAttachment().onErrorReturn{ Optional.empty }
val obs = Observables.combineLatest(avatar, attachment) {avatar, attachment ->
if (!avatar.isPresent()) {
//logic
}
if (!attachment.isPresent()) {
//logic
}
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorReturn { it }
.subscribe()
If you use java 7 or lower in you project, you can write your own Optional
class Optional<T>(val value: T?) {
companion object {
fun <T> empty(): Optional<T> = Optional(null)
}
fun isPresent() = value != null
}

Related

Retain errorBody from HttpException

I am trying to map the error body from an exception into into a useful model with now luck, using moshi and retrofit 2.9.0
I found numerous posts discussing the same issue but none of the solutions worked for me.
My code is the following
private fun getErrorMessageFromGenericResponse(httpException: HttpException): String? {
var errorMessage: String? = null
try {
val body = httpException.response()!!.errorBody()!!
val errorResponse = body.string().toObject(ErrorResponse::class.java)
errorMessage = errorResponse?.message
} catch (e: IOException) {
e.printStackTrace()
} finally {
return errorMessage
}
}
fun <T> String.toObject(objectClass: Class<T>): T? {
val moshi = Moshi.Builder().build()
val adapter = moshi.adapter(objectClass).lenient()
return adapter.fromJson(this)
}
I tried also using this but it also does not work:
httpException.response()!!.errorBody()!!.source().buffer.snapshot().utf8()
I am probably missing something really simple as I think its a common usecase.
Thanks in advance.
fun handleErrorResponse(e: Throwable): String {
return when (e) {
is HttpException -> {
parseHTTPError(e.response()!!.errorBody())
}
is SocketTimeoutException -> {
ApiConstants.TIME_OUT
}
is IOException -> {
ApiConstants.SERVERERROR
}
else -> ApiConstants.SERVERERROR
}
}
fun parseHTTPError(responseBody: ResponseBody?): String {
try {
val jsonObject=JSONObject(responseBody!!.string())
try {
val error=jsonObject.getJSONArray("message")
return error[0].toString()
}
catch (ex: Exception) {
responseBody!!.close()
return ""
}
responseBody.close()
return ""
}

Mvvm Coroutine Mockito testing ViewModel giving error

My test case to test the viewmodel looks like this :
#Before
fun setUp() {
loginActivityViewModel = LoginActivityViewModel(loginRepository)
.apply { users.observeForever(userObserver) }
}
#Test
fun `check user response when get successful response from server`() {
testCoroutineRule.runBlockingTest {
//Given
whenever(loginRepository.getLoginResponse(loginRequest)).then(Answer { loginResponse })
//When
loginActivityViewModel.loginResponse(loginRequest)
//Then
verify(userObserver).onChanged(Resource.loading(data = null))
verify(userObserver).onChanged(Resource.success(data = loginResponse))
}
}
#Test
fun `check user response when get unsuccessful response from server`() {
testCoroutineRule.runBlockingTest {
//Given
whenever(loginRepository.getLoginResponse(loginRequest)).thenThrow(Error("Some error"))
//When
loginActivityViewModel.loginResponse(loginRequest)
//Then
verify(userObserver).onChanged(Resource.loading(data = null))
verify(userObserver).onChanged(Resource.error(message = "Some error"))
}
}
Inside this first test case run successfully but when it run 2nd one giving this error:
Wanted but not invoked: userObserver.onChanged(
Resource(status=ERROR, data=null, message=Some error) );
-> at com.android.loginapp.viewmodel.LoginActivityViewModelTest$check user response when get unsuccessful response from
server$1.invokeSuspend(LoginActivityViewModelTest.kt:83)
However, there was exactly 1 interaction with this mock:
userObserver.onChanged(
Resource(status=LOADING, data=null, message=null) );
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
My viewModel network calling method look like this:
fun loginResponse(loginRequest: LoginRequest) {
viewModelScope.launch {
users.postValue(Resource.loading(null))
try {
val usersFromApi = loginRepository.getLoginResponse(loginRequest)
users.postValue(Resource.success(usersFromApi))
} catch (e: Exception) {
users.postValue(Resource.error(e.message.toString()))
}
}
}
Not sure why it's giving this error.
I need use .thenThrow(RuntimeException("test error")) then only it will pass.

Multiple Retrofit calls with Flow

I made app where user can add server (recycler row) to favorites. It only saves the IP and Port. Than, when user open FavoriteFragment Retrofit makes calls for each server
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Server
So in repository I mix the sources and make multiple calls:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
list.add(server)
}
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
and then in ViewModel I create LiveData object
fun getFavoriteServers() {
viewModelScope.launch {
repository.getFavoriteServersToRecyclerView()
.onEach { dataState ->
_favoriteServers.value = dataState
}.launchIn(viewModelScope)
}
}
And everything works fine till the Favorite server is not more available in the Lobby and the Retrofit call failure.
My question is: how to skip the failed call in the loop without crashing whole function.
Emit another flow in catch with emitAll if you wish to continue flow like onResumeNext with RxJava
catch { cause ->
emitAll(flow { emit(DataState.Errorcause)})
}
Ok, I found the solution:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val list: MutableList<Server> = mutableListOf()
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val job = CoroutineScope(coroutineContext).launch {
getFavoritesServersNotLiveData.forEach { fav ->
val server = getServer(fav.ip, fav.port)
server.collect { dataState ->
when (dataState) {
is DataState.Loading -> Log.d(TAG, "loading")
is DataState.Error -> Log.d(TAG, dataState.exception.message!!)
is DataState.Success -> {
list.add(dataState.data)
Log.d(TAG, dataState.data.toString())
}
}
}
}
}
job.join()
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
when using retrofit you can wrap response object with Response<T> (import response from retrofit) so that,
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Response<Server>
and then in the Repository you can check if network failed without using try-catch
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
if(getFavoritesServersNotLiveData.isSuccessful) {
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.body().forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
// if the above request fails it wont go to the else block
list.add(server)
}
emit(DataState.Success(list))
} else {
val error = getFavoritesServersNotLiveData.errorBody()!!
//do something with error
}
}

How to handle both success and fail responses in Retrofit and RxJava2?

I have the following request:
#POST("devices/link/")
fun linkDevice(
#Body deviceInfo: DeviceInfo
): Single<Response<UserDevice>>
I want to achieve the following:
If request was successful, get DeviceInfo and, depending on status code, report event to analytics
If request failed, get exception, transform it and rethrow.
So, the code is:
fun linkDevice(): Single<UserDevice> {
val deviceInfo = deviceUtil.getDeviceInfo()
return devicesService
.linkDevice(deviceInfo)
.onErrorResumeNext { e ->
val transformedException = transformRequestException(e) { httpException ->
return#transformRequestException when (httpException.code()) {
409 -> DeviceAlreadyLinkedException()
else -> null
}
}
Single.error(transformedException)
}
.flatMap { response ->
if (response.isSuccessful) {
val userDevice = response.body()
userDevice?.let {
userPreferences.setDeviceId(it.id)
}
when (response.code()) {
200 -> {
// TODO: Analytics
}
201 -> {
// TODO: Analytics
}
}
Single.just(userDevice)
} else {
throw ApiException()
}
}
}
/**
* Transform exception that came from request
* By default, handles only network and HTTP 401 exceptions, but can contain custom logic, passed with specialTransformer
*/
fun transformRequestException(
e: Throwable,
specialTransformer: ((httpException: HttpException) -> Exception?)? = null
): Exception {
return if (e is HttpException) {
if (e.code() == 401) {
NotAuthenticatedException()
} else {
specialTransformer?.invoke(e) ?: ApiException()
}
} else {
NetworkException()
}
}
But this code does not work, if I operate with Response object, it will work only with Single<UserDevice> in my Retrofit service.
But, with Single<UserDevice> I can't get 2XX codes.
So, how to implement the desired behavior?
So, as for now, I've solved as following:
data class RequestResult<T>(
val data: T,
val code: Int
)
fun <T> transformResponse(response: Response<T>): Single<RequestResult<T>> {
if (response.isSuccessful) {
return Single.just(RequestResult(response.body()!!, response.code()))
} else {
throw HttpException(response)
}
}
fun <T> transformEmptyResponse(response: Response<T>): Completable {
if (response.isSuccessful) {
return Completable.complete()
} else {
throw HttpException(response)
}
}
And:
fun linkDevice(): Single<UserDevice> {
val deviceInfo = deviceUtil.getDeviceInfo()
return devicesService
.linkDevice(deviceInfo)
.flatMap { transformResponse(it) }
.flatMap { requestResult ->
requestResult.data.let { userDevice ->
userPreferences.setDeviceId(userDevice.id)
when (requestResult.code) {
200 -> {
// TODO: Analytics
}
201 -> {
// TODO: Analytics
}
}
Single.just(userDevice)
}
}
.onErrorResumeNext { e ->
val transformedException = transformRequestException(e) { httpException ->
return#transformRequestException when (httpException.code()) {
409 -> DeviceAlreadyLinkedException()
else -> null
}
}
Single.error(transformedException)
}
}
Looks like hack, but I did not find any better solution, and it works.

retryWhen on RxJava with Rx2Apollo

I'm using Rx2Apollo to make a graphql call:
private fun registerCardToken(token: String): io.reactivex.Observable<RegisterCardTokenMutation.RegisterCreditCard> {
val apolloCall = apolloClient().mutate(RegisterCardTokenMutation.builder().token(token).build())
return Rx2Apollo.from(apolloCall).map {
(it.data() as RegisterCardTokenMutation.Data).registerCreditCard()
}.doOnError({ error ->
//Log.e("registerCardToke", error.message)
})
}
This works well, but I want to handle specific error and retry this onces. I have tried to work around this using retryWhen and retry , but not able to write any executable code yet.
The retry persons a token refresh before performing the actual retry. Here's the token refresh sample:
private fun refreshBearerToken(callback: OnCompleteListener<GetTokenResult>) {
FirebaseAuth.getInstance().currentUser?.getIdToken(true)?.addOnCompleteListener(callback)
}
First, you have to turn refreshBearerToken into an Observable
val refreshTokenSource = Observable.create({ emitter ->
FirebaseAuth.
getInstance().
currentUser?.
getIdToken(true)?.
addOnCompleteListener({ task ->
if (task.isSuccessful()) {
emitter.onNext(task.getResult())
emitter.onComplete()
} else {
emitter.onError(task.getException())
}
})
})
Second, use some external reference holding the current token and conditionally use it before calling registerCardToken:
val currentToken = AtomicReference<String>()
val registerCardTokenObservable = Observable.defer({
val token = currentToken.get()
if (token == null) {
return refreshTokenSource
.doOnNext({ currentToken.set(it) })
.flatMap({ registerCardToken(it) })
}
return registerCardToken(token)
})
.retry({ error ->
if ((error is IOException) || (error.getMessage().contains("network")) {
currentToken.set(null)
return true
}
return false
})

Categories

Resources