How to postpone Observable subscription until certain event has been triggered and skip that check on subsequent subscriptions - kotlin

I have an Observable<Item>.
The task is that this observable should emit values only after external event notifies it that it is allowed to (let's say some data should be validated prior to proceeding with emitting Item's). So each subscriber should wait to that event before receiving Item's. But after event has been delivered all subsequent subscriptions should not wait and receive Item's right away ( so publish/connect doesn't work in this case). This also means that itemSubject should not be even subscribed to prior the event.
My solution so far look like so :
var itemSubject = Observable<Item> ....
var triggerSubject = PublishSubject.create<Item>()
...
fun observeItem() : Observable<Item> {
return triggerSubject.concatWith(itemSubject)
}
...
fun notifyTrigger() {
triggerSubject.onComplete()
}
Is there any idiomatic way of doing this in Rx?

Related

Kotlin Flow - Some emitted events not received when collect

I am using MutableStateFlow. My flow type is sealed class with different states (Loading, Success, Error, etc). Initial value of my flow is empty:
private val _updateDepartmentsState = MutableStateFlow<DepartmentFetchState>(DepartmentFetchState.Empty)
In my repository I'm emitting different states. For example :
suspend fun updateDepartments() {
_updateDepartmentsState.emit(DepartmentFetchState.Loading)
try {
remoteDataSource.updateDepartments()
// here some code
_updateDepartmentsState.emit(DepartmentFetchState.Success(data))
} catch(e: NetworkException) {
_updateDepartmentsState.emit(DepartmentFetchState.Error)
}
}
Also in my repository I have read only flow:
val updateDepartmentsState = _updateDepartmentsState.asStateFlow()
In view model I'm collect flow via interactor. My code inside view model:
updateDepartmentsState.emitAll(
interactor
.updateState // state flow (`updateDepartmentsState` ) from repository via interactor
.map { state->
when (state) {
DepartmentFetchState.Loading -> {}
DepartmentFetchState.Error-> {}
...
}
}.also {
interactor.updateDepartments() // call updateDepartments() from repository via interator
}
As I understand from the documentation, after we have completed the collect, we must get the initial value. But it doesn't happen. Moreover, I do not receive state DepartmentFetchState.Loading. I receive only last state - DepartmentFetchState.Success.
But the most interesting thing is that if I re-call the code from the view model (for example, when updating by swipe), then I get the DepartmentFetchState.Loading state, and then the DepartmentFetchState.Success state, as expected.
I don't understand why on the first call, the initial value that I set when initializing the flow and the DepartmentFetchState.Loading state are lost.
Please, help me(
What you have described is the intended purposes of StateFlow.
Moreover, I do not receive state DepartmentFetchState.Loading. I receive only last state - DepartmentFetchState.Success.
This is because StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. And by the time you start collecting the flow your updateDepartments() has already been finished with a DepartmentFetchState.Success. Means that from this moment onward when you collect the flow, the current state is DepartmentFetchState.Success.
And then:
But the most interesting thing is that if I re-call the code from the view model (for example, when updating by swipe), then I get the DepartmentFetchState.Loading state, and then the DepartmentFetchState.Success state, as expected.
When you re-call the code you receive the result as expected, that's because you have been collecting from the flow as your updateDepartments() execute it will emit the current state DepartmentFetchState.Loading, and then DepartmentFetchState.Success respectively.

Should I emit from a coroutine when collecting from a different flow?

I have a use case where I need to trigger on a specific event collected from a flow and restart it when it closes. I also need to emit all of the events to a different flow. My current implementation looks like this:
scope.launch {
val flowToReturn = MutableSharedFlow<Event>()
while (true) {
client
.connect() // returns Flow<Event>
.catch { ... } // ignore errors
.onEach { launch { flowToReturn.emit(it) } } // problem here
.filterIsInstance<Event.Some>()
.collect { someEvent ->
doStuff(someEvent)
}
}
}.start()
The idea is to always reconnect when the client disconnects (collect then returns and a new iteration begins) while having the outer flow lifecycle separate from the inner (connection) one. It being a shared flow with potentially multiple subscribers is a secondary concern.
As the emit documentation states it is not thread-safe. Should I call it from a new coroutine then? My concern is that the emit will suspend if there are no subscribers to the outer flow and I need to run the downstream pipeline regardless.
The MutableSharedFlow.emit() documentation say that it is thread-safe. Maybe you were accidentally looking at FlowCollector.emit(), which is not thread-safe. MutableSharedFlow is a subtype of FlowCollector but promotes emit() to being thread-safe since it's not intended to be used as a Flow builder receiver like a plain FlowCollector. There's no reason to launch a coroutine just to emit to your shared flow.
There's no reason to call start() on a coroutine Job that was created with launch because launch both creates the Job and starts it.
You will need to declare flowToReturn before your launch call to be able to have it in scope to return from this outer function.

Can I build a Kotlin SharedFlow where the consumer dictates replay length?

Question
When instantiatiang a Kotlin MutableSharedFlow<T> class it allows you to specify replay length of n >= 0. All consumers will get n number of events replayed. Is it a good way to extend or wrap MutableSharedFlow so that the consumer dictactes how many (if any) events he/she wants replayed?
Example desired consumer code
flow.collectWithReplay(count = 1) { event -> ... }
Count would of course have to be equal or less than the upper boundary decided by the flow instance.
Rationale
Some times you want to act differently upon events that are old and new. An example is when the event contains one-time information that is irellevant after consumed once (e.g. data for an error dialog). You may still want to know that the last state was an error, but since it is old you don't show a dialog again. You'd then call flow.replayCache.lastOrNull() to get the old and then subscribe to new using .collectWitReplay(0).
Other times you don't want that distinction and then it would be a hassle to do the two calls separately. .collectWithReplay(1) then yields less and prettier code.
Solution attempted
I have made a solution using my own 1-element replay cache, which solves a special case for n=1. It would be trivial to extend to any n - that's not the point, but I dislike a couple of things about it:
a) It doesn't utilize the built in replay mechanism of SharedFlow
b) It's not thread-safe. collectWithReplay might lose an event emitted in between its line 1 and 2
c) Not sure if I lose any performance by losing inline on the collect method signature
open class FlowEventBus<T>() {
private val _flow = MutableSharedFlow<T>(replay = 0)
var latest: T? = null
private set
suspend fun emit(event: T) {
latest = event
_flow.emit(event) // suspends until all subscribers receive the event
}
/** Consumers who only wants events occuring from now on subscribe here */
suspend fun collect(action: suspend (value: T) -> Unit) = _flow.collect(action)
/** Consumers who wants the last event emitted as well as future events subscribe here */
suspend fun collectWithReplay(action: suspend (value: T) -> Unit) {
latest?.let { action(it) } // Replay any cached event
_flow.collect(action) // Listen for new events
}
}
Answer to main question
Here the foundation for a solution based on the suggestion from #tenfour04
val mainFlow = MutableSharedFlow<String>(10)
If consumers want a different replay value, the do this:
val flowForTwo = mainFlow.shareIn(threadPoolScope, SharingStarted.Eagerly, 2)
flowForTwo.collect { }
You'll be creating a new SharedFlow each time you do this though, so performance may suffer.
See working test
Variation: Event bus with zero or one replay
Here is a solution where the flow is wrapped in an event bus and the consumer may decide between replay length of 0 or 1. This solution comes with some race condition quirks when you emit and collect very close in time. Run and understand this failing unit before using in production. I don't know how to fix it, or if it's worth fixing. You might be better off just using a variation of my original idea.
/**
* FlowEventBus where consumer can decide between single replay or no replay when collecting.
* Warning: It has some concurrency issues that is apparent when you run the tests
*/
class FlowEventBus<T> {
private val threadPoolScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val eventsWithSingleReplay = MutableSharedFlow<T>(replay = 1) // private mutable shared flow
private val eventsWithoutReplay = eventsWithSingleReplay.shareIn(threadPoolScope, SharingStarted.Eagerly, replay = 0)
val latest: T?
get() = eventsWithSingleReplay.replayCache.lastOrNull()
/** Emit a new event */
suspend fun emit(event: T) = eventsWithSingleReplay.emit(event)
/** Consumers who only wants events occuring from now on subscribe here */
suspend fun collect(action: suspend (value: T) -> Unit) = eventsWithoutReplay.collect(action)
/** Consumers who wants the last event emitted as well as future events subscribe here */
suspend fun collectWithReplay(action: suspend (value: T) -> Unit) {
eventsWithSingleReplay.collect(action)
}
}

