I am a beginner with rxjava/rxkotlin/rxandroid.
I need to deal with three diferent async-calls in a sequence.
The problem is that the first step returns a Single<LocationResult>, the second a Completableand the third again a Completable.
(Single -> Completable -> Completable)
The problem is now that the last Completable depends on the data of the first Single
My current solution:
I think this is a bad solution, but I don't know how to do this right.
val ft = FenceTransaction(applicationContext, apiClient)
stream
.flatMap { locationResult ->
ft.removeAll()
return#flatMap ft.commit().toSingle({ return#toSingle locationResult })
}
.flatMapCompletable {
ft.recycle()
ft.setZone(it.location.longitude, it.location.latitude, ZONE_RADIUS)
val dots = DotFilter().getFilteredDots()
for (dot in dots) {
ft.addDot(dot)
}
return#flatMapCompletable ft.commit()
}
.subscribeBy(
onComplete = {
"transaction complete".logi(this)
},
onError = {
"transaction error".logi(this)
})
Is this approch the correct way to do it?
And how should I dispose the Completeables?
Generally when should I dispose Observables?
No idea if you still have this issue but generally for Single->Completable->Completable you'd do:
val disposable = service.getSingleResult()
.flatMapCompletable { locationResult ->
callReturningCompletable(locationResult)
}
.andThen { finalCompletableCall() }
.subscribe({...}, {...})
Related
For start I must say I am begginer in RxJava.
Data class:
#Entity(tableName = "google_book")
data class GoogleBook (
#PrimaryKey(autoGenerate = true) val id: Int=0,
val items: ArrayList<VolumeInfo>)
data class VolumeInfo(val volumeInfo: BookInfo){
data class BookInfo(val title: String, val publisher: String, val description: String, val imageLinks: ImageLinks?)
data class ImageLinks(val smallThumbnail: String?)
}
Function which helps me save data to database:
fun searchBooks(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
bookRepository.getBooksFromApi(query)
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { x ->
x?.let { googleBook ->
searchJob?.cancel()
searchJob = viewModelScope.launch {
bookRepository.deleteGoogleBook()
bookRepository.insertGoogleBook(googleBook)
}
} ?: kotlin.run {
Log.d(TAG, "observeTasks: Error")
}
}
}
}
}
As seen I want to filter list within GoogleBook object by image parameter but It doesnt work. I cannot add filtering for data class ImageLinks so I have no Idea how can I make it right
I am asking mostly about this part:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null }
}
t
}
Thanks for reading
welcome to RxJava, you gonna love it.
As far as I can tell the issue with your filtering simply relies here:
.map { t ->
t.items.map {
it.volumeInfo.imageLinks?.smallThumbnail?.filter { x -> x != null })
} // this returns you a new list filtered list here, but does not modify the original one
t // but you return the same data object here, it is not modified at all
}
// also consider naming it bookInfo if it is actually a bookInfo
What you should do is make a copy of your object with the filtered elements, something like this:
fun filterGoogleBookBySmallThumbNail(googleBook: GoogleBook): GoogleBook {
val filteredItems = googleBook.items.filter { it.volumeInfo.imageLinks?.smallThumbnail == null }
return googleBook.copy(items = ArrayList(filteredItems)) // now a new googleBook item is created with the filtered elements
}
// snippet to adjust then
bookRepository.getBooksFromApi(query)
.map { googleBook -> filterGoogleBookBySmallThumbNail(googleBook) }
//...
Some additional notes / suggestions I have:
I don't see you actually disposing of the subscription of the Observable.
bookRepository.getBooksFromApi(query) If this line returns an Observable, even if you cancel the job, you will be still observing that Observable. If it returns a Single then you are in luck, because after one element it is disposed.
To properly dispose, in cancellation you would have to do something like this(still i would recommend the other two rather, just wanted to note the not disposing):
searchJob = viewModelScope.launch {
val text = query.trim()
if (text.isNotEmpty()) {
val disposable = bookRepository.getBooksFromApi(query)
//...
.subscribe { x ->
//...
}
try {
awaitCancellation() // this actually suspends the coroutine until it is cancelled
} catch (cancellableException: CancellationException) {
disposable.dispose() // this disposes the observable subscription
// that way the coroutine stays alive as long as it's not cancelled, and at that point it actually cleans up the Rx Subscription
}
Seems wasteful that you start a new coroutine job just to do actions
If you want to go the Rx way, you could make the
bookRepository.deleteGoogleBook() and bookRepository.insertGoogleBook(googleBook) Completable, and setup the observable as:
bookRepository.getBooksFromApi(query)
//..
.flatMap {
bookRepository.deleteGoogleBook().andThen(bookRepository.insertGoogleBook(it)).andThen(Observable.just(it))
}
//..subscribeOn
.subscribe()
Seems weird you are mixing coroutine and RX this way
if you don't want to go full Rx, you may consider converting your Observable into a kotlin coroutine Flow, that would be easier to handle with coroutine cancellations and calling suspend functions.
I hope it's helpful
Problem
The issue with reactive programming patterns for one-time events is that they may be re-emitted to the subscriber after the initial one-time event has occurred.
For LiveData the SingleLiveEvent provides a solution using an EventObserver which may also be applied to Kotlin Flow.
Question
Can an AsyncSubject observable be created to handle the case of the SingleLiveEvent in RxJava? The main issue seems to be if there a way for an AsyncSubject to be manually "re-opened" to re-emit data after onComplete is called?
Potential solution
AsyncSubject seems like a potential solution for RxJava, without creating an EventObserver, as the documentation states that it will only publish it when the sequence is completed.
Implementation - Loading status sample
A loading boolean is emitted from the ViewModel method initFeed and view effect state to the view, a fragment in this case. The loading boolean works as expected on the initialization of the fragment and ViewModel sending true via onNext, and completing with onComplete on either a successful or erroneous attempt.
However, the attempt to re-emit a value fails when for example a swipe to refresh initiates the same initFeed method. It seems that onNext cannot be used after onComplete is called for the same object.
SomeViewEffect.kt
data class _FeedViewEffect(
val _isLoading: AsyncSubject<Boolean> = AsyncSubject.create(),
)
data class FeedViewEffect(private val _viewEffect: _FeedViewEffect) {
val isLoading: AsyncSubject<Boolean> = _viewEffect._isLoading
}
SomeViewModel.kt
private fun initFeed(toRetry: Boolean) {
val disposable = feedRepository.initFeed(pagedListBoundaryCallback(toRetry))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { results ->
when (results.status) {
LOADING -> {
Log.v(LOG_TAG, "initFeed ${LOADING.name}")
_viewEffect._isLoading.onNext(true)
}
SUCCESS -> {
Log.v(LOG_TAG, "initFeed ${SUCCESS.name}")
_viewEffect._isLoading.onNext(false)
_viewEffect._isLoading.onComplete()
_viewState._feed.onNext(results.data)
}
ERROR -> {
Log.v(LOG_TAG, "initFeed ${ERROR.name}")
_viewEffect._isLoading.onNext(false)
_viewEffect._isLoading.onComplete()
_viewEffect._isError.onNext(true)
}
}
}
disposables.add(disposable)
}
SomeFragment.kt
private fun initViewEffects() {
val isLoadingDisposable = viewModel.viewEffect.isLoading
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Log.v(LOG_TAG, "Error loading isLoading") }
.subscribe { isLoading ->
if (isLoading) progressBar.visibility = VISIBLE
else {
progressBar.visibility = GONE
swipeToRefresh.isRefreshing = false
}
}
compositeDisposable.addAll(isLoadingDisposable, isErrorDisposable)
}
It is not very clear why you need AsyncSubject which emits only last event. Did you try to use Behavior or Publish Processor for this situation?
Use an Event Wrapper
An AsyncSubject does not seem to be a suitable solution to handle one-time occurrence emissions from an Observable to a Subscriber. After onComplete is called an AsyncSubject can not "re-open" to emit future one-time events.
Using an event wrapper such as an Event, as outlined in LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) is the best approach.
FeedViewEffect.kt
data class _FeedViewEffect(
val _isLoading: BehaviorSubject<Event<Boolean>> = BehaviorSubject.create()
)
data class FeedViewEffect(private val _viewEffect: _FeedViewEffect) {
val isLoading: BehaviorSubject<Event<Boolean>> = _viewEffect._isLoading
}
FeedViewModel.kt
private fun initFeed(toRetry: Boolean) {
val disposable = feedRepository.initFeed(pagedListBoundaryCallback(toRetry))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { results ->
when (results.status) {
LOADING -> _viewEffect._isLoading.onNext(Event(true))
SUCCESS -> _viewEffect._isLoading.onNext(Event(false))
ERROR -> _viewEffect._isLoading.onNext(Event(false))
}
}
disposables.add(disposable)
}
FeedFragment.kt
#ExperimentalCoroutinesApi
private fun initViewEffects() {
val isLoadingDisposable = viewModel.viewEffect.isLoading
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError { Log.v(LOG_TAG, "Error loading isLoading") }
.subscribe { isLoading ->
if (isLoading.getContentIfNotHandled() == true) {
progressBar.visibility = VISIBLE
} else {
progressBar.visibility = GONE
swipeToRefresh.isRefreshing = false
}
}
compositeDisposable.addAll(isLoadingDisposable)
}
I am making 2 RX calls that are nested within each other and are co-dependent. There is an issue with the server (which cannot be solved right now for various reasons) which returns errors in the 2nd nested call.
Until this gets solved, I need to have it that if the 2nd call returns an error, the results of the first call are discarded as well. Right now, the entire iterative process stops the moment any of these error responses occur, and so my goal is to skip over them.
Here is what my call structure currently looks like:
fun getAllDynamicUtterances(module: String) {
var uttList: ArrayList<DynamicUtterance>? = ArrayList()
rxSubs?.add(
repository.getDynamicUtterances(module).map{res ->
res.uttSets.forEach {utt ->
utt.module = res.module!!
utt.transferInputValues()
utt.generateDefaultFlatTree()
uttList?.add(utt)
insertDynamicUtterance(utt)
repository.updateDynamicUtteranceView(utt).blockingForEach {
utt.assignSelectionStrings(it)
repository.storeDynamicUttPieces(utt.inputUttPieces)
utt.uttLinearisations = it.linearisations
updateDynamicUtterance(utt)
}
}
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe ({
allDynamicUtterances?.postValue(uttList)
},{
Log.e("errorHandle",it.toString())
})
)
}
My thinking is to include an if statement that almost does a "peek" of the second call before proceeding, but I'm not sure how to go about it. This is what I came up with so far to give an idea of my thinking:
fun getAllDynamicUtterances(module: String) {
var uttList: ArrayList<DynamicUtterance>? = ArrayList()
rxSubs?.add(
repository.getDynamicUtterances(module).map{res ->
res.uttSets.forEach {utt ->
utt.module = res.module!!
utt.transferInputValues()
utt.generateDefaultFlatTree()
if (doesNotReturnError(utt)){ // <- add this
uttList?.add(utt)
insertDynamicUtterance(utt)
repository.updateDynamicUtteranceView(utt).blockingForEach {
utt.assignSelectionStrings(it)
repository.storeDynamicUttPieces(utt.inputUttPieces)
utt.uttLinearisations = it.linearisations
updateDynamicUtterance(utt)
}
}
}
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).subscribe ({
allDynamicUtterances?.postValue(uttList)
},{
Log.e("errorHandle",it.toString())
})
)
}
and then adding this function, or a function that performs what it is im trying to achieve in any case.
private fun doesNotReturnError(utt: DynamicUtterance): Boolean{
rxSubs?.add(
repository.updateDynamicUtteranceView(utt).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
//cant put a return here :(
}, {
Timber.e(it)
})
)
//returning over here will return nothing wont it?
}
I welcome comments on how to improve my getAllDynamicUtterances function
Sounds like a job for flatMap instead of map.
repository.getDynamicUtterances(module).flatMap{res ->
res.uttSets.forEach {utt ->
utt.module = res.module!!
utt.transferInputValues()
utt.generateDefaultFlatTree()
return if (doesNotReturnError(utt)){ // <- add this
uttList?.add(utt)
insertDynamicUtterance(utt)
repository.updateDynamicUtteranceView(utt).doOnNext {
utt.assignSelectionStrings(it)
repository.storeDynamicUttPieces(utt.inputUttPieces)
utt.uttLinearisations = it.linearisations
updateDynamicUtterance(utt)
}
} else {
Observable.error(utt.getError())
}
}
I'm following Kaushik Gopal's implementation of MVI with a ViewStates Observable and a ViewEffects Observable. Example can be seen here: https://github.com/kaushikgopal/movies-usf/blob/master/app/src/main/java/co/kaush/msusf/movies/MSMovieVm.kt
Relevant snippet:
init {
eventEmitter
.doOnNext { Timber.d("[Event]: $it") }
.eventToResult()
.doOnNext { Timber.d("[Result]: $it") }
.share()
.also { result ->
viewStates = result
.resultToViewState()
.doOnNext { Timber.d("[ViewState]: $it") }
.replay(1)
.autoConnect(1) { disposable = it }
.distinctUntilChanged()
viewEffects = result
.resultToViewEffect()
.doOnNext { Timber.d("[ViewEffect]: $it") }
}
}
The major difference is that I'm using Fragments instead of Activities. Everything works the first time around, but when I go from Fragment A to Fragment B, then back to Fragment A, the result observable triggers new ViewStates, but no new ViewEffects. Is there a special way I should be subscribing/unsubscribing or am I constructing the rx chain in a strange way? I currently subscribe to both the ViewStates and ViewEffects Observables in onViewCreated() and dispose in onViewDestroyed(). I've verified that both Observables are subscribed to in each case, it's just that when I come back to Fragment A, and input new ViewEvents, I get new ViewStates but not ViewEffects. Maybe it's a multi-casting issue?
I have a simple case i'm trying to do with rxJava 2 but i'm stuck.
I have a locationProvider which returns a Maybe (some model with LatLng). Each time a user clicks on the button, I want to get his current location and save the location to the db.
Just to be clear,
The RxView.clicks(save_btn) should be on the MainThread, the location and db save should be on an async thread, and response should come back to mainThread.
How should I do this?
addDisposeable(RxView.clicks(save_btn)
.flatMap { locationProvider.getLastKnowLocation().toObservable() }
.map {
val place = Place(UUID.randomUUID().toString(), "test-address", it.latLng)
db.placeDao().insertAll(place)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Toast.makeText(this#AddPlaceActivity, "saved", Toast.LENGTH_SHORT).show()
}, { throwable -> Timber.e(throwable) }))
I've also tried to simplify it to this:
RxView.clicks(save_btn).share()
.flatMap { locationProvider.getLastKnowLocation().toObservable() }
.subscribe({
val place = Place(UUID.randomUUID().toString(), "test-address", it.latLng)
db.placeDao().insertAll(place)
Timber.d("place-saved")
}, { throwable -> Timber.e(throwable) }))
But this code only execute for the first click and doesn't continue after.
Is it due to the Maybe.toObservable()? how should it be resolved?
Thanks for your help.
This is the locationProvider's code, i'm using RxLocation:
fun getLastKnowLocation(): Maybe<LocationData> {
return rxLocation.location()
.lastLocation()
.map {
val address = geocoder.getFromLocation(it.latitude, it.longitude, 1).first()
val latlng = LatLng(it.latitude, it.longitude)
LocationData(latlng, address)
}
}
Unfortunately,the user who set me in the correct path deleted his answer :(
This is what I ended up with
addDisposeable(
RxView.clicks(save_btn).share()
.observeOn(Schedulers.computation())
.withLatestFrom(locationProvider.getLastKnowLocation().toObservable(), BiFunction { _:Any, l:LocationData -> l })
.map {
val place = Place(UUID.randomUUID().toString(), "test-address", it.latLng)
db.placeDao().insertAll(place)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Timber.d("place-saved")
}, { throwable -> Timber.e(throwable) }))
I couldn't find a nicer way for the 'BiFunction' part, if anyone has something nicer instead.
Also note that you need to call observeOn twice for the thread switching part.