How to refresh parameters of a observable inside retrywhen - android

I`m trying to implement a autoLogin function with retry when and need to change the parameters of the previous observable after the auto login is done.
So i created a function
class TestClass(): Function<Flowable<out Throwable>, Flowable<*>> {
override fun apply(flowable: Flowable<out Throwable>): Flowable<*> {
return flowable
.flatMap {
if (it is HttpException && it.code() == 401 ) {
Timber.d("apply: Doing AutoLogin.")
dataSource
.login(document, password)
.map {
//Auto login done, update user
}
.doOnError({
// login failed
})
.toFlowable()
} else {
Flowable.error(it)
}
}
}
}
And apply it this way:
dataSource.apiCall(user)
.retryWhen(TestClass())
.subscribe()
Tried this way too:
Single.defer {
dataSource.apiCall(user)
.retryWhen(TestClass())
}
.subscribe()
}
The problem is, after the login when the request is retried, the user isn`t updated.
What i`m doing wrong?

Related

Firebase RemoteConfig with RxJava

I am trying to use RxJava with FirebaseRemoteConfig but not sure how to make the two work, tried using Completable but I get an error The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with.
I have this problem where I need to fetch from RemoteConfig then initialize the String variable from MySingleton.class with this latest config to be use later. The said String variable must not be null or empty so the flow would be.
During Splash
Call fetchAndActivate
Listen for both OnSuccess and OnFailure
If success initialize the static String variable with the latest config
If failed try to use old/cached configs
Cache might not exist yet specially on first run so check if the static String variable is empty
If empty show AlertDialog for retry.
If not proceed to main activity.
What I am trying to do is to use RxJava and listen for OnSuccess or OnFailure listeners, which I can probably apply as well when getting just a single document when using Firebase Firestore in the future.
How can I do this?
So far this is what I got
class RemoteConfig {
companion object {
private val remoteConfig: FirebaseRemoteConfig by lazy {
FirebaseRemoteConfig.getInstance()
}
private val remoteConfigSettings: FirebaseRemoteConfigSettings by lazy {
FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(1800)
.build()
}
fun init(context: Context): Completable {
remoteConfig.setConfigSettingsAsync(remoteConfigSettings)
return Completable.create {
fetchConfig(context)
}
}
private fun fetchConfig(context: Context): Completable {
return Completable.create { emitter ->
remoteConfig.fetchAndActivate().addOnSuccessListener {
//Use the latest configuration
assignSource(context)
emitter.onComplete()
}.addOnFailureListener {
//Try to use old configuration instead
assignSource(context)
emitter.onError(it.cause!!)
FirebaseCrashlytics.getInstance().recordException(it)
}
}
}
private fun assignSource(context: Context) {
Singleton.staticVariable=
remoteConfig.getString(context.getString(R.string.key))
}
}
}
Splash activity
Completable.mergeArray(
RemoteConfig.init(this).subscribeOn(Schedulers.io()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(5, TimeUnit.SECONDS)
.subscribe({
proceedToMain()
}, {
if (Singleton.staticVariable.isEmpty())
AlertDialog.Builder(this)
.setMessage(it.message)
.setPositiveButton("Retry"
) { dialog, _ ->
run {
dialog.dismiss()
fetchConfig()
}
}
.setNegativeButton("Exit"
) { dialog, _ ->
dialog.dismiss()
finish()
}
.setCancelable(false)
.show()
else
proceedToMain()
})
Manage to make it work it seems all I need is to use tryOnError since it will automatically handle the case when the emitter get disposed/canceled or no longer available. Instead of Completable using just Observable is okay too.
fun init(context: Context): Observable<Boolean> {
remoteConfig.setConfigSettingsAsync(remoteConfigSettings)
return Observable.create { emitter ->
remoteConfig.fetchAndActivate().addOnSuccessListener {
//Use the latest configuration
assignSource(context)
Log.wtf("CONFIG", "SUCCESS")
emitter.onNext(it)
}.addOnFailureListener {
//Try to use old configuration instead
assignSource(context)
Log.wtf("CONFIG", "FAILED")
emitter.tryOnError(it.cause!!) // Try to throw an error if emitter still available or if the sequence is not cancelled/disposed
FirebaseCrashlytics.getInstance().recordException(it)
}.addOnCompleteListener {
emitter.onComplete()
Log.wtf("CONFIG", "COMPLETE")
}
}
}
Splash
cryptonHadItsChance = RemoteConfig.init(this)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
proceedToMain()
}, {
//Only show dialog if necessary field is not available and activity still running
if (Singleton.staticVariable.isEmpty() && !isFinishing && !isDestroyed)
alertDialog.show()
else
proceedToMain()
})

RxJava how to handle errors differently at different points in a chain

I have a chain of API calls in RxJava, and when one fails I need to abort the chain and handle the error. But each failure needs to be handled differently. I tried this:
netRequestOne()
.onErrorResumeNext {
handleErrorOne()
Single.error(it)
}
.flatMap {
netRequestTwo()
}
.onErrorResumeNext {
handleErrorTwo()
Single.error(it)
}
// more flatMaps with requests...
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
// deal with success
},
{
// no-op
}
)
But if a request throws an exception, all of the subsequent onErrorResumeNext()'s are called, not just the one tied to that request.
You can achieve the requested behavior for example with this:
val requestOne = Single.just("Response 1")
//.doOnSuccess { throw RuntimeException("Fail second request") } // (1)
.doOnError {
println("handleErrorOne")
}
val requestTwo = Single.just(10)
//.doOnSuccess { throw RuntimeException("Fail second request") } // (2)
.doOnError {
println("handleErrorTwo")
}
requestOne
.flatMap { oneResult -> requestTwo.map { twoResult -> Pair(oneResult, twoResult) } }
.doOnSuccess { responses: Pair<String, Int> ->
println(responses)
}
.flatMap { Single.just("More flatMaps") }
.subscribe({}, {})
You can uncomment (1) and/or (2) to simulate a fail in the first or second request. In case, both requests end successfully, responses are combined and you do some other processing.

onCompletion not called while using Koltin Flow with LiveData

So here is what I was trying to do with Flow, I am showing a ProgressBar in onStart and trying to hide the ProgressBar in onCompletion.
In ViewModel class appDatabase.eventDao().getAllEvents() returns Flow<List<EntityEvents>
#ExperimentalCoroutinesApi
val allEvents: LiveData<Outcome<List<Event>>> = _fetchEvents.switchMap { _ ->
appDatabase.eventDao().getAllEvents()
.map { eventListMapper.map(it) }
.map { sortEventsBasedOnPreference(it) }
.flowOn(Dispatchers.IO)
.map { Outcome.success(it) }
.onStart { emitLoading(true) }
.onCompletion { emitLoading(false) }
.catch { emitFailure(it, R.string.err_something_wrong) }
.asLiveData(context = viewModelScope.coroutineContext)
}
All working fine, what I am not able to figure out why is onCompletion not called when the task is completed?
if appDatabase.eventDao().getAllEvents() is based Room on Flow, never called onCompletion().
Why?
Because getAllXXX() Query is 'Hot'.
Actually, query is not completed. Only data is emited.
When the data changes, the query will emit data again.

Pass value from single across completable to result in rxjava Android

This is what I want:
Check if I have data about products in database.
If I have data I run Single to get data from DB.
If not I run Single for get data from backend
If I get response I want to save data in DB using Completable.
After saving data I want to map values from step 2 or 3 to view model
In result I want to send data to activity.
This is what I have now:
checkProductsInDBUseCase.run()
.flatMap {
if (it) {
getProductsFromDBUseCase.run()
} else {
getProductsUseCase.run(3)
}
}.map {
it.products.map { item -> item.toViewModel() }
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onSuccess = {
view.showBikes(it)
},
onError = {
view.showBikesError(it.message.toString())
}
).addTo(disposables)
Between flat map and map I need to run saveDataUseCase(it), but I don't know how to pass itfrom completable to map. Any ideas?
If your saveDataUseCase() is Completable then you can do this
checkProductsInDBUseCase.run()
.flatMap {
if (it) {
getProductsFromDBUseCase.run()
} else {
getProductsUseCase.run(3)
}
}
.faltMap {
saveDataUseCase(it).toSingleDefault(it)
}
.map {
it.products.map { item -> item.toViewModel() }
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onSuccess = {
view.showBikes(it)
},
onError = {
view.showBikesError(it.message.toString())
}
).addTo(disposables)
But if you change return type of saveDataUseCase() to Unit, you can use Fred's answer. It would be better
Here I'd use doOnSuccess. This seems ideal especially because you're creating a side effect, which we usually use the doOnXXX methods for.
checkProductsInDBUseCase.run()
.flatMap {
if (it) {
getProductsFromDBUseCase.run()
} else {
getProductsUseCase.run(3)
}
}
.doOnSuccess {
saveDataUseCase(it)
}
.map {
it.products.map { item -> item.toViewModel() }
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onSuccess = {
view.showBikes(it)
},
onError = {
view.showBikesError(it.message.toString())
}
).addTo(disposables)
The method will not change the result of the flatMap so you will still get the correct object inside the map function.

Poll second URL until JSON returns an expected parameter

I'm using a travel API which I first hit with a request to create a session and then I use the session URL returned from that URL to then call until its status parameter returns UpdatesComplete.
Here's what I have so far:
lateinit var pollUrl: String
travelInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
pollUrl = url
travelInteractor.pollResults(pollUrl)
.retryWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
}
.doOnNext {
if (it.status != "UpdatesComplete") travelInteractor.pollResults(pollUrl)
.retryWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
}
.subscribe({
// Subscription stuff
)}
What's currently happening is that it will call doOnNext() and then it will make the network poll but I won't be capturing the subscription and also won't chain another poll. Is there a more efficient way I can be writing this?
Solution
Thanks to iagreen I managed to achieve this with:
lateinit var pollUrl: String
travelInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
travelInteractor.pollResults(url)
.retryWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
.repeatWhen {
it.delay(1000, TimeUnit.MILLISECONDS)
}
.filter {
it.itineraries.map { ... } // Use response here appropriately and then check status
it.status == "UpdatesComplete"
}
.take(1)
}
.subscribe({
// Subscription stuff
)}
I am assuming your pollResults(url) method returns a Single or an Observable that behaves like a single -- it returns one result and then onComplete. If that is truly the case, you can use repeatWhen to retry the request on success and retryWhen to retry on error. See the code below.
skyScannerInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
skyScannerInteractor.pollResults(url)
.repeatWhen { complete -> complete.delay(1, TimeUnit.SECONDS) }
.retryWhen { errors -> errors.delay(1, TimeUnit.SECONDS) }
.filter({ it.status == "UpdatesComplete" })
.take(1) // Take the first valid value and complete
}
.subscribe({
// Subscription stuff
)}
A little explanation -
repeatWhen/retryWhen will try the request every second.
filter will cause elements with the wrong status to be ignored.
When you get the first status == "UpdatesComplete" element, take(1) will emit that value and complete -- this will have the effect of cancelling the retries.
Note: In the case of an error, retrying a network request forever is usually the wrong thing to do. I recommend you modify the retryWhen above to suit you use case to terminate in the event of network failure. For example, you could retry three times and then propagate the error. See this article for some examples on how you could do that. It is also a good reference on repeatWhen/retryWhen.
retry() and retryWhen() both respond to an onError event in the Observable, that is why it isn't actually retrying; you're not receiving those onError events in your pollResults() Observable. Right now, your retrying code doesn't actually depend on the JSON response.
There are two ways I would think about going about this:
Throw an exception in your pollResults() Observable if the JSON response is unsatisfactory. This should trigger the retryWhen(). You'll need to test for it in the Observable somewhere.
Reorganize your Observable like this:
`
lateinit var pollUrl: String
skyScannerInteractor.createSession("LHR", "AKL", "2018-04-20", "2018-04-22")
.doOnSubscribe {
loading.postValue(true)
}
.flatMap { url ->
pollUrl = url
skyScannerInteractor.pollResults(pollUrl)
}
.doOnNext {
if (it.status != "UpdatesComplete") {
throw IOException("Updates not complete.") //Trigger onError
}
}
.retryWhen { //Retry the Observable (createSession) when onError is called
it.delay(1000, TimeUnit.MILLISECONDS)
}
.subscribe({
// Will give result only when UpdatesComplete
}

Categories

Resources