I have a problem with kotlin observables, I have searched through internet and stackoverflow but I think I am missing something in concepts. I have dashboardRepository which has method called getCallsCountForWeek, this basically returns flowable list for last 7 days and now I need to iterate through all flowables and then update my graph with count of calls user made for that day. Here is my code
fun getCallsCountForWeek(calendar: Calendar) : List<Flowable<Float>> {
val result = ArrayList<Flowable<Float>>()
for(index in 0..6) {
calendar.add(Calendar.DAY_OF_MONTH, -index)
result.add(dashbordDao.getCallsCountForDay(customSharedPreferences.getUser()?.id!!, CustomDateTimeUtil.getStartOfDay(calendar), CustomDateTimeUtil.getEndOfDay(calendar)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()))
}
return result
}
Observable.fromArray(dashboardRepository
.getCallsCountForWeek(calendar). map {
items -> kotlin.run {
items.forEach {
it.subscribe({
Log.e("Result", " Count: " + it)
},{
Log.e("Error", "" + it)
})
}
}
}.doOnComplete {
//We will do this when it is completed
Log.e("Result", "Completed")
}.doFinally {
Log.e("Result", "Finally")
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe()
The problem is that doFinally and doOnComplete are called before map completes its iteration through all the flowables. I tried to use .zip for flowables but apparently could not make it work too.
According to other posts on stack overflow, doOnComplete is called when subscription is successful but I want that to happen after everything is done inside .map.
You should use flatMap or flatMapIterable instead of map and have only one subscribe call
Observable.fromArray(dashboardRepository
.getCallsCountForWeek(calendar)
.flatMapIterable { it } // iterate over list
.flatMap { it } // use flowables from list
.doOnNext { /* do something with every item */ }
.doOnComplete {
//We will do this when it is completed
Log.e("Result", "Completed")
}
.doFinally {
Log.e("Result", "Finally")
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.ignoreElements () // if you already handled everything in the doOnNext
.subscribe()
After looking into answer from Eugene Popovich. I was pointed into right direction and then I did the following and it worked.
So, first thing, I modified my function to return list of Single Observable instead of Flowable as that any who made ore sense. Once done I did following as suggested by Eugene but just using flatMapSingle instead of flatMap.
Observable.fromArray(dashboardRepository.getCallsCountForWeek(calendar))
.flatMapIterable { it } // iterate over list
.flatMapSingle {
it
}
.doOnNext {
barEtries.add( BarEntry(index++, it))
}
.doOnComplete {
//We will do this when it is completed
Log.e("Result", "Completed "+barEtries)
setBarChartData()
}
.doFinally {
Log.e("Result", "Finally")
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.ignoreElements () // if you already handled everything in the doOnNext
.subscribe()
The change in getCallsCountForWeek was as below, basically just used single instead of Flowable because it made more sense and flatMapSingle provided out of the box resolution without calling even subscribe.
fun getCallsCountForWeek(calendar: Calendar) : ArrayList<Single<Float>> {
val result = ArrayList<Single<Float>>()
for(index in 0..6) {
calendar.add(Calendar.DAY_OF_MONTH, -index)
result.add(dashbordDao.getCallsCountForDay(customSharedPreferences.getUser()?.id!!, CustomDateTimeUtil.getStartOfDay(calendar), CustomDateTimeUtil.getEndOfDay(calendar)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()))
}
return result
}
Related
I'm confused about how to implement this in RxJava.
I would like to
take an object from my database
upload it
delete it from the database
take the next item from the database and repeat 2 and 3
complete when the database has no objects remaining
I know how to do this via loading all objects from the database at first and creating an Observable like this Observable.fromIterable(allMyDbObjects), however I would like to take objects one at a time, in case the database is updated while I'm uploading.
I can't figure out how to do this. I've looked at repeatUntil but it just seems to repeat instantly. Here is pseudocode for what I'm thinking:
getFirstDbObject()
.flatMapCompletable { obj ->
upload(obj)
.doOnComplete {
deleteFromDb(obj)
}
}
.repeatUntil {
// dbIsEmptyLogic.
// This doesn't work. I need to somehow call getFirstDbObject() again
}
Can anyone help?
Assuming getFirstDbObject() returns a Maybe, you can achieve this by mapping the result to a boolean (true if the database is empty, false if not) and then repeating the sequence until getFirstDbObject() returns empty and the stream completes.
getFirstDbObject()
.toObservable()
.flatMapSingle { obj ->
upload(obj)
.doOnComplete { deleteFromDb(obj) } // probably better to use .andThen(deleteFromDb(obj)) if delete also returns a completable
.toSingleDefault(false)
}
.defaultIfEmpty(true)
.repeat()
.takeUntil { isComplete ->
isComplete
}
This is a working solution in my code base.
val source = HashSet<String>()
source.add("a")
source.add("b")
source.add("c")
source.add("d")
source.add("e")
io.reactivex.Observable.just(Unit)
.flatMap { it ->
io.reactivex.Observable.fromCallable {
println("first flatmap print $it")
// uploadObj()
source.first()
}
}.flatMap {
// delete
io.reactivex.Observable.fromCallable {
source.remove(it)
println("second flatmap remove $it")
// delete object
}
}
.repeatUntil { source.isEmpty() }
.subscribe()
I want to convert every object in my list to an another object. But in doing so my code stucks in converting them back to List
override fun myFunction(): LiveData<MutableList<MyModel>> {
return mySdk
.getAllElements() // Returns Flowable<List<CustomObject>>
.flatMap { Flowable.fromIterable(it) }
.map {MyModel(it.name!!, it.phoneNumber!!) }
.toList() //Debugger does not enter here
.toFlowable()
.onErrorReturn { Collections.emptyList() }
.subscribeOn(Schedulers.io())
.to { LiveDataReactiveStreams.fromPublisher(it) }
}
Everything is fine until mapping. But debugger does not even stop at toList or any other below toList. How can I solve this?
Using flatMap() you'll only flatten the Flowable of lists to a single Flowable of the elements. Calling toList() on it requires the Flowable to complete and therefore you'll most likely never get there. If you only want to map the elements in the list and have an item with the new list emitted, you should do the mapping within flatMap() or maybe try using concatMap() to keep the order:
...
.concatMapSingle { list ->
Observable.fromIterable(list).map {
MyModel(it.name!!, it.phoneNumber!!)
}.toList()
}
...
Here is my solution to this. Thanks to Tim for leading me to right answer.
override fun myFunction(): LiveData<MutableList<MyModel>> {
return mySdk
.getAllElements() // Returns Flowable<List<CustomObject>>
.flatMapSingle { Observable.fromIterable(it).map { MyModel(it.name!!, it.phoneNumber!!) }.toList() }
.toFlowable()
.onErrorReturn { Collections.emptyList() }
.subscribeOn(Schedulers.io())
.to { LiveDataReactiveStreams.fromPublisher(it) }
}
How take process multithreaded load for the item list. I get from api list string elements. Next need to get data for items this list. Load need to use rxjava. Result need do getting to the single subscribe.
Next need to get data for items this list
What it has to be? Just method inside your class or separate network request for each of strings?
For first case:
getListFromApi()
.toFlowable()
.flatMap { Flowable.fromIterable(it) }
.map { getSomeData(it) }
.toList()
.subscribe()
Second case:
getListFromApi()
.toFlowable()
.flatMap { Flowable.fromIterable(it) }
.map { requestForSomeData(it) }
.toList()
.flatMap { flowablesList -> Single.zip(flowablesList.map { it.firstOrError() }) { it.toList() } }
.subscribe()
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
}
I'm trying to do a simple search UI, where the text change triggers a search in the service and that gets mapped to a ViewState. It would seem easy, but the following code doesn't work:
queryText.filter { it.length > 3 }
.switchMap { service.search(it) }
.onErrorReturn { SearchResponse(null, it.message) }
.map { SearchViewState(items = it.items, error = it.error) }
.startWith { SearchViewState(loading = true) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { viewState.onNext(it) }
I've no idea what I did wrong, but through debugging I can see that the stream throws a NetworkOnMainThreadException and then terminates so new events are no longer processed.
What is the correct way to do this?
I assume queryText is the source of textchanges which happen on the main thread. Therefore subscribeOn has no effect on it. You should apply subscribeOn to the actual network call:
queryText.filter { it.length > 3 }
.switchMap {
service.search(it)
.subscribeOn(Schedulers.io())
.onErrorReturn { SearchResponse(null, it.message) }
.map { SearchViewState(items = it.items, error = it.error) }
.startWith ( SearchViewState(loading = true) )
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { viewState.onNext(it) }
In addition, I think you have to do the error recovery and state changes associated with the particular network call, otherwise a failure will stop the entire sequence.