I'm trying to wrap my head around how to implement something in RxJava (2.0). It's for Android and I'm using Kotlin, although the choice of platform and language shouldn't matter here.
The idea is that I'd base some sort of MVP architecture on RxJava. In this implementation I'm thinking about an Activity
(could be a Fragment
or a custom View
as well) exposes a stream of values (Boolean
s for simplicity) which indicate lifecycle events, or whether the view is attached or detached.
The underlying idea is basically this:
private val lifecycleEvents = PublishSubject.create<Boolean>()
val screenStates: Observable<Boolean> = lifecycleEvents.hide()
override fun onResume() {
super.onResume()
lifecycleEvents.onNext(true) // I'm attached!
}
override fun onPause() {
lifecycleEvents.onNext(false) // I'm detached!
super.onPause()
}
override fun onDestroy() {
lifecycleEvents.onComplete() // I'm gone
super.onDestroy()
}
And then from the other end, the Presenter exposes an Observable
that is a stream of objects representing screen states - to be rendered by the View.
(This follows the concept explained in this series http://hannesdorfmann.com/android/mosby3-mvi-1 - which boils down to the fact that the Presenter feeds the View with standalone objects encapsulating screen states in their entirety rather than multiple different methods on the View).
And then I'd like to bind these two observable streams so that:
Whenever the View gets detached, input from the Presenter is disregarded (and it's not buffered, so as not to run into any backpressure problems)
However, once the View gets reattached, it picks up the latest state the Presenter emmitted. In other words, only one state instance is to be buffered at most.
It would work as follows (assuming the states are of String
type for simplicity):
val merged: Observable<String> = ???
val attached = true
val disattached = false
screenStates.onNext(attached)
fromPresenter.onNext("state A")
fromPresenter.onNext("state B")
screenStates.onNext(disattached)
fromPresenter.onNext("state C") // this won't survive at the end
fromPresenter.onNext("state D") // this will "override" the previous one.
// as that's the last state from BEFORE the screen is reattached
screenStates.onNext(attached)
// "state D" should be replayed at this point, "state C" is skipped and lost
fromPresenter.onNext("state E")
// what "merged" is supposed to have received at this point:
// "state A", "state B", "state D", "state E"
I'm not sure what the best, idiomatic solution is.
I tried to implement it as an ObservableTransformer
, but I couldn't quite get it right. I believe the transformer should be stateless, whereas my solution gravitated towards explicitly keeping track of what was emmitted and buffering the last element "manually" etc., which feels messy and too imperative, so I suppose it's wrong.
I found https://github.com/akarnokd/RxJava2Extensions/blob/master/src/main/java/hu/akarnokd/rxjava2/operators/FlowableValve.java, but the implementation looks very complex and I can't believe it couldn't be done in a simpler manner (I don't need all the flexibility, I only want something that works for the described usecase).
Any insights would be appreciated, including whether there's something else I should take into consideration still, within the context of Android. Also note that I don't use RxKotlin bindings (I may, I just didn't suppose they should be required here).
EDIT:
Below is my current implementation. As I said, I'm not too happy about it because it's explicitly stateful, and I believe this should be achieved declaratively, leveraging some constructs of RxJava.
I needed to merge two streams of different types, and because combineLatest
nor zip
didn't quite do it, I resorted to a trick, creating a common wrapper for both distinct type of events. It introduces certain overhead again.
sealed class Event
class StateEvent(val state: String): Event()
class LifecycleEvent(val attached: Boolean): Event()
class ValveTransformer(val valve: Observable<Boolean>) : ObservableTransformer<String, String> {
var lastStateEvent: Event? = null
var lastLifecycleEvent = LifecycleEvent(false)
private fun buffer(event: StateEvent) {
lastStateEvent = event
}
private fun buffer(event: LifecycleEvent) {
lastLifecycleEvent = event
}
private fun popLastState(): String {
val bufferedState = (lastStateEvent as StateEvent).state
lastStateEvent = null
return bufferedState
}
override fun apply(upstream: Observable<String>): ObservableSource<String> = Observable
.merge(
upstream.map(::StateEvent).doOnNext { buffer(it) },
valve.distinctUntilChanged().map(::LifecycleEvent).doOnNext { buffer (it) })
.switchMap { when {
it is LifecycleEvent && it.attached && lastStateEvent != null ->
// the screen is attached now, pump the pending state out of the buffer
just(popLastState())
it is StateEvent && lastLifecycleEvent.attached -> just(it.state)
else -> empty<String>()
} }
}