Flow that emits the last value periodically, and when a new value arrives

I want to create a Kotlin coroutines Flow that emits values when
they change, and
periodically emits the last value available, every x duration since the last change or last emit.
You could create a Flow that emits at a regular interval and then just use combine. Each time you'd combine the values, you'd really just be passing along the current value of the original Flow you are interested in.
// This is the main flow you are interested in. This uses
// a Flow builder just as a simple example but this could
// be any kind of Flow, like a (Mutable)StateFlow.
val emitter = flow {
emit("Your data")
// ...
}
// This just serves as a timer.
val timer = flow {
while (currentCoroutineContext().isActive) {
emit(Unit)
delay(500)
}
}
// This will emit whenever either of the Flows emits and
// continues to do so until "emitter" stops emitting.
combine(
emitter,
timer
) { value, ticker ->
// Always just return the value of your
// main Flow.
value
}
This seems to work -- every time a new value arrives, transformLatest cancels any previous lambdas and starts a new one. So this approach emits, and then continues to emit periodically until a new value arrives.
flow.transformLatest { value ->
while(currentCoroutineContext().isActive) {
emit(value)
delay(x)
}
}

PublishSubject `subscribeOn` behavior

Why is the subscribe never printing anything here? Just out of curiosity. This is bad practice anyways: I would normally use observeOn instead. However, I can't figure out why the subscribe is never reached...
val subject: PublishSubject<Int> = PublishSubject.create()
val countDownLatch = CountDownLatch(1)
subject
.map { it + 1 }
.subscribeOn(Schedulers.computation())
.subscribe {
println(Thread.currentThread().name)
countDownLatch.countDown()
}
subject.onNext(1)
countDownLatch.await()
Why this happens
In the process of subscribing, an observer signals its readiness to receive items to the observable via a Subscribe notification. See the Observable contract for details.
Furthermore, the Subject documentation states:
Note that a PublishSubject may begin emitting items immediately upon creation (unless you have taken steps to prevent this), and so there is a risk that one or more items may be lost between the time the Subject is created and the observer subscribes to it.
When you call subject.onNext(_) immediately after attempting to subscribe on a new thread via .subscribeOn(Schedulers.computation()), the observable (i.e. subject) may still be waiting for a Subscribe notification from the observer. For example:
subject
.subscribeOn(Schedulers.computation())
.subscribe { println("received item") }
// this usually prints nothing!
subject.onNext(1)
However, if you add a bit of a time delay before you emit your first item, the observable is much more likely to receive the Subscribe notification from the observer before you call subject.onNext(_). For example:
subject
.subscribeOn(Schedulers.computation())
.subscribe { println("received item") }
// wait for subscription to be established properly
Thread.sleep(1000)
// this usually prints "received item"
subject.onNext(1)
What to do?
If you want all your subscriptions to receive all the items emitted by an observable, you can do one of the following:
Block the main thread to wait until all observers are subscribed before you call subject.onNext(_).
Create a new observable that waits until all observables are subscribed before calling subject.onNext(_) inside itself.
These might also be of use:
ReplaySubject: This allows you to store a history of all previous items, and re-emit them on each subscription. Downside: you need to store some arbitrary number of items in memory.
ConnectableObservable: This ensures that an observable only emits items after .connect() is called. In particular, the .autoConnect(n) operator ensures that the observable only emits after n observers have successfully subscribed.
Example: blocking the main thread until subscribed
val subject: PublishSubject<Int> = PublishSubject.create()
val countDownLatch = CountDownLatch(1)
val isSubscribedLatch = CountDownLatch(1)
subject
.subscribeOn(Schedulers.computation())
.doOnSubscribe { isSubscribedLatch.countDown() }
.map { it + 1 }
.subscribe {
countDownLatch.countDown()
println(Thread.currentThread().name)
}
isSubscribedLatch.await()
subject.onNext(1)
countDownLatch.await()