Flow under the hood: how does it really work
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
Flow. This understanding should help you both with using
Flow on real-life projects, and with implementing your own
Flow processing functions. Have fun :)
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
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.
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
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
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
Presented builder is the most basic way to create a flow. Most of the other functions use it under the hood.
SharedFlowstart a coroutine, so the way they work is different. Their elements' production can work independently of elements consumption.
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
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.
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.