I am trying to transform an Observable in my login function but I keep getting this error. This my code and error I am getting on flatMap
fun login(phoneNumber: String, password: String, deviceId: String) {
// remove previous subscriptions
disposables.clear()
// function to save userInfo and access token
val saveResponse = { response: LoginResponse ->
val user = response?.user
val token = response?.token
// userManager.updateToken(champion, token, deviceId)
}
// on success callback
val onSuccess = { isSuccess: Boolean ->
progressBarVisibility.postValue(false)
loginSuccess.postValue(isSuccess)
if (!isSuccess) errorMessage.postValue("An error occurred please try again.")
}
// on failure callback
val onError = { throwable: Throwable ->
val message = when (throwable) {
is HttpException -> when (throwable.code()) {
400 -> "Enter valid Phone Number or Password"
422 -> "Incorrect Phone Number or Password"
else -> throwable.toErrorMessage()
}
else -> "An Error Occurred."
}
// show error message
errorMessage.postValue(message)
progressBarVisibility.postValue(false)
}
val disposable = accountUseCase.login(LoginRequest(phoneNumber, password)).observeOnUI()
.doOnSubscribe { progressBarVisibility.postValue(true) }
.flatMap {
val resp = it.data
when (resp) {
null -> Single.just(false)
else -> saveResponse(it)
}
}
.subscribe(onSuccess, onError)
// add subscription to disposables
disposables.add(disposable)
}
error
Type mismatch. Required: ((BaseResponse<LoginResponse>!) → SingleSource<out (???..???)>!)! Found: (BaseResponse<LoginResponse>!) → Any!
The problem is with the implicit return of your flatMap:
when (resp) {
null -> Single.just(false)
else -> saveResponse(it)
}
In the null branch, the return type is Single<Boolean>.
In the else branch, you return the result of saveResponse. But the return type of saveResponse is Unit, because it doesn't return anything.
Kotlin therefore infers the return type of your flatMap to be either Single<Boolean> or Unit, and the only common supertype of these is Any.
This is why you get the error message: Found: (BaseResponse<LoginResponse>!) → Any!
You need to have saveResponse return something, probably also a Single<Boolean>, depending on your usecase.
Related
With migration to kotlin, view model and recent changes in [kotlin test lib][1] I am working on issue with test.
I have a scenario:
request a web resource asynchronously
in case of error put the request in cache and update state with new pending request
All of this with help of kotlin flow and view model.
Scenario works well when executes on emulator, but fails when I run test for it. The issue is catch block of flow has not been triggered when error has thrown in flow.
Here is the code:
fun mintToken(to: String, value: Value, uri: String) {
logger.d("[start] mintToken()")
viewModelScope.launch {
repository.mintToken(to, value, uri)
.catch { it ->
if (it is TransactionException
&& it.message!!.contains("Transaction receipt was not generated after 600 seconds for transaction")) {
cacheRepository.createChainTx(to, value, uri) // TODO consider always put in pending cache and remove after it confirms as succeeded
val txReceipt = TransactionReceipt()
txReceipt.transactionHash = ""
emit(Response.Data(txReceipt))
} else {
emit(Response.Error.Exception(it))
}
}
.flowOn(Dispatchers.IO)
.collect {
logger.d(it.toString())
when (it) {
is Response.Data -> {
if (it.data.transactionHash.isEmpty()) {
state.update {
it.copy(
status = Status.MINT_TOKEN,
pendingTx = it.pendingTx + Transaction(to, value, uri)
)
}
}
}
is Response.Error.Message -> {
val errorMsg = "Something went wrong on mint a token with error ${it.msg}"
logger.d(errorMsg)
state.update {
val newErrors = it.errors + "Something went wrong on mint a token with error ${errorMsg}"
it.copy(status = Status.MINT_TOKEN, errors = newErrors)
}
}
is Response.Error.Exception -> {
logger.e("Something went wrong on mint a token ${to}, ${value}, ${uri}", it.error)
state.update {
val newErrors = it.errors + "Something went wrong on mint a token ${to}, ${value}, ${uri}"
it.copy(status = Status.MINT_TOKEN, errors = newErrors)
}
}
}
}
}
logger.d("[end] mintToken()")
}
#Throws(TransactionException::class)
override fun mintToken(to: String, value: Value, uri: String): Flow<Response<TransactionReceipt>> {
return flow {
throw TransactionException(
"Transaction receipt was not generated after 600 seconds for transaction",
"")
}
}
Test code for this is:
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Set the main coroutines dispatcher for unit testing.
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
private lateinit var subj: WalletViewModel
#Test
fun `when mintToken() is called with correct values, timeout exception is returned and pending tx are updated with new value`() = runTest {
val to = "0x6f1d841afce211dAead45e6109895c20f8ee92f0"
val url = "https://google.com"
val testValue = Value(
"Software Development",
BigInteger.valueOf(1000L),
BigInteger.valueOf(2000L),
false,
BigInteger.valueOf(0)
)
subj.mintToken(to, testValue, url)
assertThat(
"There is no pending transaction after mint a new token with timeout error",
subj.uiState.value.pendingTx.isNotEmpty()
)
}
Test code differs from dev code by replacing dispatcher in MainCoroutineRule and using kotlin construction runTest {}. How does it affect this case? Does issue case lays in some other place?
[1]: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md
This is my ViewModle class methods
private fun handleResponse(response: Response<OtpSentDataClass>): Resource<OtpSentDataClass> {
if (response.isSuccessful) {
response.body()?.let { otpSentResponse ->
return Resource.Success(otpSentResponse)
}
} else {
val body =
Gson().fromJson(response.errorBody()?.charStream(), ErrorBodyResponse::class.java)
when {
response.code() == 400 -> {
return (Resource.Failure(Constants.error400, body?.message))
}
response.code() == 401 -> {
return (Resource.Failure(Constants.error401, body?.message))
}
response.code() == 403 -> {
return (Resource.Failure(Constants.error403, body?.message))
}
...
response.code() == 408 -> {
return (Resource.Failure(Constants.error408, body?.message))
}
...
...
response.code() == 413 -> {
return (Resource.Failure(Constants.error413, body?.message))
}
...
response.code() == 511 -> {
return (Resource.Failure(Constants.error511, body?.message))
}
else -> {
return Resource.Failure("Something went wrong ${body?.message}")
}
}
}
val body = Gson().fromJson(
response.errorBody()?.charStream(),
ErrorBodyResponse::class.java
)
return Resource.Failure("Something went wrong ${body?.message}")
}
similarly i have other methods in this same viewmodle
like
private fun handleDeviceDetailResponse(response: Response<DeviceDetailResponse>): Resource<DeviceDetailResponse>
private fun handleVerifyResponse(response: Response<VerifyOtpResponse>): Resource<VerifyOtpResponse>
Now the problem is I have many other viewmodels which has these kind of handle response methods, which creates lots of boilerplate.
How should I keep my code clean?
I would put your error codes in a map, so you can retrieve them by number. I don't know what type your constants are, so I'm just treating them as Strings as an example.
object Constants {
//...
private val errorCodes = mapOf(
400 to "bad_request",
401 to "unauthorized",
// etc.
)
const val UNSPECIFIED_ERROR_CODE_STRING
fun errorCodeToString(code: Int): String = errorCodes[code] ?: UNSPECIFIED_ERROR_CODE_STRING
}
Then you can use your function in you application code. Also, your code is tripping over nullability because you separately check isSuccessful and then get your body(). body() is non-null if and only if isSuccessful is true, so there's no reason to check isSuccessful. Your code as written forces you to write a redundant branch of failure code at the bottom that will never be reached.
private fun handleResponse(response: Response<OtpSentDataClass>): Resource<OtpSentDataClass> {
response.body()?.let { otpSentResponse ->
return Resource.Success(otpSentResponse)
}
// We can assert non-null errorBody, because body and errorBody are never both null
val errorBody = Gson().fromJson(response.errorBody()!!.charStream(), ErrorBodyResponse::class.java)
val errorCode = Constants.errorCodeToString(response.code())
return when (errorCode) {
UNSPECIFIED_ERROR_CODE_STRING -> Resource.Failure("Something went wrong ${errorBody?.message}")
else -> Resource.Failure(errorCode, errorBody?.message)
}
}
I'm trying to update the validate the list and update the value accordingly but when I'm getting an error, the process gets stopped. I'm calling it in ViewModel class as:
fun validateList(list: List<Model>): Single<List< Model >> {
return Observable.fromIterable(list)
.toFlowable(BackpressureStrategy.LATEST)
.flatMapSingle { validate() }
.toList()
.map { list.mapIndexed { index, testModel ->
(if (it[index] != null) {
testModel.isVerified = it[index].toString()
} else throw Exception("ERROR")); testModel }
}
.onErrorResumeNext { error -> }
And I'm calling it from fragment as:
private fun verify() {
showLoading(true)
testViewModel.validateList(arrayList)
.doFinally {
showLoading(false)
}
.asyncToUiSingle()
.subscribe({
adjustCard(it)
}, {
it.printStackTrace()
})
.addTo(disposables)
}
TestModel:
data class TestModel(
val title: String,
var isVerified: String? = null,
var reason: String? = null )
Please help me to understand how I can update value of reason field in list if one element gets failed status and continue the validations for other elements.
onErrorResumeNext require return an ObservableSource and it will be invoke directly after error happened:
. onErrorResumeNext { error ->
val newList = // Code for new list with updated data
Observable.just(newList) // The new list will be emit to subscriber
}
In your case, I think you can use retry, it will be continue the chain after error happened with given condition, let try:
var index = 0
val validatedList = mutableListOf<Model>()
return Observable.fromIterable(list)
//...
.map {
for (i in index until list.size) {
val testModel = list[i];
if (testModel != null) {
testModel. isVerified = testModel.toString()
validatedList.add(testModel);
} else {
throw Exception("ERROR");
}
}
validatedList
}
.retry { error ->
error.printStackTrace()
index++
index < list.size
}
That's mean when error happened, the retry will trigger and continue the chain if u didn't in the end of list. But, I don't think that's best practice, you maybe try another way. Hope can help you.
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
}
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
})