Catch error but continu iterating the list - android

I have a function in Kotlin which is doing this :
fun getItemByIds(ids: List<Long>): Single<List<Item>> {
return Flowable.fromIterable(ids)
.flatMapSingle { getItem(it) }
.toList()
}
This working well but in case of an error generated by the getItem such as a http error 404, the code is not continuing to iterate.
getItem is returning a Single<> but in case of http error, I return Single.error(it).
What I am looking to do is make sure that the list is still build and in case of an error, I am just dismissing the error and build the list with the item who generate the error.
Any idea ?

There's an Rx operator to solve your problem:
fun getItemByIds(ids: List<Long>): Single<List<Item>> {
return Flowable.fromIterable(ids)
.concatMapEagerDelayError({ id -> getItem(id).toFlowable() }, true).toList()
}
What are the advantages?
You'll get the error at the end (if any), and you can decide what to do with it depending on what it is. Maybe you don't want to do the same thing if you get a NullPointerException for instance instead of an IOException.
You can go granular with the overloaded version of concatMapEagerDelayError specifying things like maxConcurrency.
You could even map your errors using onErrorResumeNext to find out the ids of the items that errored or you could retry / retryWhen to specify your retry strategy.

Related

Arrow.kt loading state

I used arrow.kt library so many times, I really enjoyed the features they gave to extend kotlin. I like how Either<E,T> can represent the success/failed states seamlessly. I am just wondering if arrow.kt has a way to represent loading state along with the other two. In other word, we will have a type that offers three states: Success, Failure, Loading.
I tried to use Option<T> as right member, but None can't represent loading state as required.
Is there any thing I can try?
Either<A, B> represents a single result. You are interested in either the left side or the right side. There's no third.
You could play it in many ways.
For example, consider both Failure and Success as a right result.
sealed interface State {
object Loading: State
sealed interface Result: State {
object Failure: Result
object Success: Result
}
}
//...
getState().map { result ->
when(result) {
Failure -> TODO("Handle error")
Success -> TODO("Handle success")
}
}.mapLeft { loading -> TODO() }
and return Either<Loading, Result>.
Or have Either<Loading, Either<Failure, Success>>
And it is even possible that some kind of subscription to Flow<State> or rx Observable will suit you more than a single result if you expect to receive multiple responses.

Kotlin Flow execute two API calls in parallel and collect each result as it arrives

I am trying to implement cache then network strategy for my API call using Kotlin Flows.
Here is what I am trying right now
flowOf(
remoteDataSource.getDataFromCache() // suspending function returning Flow<Data>
.catch { error -> Timber.e(error) },
remoteDataSource.getDataFromServer() // suspending function returning Flow<Data>
).flattenConcat().collect {
Timber.i("Response Received")
}
Problem here is collect is only called when getDataFromServer returns. My expectation is that I should get first event from cache and then second event from server after a few milliseconds. In this case "Response Received"gets printed twice but immediately one after other.
In this other variant "Response Received" only gets printed once that is after getDataFromServer() returns.
remoteDataSource.getDataFromCache() // suspending function returning Flow<Data>
.catch { error -> Timber.e(error) }
.flatMapConcat {
remoteDataSource.getDataFromServer() // suspending function returning Flow<Data>
}
.collect {
Timber.i("Response Received")
}
I was using RxJava's Flowable.concat() before and it was working perfectly. Is there something in Kotlin Flows which can emulate that behaviour?
Problem here is collect is only called when getDataFromServer returns.
The first problematic thing with your design is that the Flow-returning function is also suspendable. That's two layers of suspendability. Functions should return flows without any delays and the flows themselves should emit items as they come in. If you followed this guideline, your initial code would already work.
The way you wrote these functions, they can still work if you write this:
flow<String> {
emitAll(getCached())
emitAll(getFromServer())
}
This statement completes immediately, returning a cold flow. When you call collect on it, it first calls getCached() and emits the cached value, and then calls getFromServer() and emits the server response.
The above solution starts the server call only after you consume the cached value. If you need the two flows to be active concurrently, use flatMapMerge.
Assuming you fixed the above basic problem and made your Flow-returning functions non-suspending, all you need is this:
flowOf(getCached(), getFromServer()).flattenMerge()
If for some reason you can't do that, you have to add the emitAll wrapper around each call:
flowOf(
flow { emitAll(getCached()) },
flow { emitAll(getFromServer()) }
).flattenMerge()
Recently, merge operator was added to the Kotlin coroutines version 1.3.3. Here is the merged PR.
Using the merge operator, you should be able to get the result as and when it arrives.
Turns out in case of flowOf(someOperation()) someOperation() needs to be completed for downstream to start processing. Its like Observable.just(someOperation()) in RxJava world.
In second scenario flatMapConcat is actually a transform operator so it obviously returns final processed output.
There seems to be lack of native concat like operators in Flow world. This is how I solved this problem in the end
flow {
remoteDataSource.getDataFromCache()
.catch { error -> Timber.e(error) }
.onCompletion {
remoteDataSource.getDataFromServer()
.collect {
emit(it)
}
}.collect { emit(it) }
}

RxJava Single and Room Error handling Problem

