How to DROP_LATEST with coroutine Flow<T>?

Viewed 728

Please help. I was trying to do:

    2sec     2sec     2sec
------[A]------[B]------[C]------...----------------> InitailFlow
       \        |        | 
        \      drop      drop
         \
     5sec \    5sec        5sec
----------[1]---------[2]---------[3]-----|> AnotherFlow
result: [A1, A2, A3]

So I have InitailFlow which emits a short amount of time (2 seconds) which is then transformed to AnotherFlow which takes longer to finish (15 seconds in total)... I would like to drop the other incoming items emitted by the InitialFlow while AnotherFlow isn't finished...

I've tried:

flow{
    delay(2000)
    emit("A")
    delay(2000)
    emit("B")
    delay(2000)
    emit("C")
}.buffer(0, BufferOverflow.DROP_LATEST)
    .onEach {
       println("Event for $it")
    }
    .flatMapConcat {
       flow {
           delay(5000)
           emit("${it}1")
           delay(5000)
           emit("${it}2")
           delay(5000)
           emit("${it}3")
        }
     }
     .onEach {
         println(it)
     }
     .launchIn(scope)

But for some reason this is the result:

Event for A
A1
A2
A3
Event for B
B1
B2
B3

It still process Event B for some reason even when I have a .buffer(0, BufferOverflow.DROP_LATEST).

Why does it still process Event B?

Is there a way to do this? I expect the output to be only:

Event for A
A1
A2
A3

Thanks in advance.

3 Answers

This should work for you:

fun <T> Flow<T>.dropIfBusy(): Flow<T> = flow {
    coroutineScope {
        val channel = produce(capacity = Channel.RENDEZVOUS) {
            collect { offer(it) }
        }
        channel.consumeEach { emit(it) }
    }
}

This is basically the "naive" buffer implementation from the kotlin docs
The only difference here is that we use channel.offer instead of channel.send When used in conjunction with a RENDEZVOUS Channel, all values that are offered to the channel, while it is suspended, are dropped, creating your desired behavior.

After playing around with @AdrianK's solution for a bit, I actually found a simpler solution using channelFlow. Due to channelFlow currently being experimental API, you have to opt in to use it though.

Like this:

fun <T> Flow<T>.dropIfBusy(): Flow<T> = channelFlow {
    collect { offer(it) }
}.buffer(0)

Everytime emit is called it gets suspended so the buffer operator has no effect.

I'm not sure about the output you're expecting to have but I think you're looking for the conflate operator:

flow{
    delay(2000)
    emit("A")
    delay(2000)
    emit("B")
    delay(2000)
    emit("C")
}
.conflate()
.onEach {
    println("Event for $it")
}
.flatMapConcat {
    flow {
        delay(5000)
        emit("${it}1")
        delay(5000)
        emit("${it}2")
        delay(5000)
        emit("${it}3")
    }
}
.onEach {
    println(it)
}
.launchIn(scope)

The conflate operator official documentation states:

Conflates flow emissions via conflated channel and runs collector in a separate coroutine. The effect of this is that emitter is never suspended due to a slow collector, but collector always gets the most recent value emitted.

You can find the conflate operator official documentation here.

Related