0

My understanding is that conflate() emits only the latest value when applied to a flow. How is the "latest value" determined since the underlying flow is a hot observable?

At any point, it can't know if more emissions are coming but it doesn't just withhold from emitting. I understand that there's a buffer involved but still unclear as to how it can determine what is the "latest value"

I tried reading the Kotlin documentation which gives this example:

val flow = flow {
    for (i in 1..30) {
        delay(100)
        emit(i)
    }
}
val result = flow.conflate().onEach { delay(1000) }.toList()
assertEquals(listOf(1, 10, 20, 30), result)

This seems to suggest that when the collection happens, conflate() simply takes the latest emission out of all (buffered) emitted events which makes sense. However, typically the collection is never delayed and it says that collection is triggered whenever an emission happens - this seems contradictory?

3 Answers3

2

I'm not sure if I understand you correctly, but your confusion seems to originate from the assumption that the producer "pushes" items to the consumer. This is not exactly how it works.

Consumer asks the flow for a next item, and if the producer produced 2 items since the last request, the consumer will get only the last one. Flow doesn't guess if there will be further emissions. It simply provides whatever is the latest at the time the consumer asks for a next item.

broot
  • 21,588
  • 3
  • 30
  • 35
  • What I'm confused about is actually what affects the consumer's speed of asking for the latest item? For example, in the context of a Composable that collects a state from the Flow - when does it actually collect - does it try to consume items every fixed number of ms? – kenkaizeng Aug 24 '23 at 08:12
  • 1
    It tries to consume a next item as soon as it finishes consuming the last one. We shouldn't assume producing of items is always slower than consuming. If that would be the case, we wouldn't at all need flows. What if the consumer sends a network request for each received item? – broot Aug 24 '23 at 09:00
  • I see, that makes sense. Thanks for the different perspective. I think I was stuck on the "pushing" model. – kenkaizeng Aug 24 '23 at 18:22
  • @kenkaizeng Such thinking is not entirely incorrect. For example if we create a flow using `flow {}` and then we collect it, then I believe `collect()` underneath invokes the lambda passed to `flow()` and whenever this lambda invokes `emit()`, then underneath it invokes the lambda passed to `collect()`. So in this case we can say the producer simply calls the collector. But this is only one possible execution pattern. If we use `conflate()`, both the producer and consumer start to run independently - both of them can emit/collect whenever they are ready to. – broot Aug 24 '23 at 18:55
  • I see, are there any other kind of models for other operators? I was trying to get a general understanding of Flows – kenkaizeng Aug 24 '23 at 19:48
1

The key line in the docs here is where it says that the "emitter is never suspended due to a slow collector."

If the collector is running fast enough to keep pace with the emitter, it will see all the emitted elements.

But if the collector is taking a long time to process values, the emitter might emit several more elements before the collector is done with the previous one. In that case, when the collector becomes ready to collect a new value, it will only see the most recent one.

In this scenario, "latest value" means "the most recent of all the values that were emitted before the collector became ready".

Sam
  • 8,330
  • 2
  • 26
  • 51
  • This is consistent with my understanding of the collection and conflate() but, what affects the speed of the collection - that is where I have a gap in understanding – kenkaizeng Aug 24 '23 at 08:10
0

For anyone struggling with a similar understanding, reading the official documentation of buffer() did wonders.

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/buffer.html

To summarize, when buffer() is used, a separate coroutine is started for the operations after buffer() and the original coroutine runs the operators before buffer().

In normal scenarios, the flow is sequential and with each emission, the collector runs its code before the next emission happens. buffer() allows the operators before buffer() to run concurrently with the operators after buffer(). With buffer() or conflate() (a variant of buffer with a strategy of LATEST), the collection comes after buffer() and the collector is run in parallel to the producer and thus there could have been many emissions since the collection occurred.