I am having this issue regarding empty database return with Room and RxJava Single.
I know that my database is empty, so I am expecting to get an empty return when I trigger
#Query("SELECT * FROM Times WHERE timestamp = :timestamp")
fun getTimes(timestamp: String): Single<Times>
The problem is when I call this function as below
timeDao.getTimes("1398332113")
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnError { Log.e("Single", "Returned null") }
.doOnSuccess { result -> times = result}
.subscribe()
The subscriber is indeed calling doOnError method like
E/Single: Returned null
but still returning an exception and crash
W/System.err: io.reactivex.exceptions.OnErrorNotImplementedException:
Query returned empty result set: SELECT * FROM Times WHERE timestamp =
?
I have seen so many similar questions on StackOverflow, but couldn't find an answer. What am I am doing wrong?
First a solution for your problem. Since version 2.1.0-alpha01 Room supports the Maybe return type which is perfect for modelling your problem.
From the documentation:
The Maybe operates with the following sequential protocol: onSubscribe (onSuccess | onError | onComplete)?
When your the item is in the db: onSuccess will be called with the data. If the db is empty onComplete will be called. onError is self explanatory.
Replacing the Single with Maybe in the Dao class will work.
Further notes:
timeDao.getTimes("1398332113")
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnError { Log.e("Single", "Returned null") }
.doOnSuccess { result -> times = result}
.subscribe()
doOnError
doOnError will execute the the lambda in case of en error, but it will still emit the error down the chain. Your program crashes because it doesn't handle errors (the subscribe() is empty).
You could do subscribe({/* doing nothing for success */}, {e -> Log.e("TAG", "Empty DB")}) to prevent the crash.
default value
If the goal is to return some default value in case the DB is empty then you will want to use onErrorReturn so that the chain continues. Take a look at this blog post for more info.
Null values
The default value can't be null, the Reactive Streams specification (RxJava2 implements it) doesn't support null values.

flatMap() in kotlin

If i make a Single invocation using Retrofit as example in Kotlin, i want to check the resulted answer and keep going with a Single or an error. As example
model.doRequest()
.flatMap { t: Response ->
if(!h.hasError) {
return#flatMap model.doAnotherRequest()
} else {
return#flatmap Single.error<Throwable>(Throwable("error))
}
}
If i make another flatMap(), subscribe() or any other things, RxJava won't know that I want to continue with response given by the doAnowhtRequest(), instead will return Any!. How i can get data given by second request?
In Java, Single.error isn't interpreted so RxJava will continue to give me the response in next invocations.
Assuming you want to return the same thing as doAnotherRequest() the problem is with Single.error<Throwable>(Throwable("error)). You're hitting the compiler that you're returning a Single<Throwable>, but you want to return a Single<whatever doAnotherRequest returns>.
Say your doAnotherRequest returns Single<Foo>, then you'd want to return Single.error<Foo>(Throwable("error)), which will return a single that will emit an error with the given exception.
Kotlin tries to infer the type you want to return, but because you're returning 2 types that the "most common" type is Any, kotlin can only infer it's that you want to return.

RxJava2 Single.Concat for repository pattern

I am using Room with RxJava2 to implement my data layer via Repository Pattern principles.
I have the following simple code which decides where to pick data from.
#Override
public Single<Team> getTeamById(int teamId) {
return Single.
concat(local.getTeamById(teamId),
remote.getTeamById(teamId)).
filter(team -> team != null).
firstOrError();
}
The problem here is that instead of going to the remote source , it returns an error from the first source (local) if the data was not available.
android.arch.persistence.room.EmptyResultSetException: Query returned empty result set: select * from teams where id = ?
How should I instruct the concat to forgo any error that is received and continue its concatenation?
Aslong you're not sure if you can receive at least one Team from you data provider, you should probably think of using Maybe instead of Single.
You can lookup the definition here:
Single as it states:
it always either emits one value or an error notification
Use Maybe instead:
Maybe
there could be 0 or 1 item or an error signalled by some reactive
source
As your error already states there seems to be a problem while extracting results from your query.
Handle your result extraction correctly, so that you check if there are results before trying extracting any. Therefor the Maybe would either return 0 or 1 item, and not throw any error at all when no Team was found.
You cannot pass null in RxJava2. So whenever your local repo is empty you just can't return null in your single. There was a question o stack about handling null objects: Handle null in RxJava2
Also here you can find an article showing you preferred implementation of repository pattern using RxJava2:
https://android.jlelse.eu/rxjava-2-single-concat-sample-for-repository-pattern-1873c456227a
So simplifying - instead of returning null from both local and remote repo pass some sort of "empty" object. That will be useful also in your business logic allowing you to recognize empty set of data.
If you want to continue when the first source errors (instead of completing as empty), you can use onErrorResumeNext instead of concat (I assume both get calls return Observable, adjust as necessary):
return local.getTeamById(teamId)
.onErrorResumeNext(error -> {
if (error instanceof EmptyResultSetException) {
return remote.getTeamById(teamId));
}
return Observable.error(error);
})
.firstOrError();
I used Maybe to solve my Rxjava2 repository pattern problem.
In your case, I would use the following code to sort it out:
//you may need to rewrite your local.getTeamById method
protected Maybe<Team> getTeamById(int teamId) {
Team team = localDataHelper.getTeamById(teamId);
return team != null ? Maybe.just(team) : Maybe.empty();
}
#Override
public Single<Team> getTeamById(int teamId) {
Maybe<Team> cacheObservable = local.getTeamById(teamId);
Maybe<Team> apiCallObservable = remote.getTeamById(teamId).toMaybe();
return Maybe.concat(cacheObservable, apiCallObservable)
.toSingle();
}

Categories

Resources