I'm trying to write an observable that would generate repeated events while the user holds down a view. My code below works well, but only the first time (e.g. if the user presses the button again, nothing happens). Can you please advise what am I doing wrong and what is best practice for this?
val touches = RxView.touches(previousButton)
touches
.filter({ event -> event.action == MotionEvent.ACTION_DOWN })
.flatMap({
Observable.interval(500, 50, TimeUnit.MILLISECONDS)
.takeUntil(touches.filter({event -> event.action == MotionEvent.ACTION_UP}))
}).subscribe({ println("down") })
The problem is that the RxView.touches observable cannot exist for more than 1 source. This means when the subscription inside of the flatMap happens it breaks the original subscription used to trigger the flatMap, making it never occur again.
There are two possible ways around this:
Use .publish(...) to share the source of events instead of using touches.
Map the events into a Boolean on/off observable, then switchMap the appropriate actions based on the current value of the observable.
1.
touches.publish { src ->
src.filter(...)
.flatMap {
Observable.interval(...)
.takeUntil(src.filter(...))
}
}
2.
touches.filter {
it.action == MotionEvent.ACTION_DOWN
or it.action == MotionEvent.ACTION_UP
}
.map { it.action == MotionEvent.ACTION_DOWN }
.distinctUntilChanged() // Avoid repeating events
.switchMap { state ->
if (state) {
Observable.interval(...)
} else {
Observable.never()
}
}
Related
So I've been exploring MVI with Kotlin Flows, basically MutableSharedFlow for Events and MutableStateFlow for States. but I have a problem adding logic to control the text changes event emissions, writing this in a human way would be something like that.
Observe text changes and only search for the latest word written
within half of a second.
So if the user removes or adds letters I only take the search for the latest word after half-second passed.
My attempt to achieve this was by writing the following, here I subscribe to events
events.flatMapConcat { it.eventToUsecase() }
.onEach { _states.value = it }
.launchIn(viewModelScope)
And I map each event to a use case using this function:
private fun SearchScreenEvent.eventToUsecase(): Flow<SearchState> {
return when (this) {
is SearchClicked -> searchUsecase(this.query)
is SearchQueryChanged ->
flowOf(this.query)
.debounce(500)
.flatMapConcat { searchUsecase(it) }
}
}
I know that I have to control the event itself, but how to control only the SearchQueryChanged event independently. with RxJava I was using Publish and switchMap operators is there something like this with Flow.
debounce() can take a lambda parameter that determines the latency per item, so I think this will work:
events.debounce {
when (it) {
is SearchClicked -> 0L
is SearchQueryChanged -> 500L
}
}
.flatMapConcat { searchUsecase(it.query) }
.onEach { _states.value = it }
.launchIn(viewModelScope)
#Tenfour04 Solution will work, but it shows a design problem, so I have to take care of the flow logic in two places now, may be more, so I've posted the same question in slack and someone guides me to redesign my flow as follow:
merge( searchCurrentQuery(),
searchWhileTyping(),
updateUiSearchText())
.flatMapMerge { it.toUsecase() }
.onEach { _states.value = it }
.launchIn(viewModelScope)
By seperating events and using merge to add them into the stream, now I could control each Flow of them as I want.
I am using viewpager2 in my application. I enabled auto slide using rxjava2 (observable interval). I want to stop auto slide when user touch the viewpager and after touch finishes start auto slide. But I can not find proper way to detect touch finish event. I tried action_up, but it triggers only when fast touch like click. It's not detect when user touch 2 second and finish touch.
Observable.interval(SLIDER_DELAY, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (!homeAdapter.touchStatus()) {
if (viewPagerSlider.currentItem < homeAdapter.itemCount - 1) {
viewPagerSlider.setCurrentItem(
viewPagerSlider.currentItem + 1,
true
)
} else {
viewPagerSlider.setCurrentItem(0, true)
}
}
}
HomeAdapter
itemView.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> isTouched = true
MotionEvent.ACTION_UP -> isTouched = false
else -> {}
}
true
}
fun touchStatus() = this.isTouched
How can I detect when user finished touch event after some time?
I have a State(Enum) that contains (Good, Non-Critical, Critical) values
So requirement is :
should trigger when state goes in non-critical state.
should trigger when state goes in critical state.
should trigger when state stays in critical state for 15 seconds.
Input :
publishSubject.onNext("Good")
publishSubject.onNext("Critcal")
publishSubject.onNext("Critcal")
publishSubject.onNext("NonCritical")
publishSubject.onNext("Critacal")
publishSubject.onNext("Critical")
publishSubject.onNext("Good")
and so on...
See Code Structure for Reference:
var publishSubject = PublishSubject.create<State>()
publishSubject.onNext(stateObject)
publishSubject
/* Business Logic Required Here ?? */
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
AppLogger.printLog("Trigger Success --> ")
}
Please help,
Thanks in Advance,
You can use distinctUntilChanged() to suppress events that don't change the state. Filter out the normal events using filter().
Use the switchMap() operator to create a new subscription when the state changes. When the state is "critical", use the interval() operator to wait out the 15 seconds. If the state changes in that 15 seconds, switchMap() will unsubscribe and re-subscribe to a new observable.
publishSubject
.distinctUntilChanged()
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.filter( state -> state != State.Normal )
.switchMap( state -> {
if (state == State.Critical) {
return Observable.interval(0, 15, TimeUnit.SECONDS) // Note 1
.map(v -> State.Critical); // Note 2
}
return Observable.just( State.Noncritical );
})
.subscribe( ... );
interval() is given an initial value of 0, causing it to emit a value immediately. After 15 seconds, the next value will be emitted, and so on.
The map() operator turns the Long emitted by interval() into
The first two parts of your requirements should be combined into one. You're asking for the chain to be triggered on NonCritical and Critical events, ergo the chain should not be triggered for Good event. Likewise, you only need to trigger an event if the state is different from a previous event. For this two .filter events should suffice:
var lastKnownState: State = null
publishSubject
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.filter(this::checkStateDiffers) // Check we have a new state
.filter { state -> state != State.Good } // Check event is good
.subscribe {
AppLogger.printLog("Trigger Success --> ")
}
...
private fun checkStateDiffers(val state: State): Boolean {
val isDifferent = state != lastKnownState
if (isDifferent) lastKnownState = state // Update known state if changed
return isDifferent
}
The timeout requirement is a bit trickier. RxJava's timeout() operator gives the option of emitting an error when nothing new has been received for a period of time. However I am assuming that you want to keep listening for events even after you receive a timeout. Likewise, if we just send another Critical event it'll be dropped by the first filter. So in this case I'd recommend a second disposable that just has the job of listening for this timeout.
Disposable timeoutDisp = publishSubject
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.timeout(15, TimeUnit.SECONDS)
.onErrorResumeNext(State.Timeout)
.filter { state -> state == State.Timeout }
.filter { state -> lastKnownState == State.Critical }
.subscribe {
AppLogger.printLog("Timeout Success --> ")
}
Also adjust the checkStateDiffers() to not save this Timeout state in the first chain.
private fun checkStateDiffers(val state: State): Boolean {
if (state == State.Timeout) return true
var isDifferent = state != lastKnownState
if (isDifferent) lastKnownState = state // Update known state if changed
return isDifferent
}
I need to handle multiple button click within short period of time in a way such that I need to get number of click that the user clicked within 500ms to reduce the number of API calls I make to the backend.
val buttonStream = view.plusButton.clicks()
buttonStream
.buffer(buttonStream.debounce(500, TimeUnit.MILLISECONDS))
.map { it.size }
.subscribe({ clicks ->
Log.i(TAG, "Number of clicks: $clicks")
})
I have implemented the above code but it doesn't display anything when I click the button. When I remove .buffer(buttonStream.debounce(500, TimeUnit.MILLISECONDS)) and just add .buffer(500, TimeUnit.MILLISECONDS) Log starts printing every 500ms. Is there any way to get my work done?
Managed to get the expected out with the following code.
val buttonStream = view.plusButton.clicks()
buttonStream
.buffer(1000, TimeUnit.MILLISECONDS)
.map { it.size }
.filter { size -> size > 0 }
.subscribe({ clicks ->
Log.i(TAG, "Number of clicks from the QUICK_ADD: $clicks")
uploadWaterIntake(clicks)
})
Here's a simplified version of what I'm trying to do (using Kotlin and RxJava)
makeServerCall()
.doOnNext {
doStuff(it)
}
//TODO: if it == 0, call asyncOperation() and wait for its callback to fire
//before running the rest of the stream. Otherwise immediately run the rest
//of the stream
.flatMap {
observable1(it)
observable2(it)
Observable.merge(
getSpotSearchObservable(observable1),
getSpotSearchObservable(observable2)
}
.subscribeBy(onNext = {
allDone()
view?
})
How do I squeeze in the call to asyncOperation() and make the rest of the stream wait for its callback to fire, but only when a certain condition is met? This seems like it's probably a trivial operation in Rx, but no obvious solution is coming to mind.
FlatMap it!
.flatMap {
if (it == 0) {
return#flatMap asyncOperation()
.ignoreElements()
.andThen(Observable.just(0))
}
return#flatMap Observable.just(it)
}
.flatMap {
observable1(it)
observable2(it)
Observable.merge(
getSpotSearchObservable(observable1),
getSpotSearchObservable(observable2)
)
}