Flow under the hood: how does it really work
Kotlin Coroutines Flow
is much simpler concept what most developers think. It is just a definition of what operations to execute. Similar to a suspending lambda expression (with some extra elements).
In this article, I would like to give you a deep understanding of how Flow
works. We will start with a very simple concepts, and use them to build our own Flow
. Then we will also define map
and filter
for Flow
. This understanding should help you both with using Flow
on real-life projects, and with implementing your own Flow
processing functions. Have fun :)
Understanding Flow
We will start our story with a simple lambda expression. We define it once, and then we can call it whenever we want.
Now we will add a parameter emit
of type (String) -> Unit
. Since this parameter is a function, we can call it on our lambda expression, but also when we call our lambda expression, we need to define a lambda expression for emit
parameter. In our example, when we call f
, we start lambda expression at 1, and when in this lambda expression we call emit
, we start the lambda expression defined when we called f
.
Now we need to make a small modification. We will define a functional interface FlowCollector
with an abstract method named emit
. We will use this interface instead of function type. The trick is, that functional interfaces can be defined with lambda expressions, so f
call can stay the same.
Calling emit
on it
is not convenient. Instead, we will make FlowCollector
a receiver. Thanks to that, inside our lambda expression there is a receiver (this
keyword) of type FlowCollector
. That means we can call this.emit
or just emit
. f
invocation can still stay the same.
Instead of passing around lambda expression, we would prefer to have an object implementing some interface. We will call this interface Flow
, and wrap our definition with an object expression.
This object creation more convenient, let's extract a builder.
The above flow expects elements of type String
. Let's make it generic. We will also use suspend
modifier to make our functions support features related to coroutines.
That is it! This is nearly exactly how Flow
and FlowCollector
interfaces, and flow
builder are implemented. When you call collect
, you invoke your builder lambda expression. When this expression calls emit
, it calls the lambda expression defined next to collect
.
Presented builder is the most basic way to create a flow. Most of the other functions use it under the hood.
Beware!
channelFlow
andSharedFlow
start a coroutine, so the way they work is different. Their elements' production can work independently of elements consumption.
How Flow
processing works
Flow
can be considered as a bit more complicated suspending lambda expression with receiver. Although its power lies in all the functions defined for its creation, processing and observing. Most of them are actually very simple under the hood. Think of the map
function, that transforms each element. It creates a new flow, so we will start with flow
builder. When this flow is started, we need to start the flow it wraps, so inside the builder, we need to call collect
method. Whenever we receive an element, we should transform it and send to the flow we created.
filter
method is similar, but it emits conditionally, when a predicate returns true
.
Most Flow
processing functions are similarly defined.
I must say, it is easier to explain it live, as I do on my workshops. However, I hope it gives you a sense of how those functions work.
Conclusion
Flow
can be considered as a bit more complicated suspending lambda expression with receiver, and its processing functions are just decorating it with new operations. There is no magic here, the way how Flow
and most of its methods are defined is simple and straightforward.
I hope, this article helped you understand how flow works. You can find more in my book Kotlin Coroutines or on my workshop dedicated to that topic.