Kotlin Flow API provides several builder functions for creating flows. These include flow
, flowOf
, asFlow
, channelFlow
, and callbackFlow
. In this article, we'll take a closer look at each of these flow builders, including their syntax, usage, and when to use them. We'll provide detailed explanations of the code examples, compare emit
vs send
, and discuss which builder to use in different situations, especially when dealing with APIs.
Conveyor Belt
We are using Conveyor Belt as an analogy to help us better understand the topics. A conveyor belt system is a device that moves materials from one place to another.
What is a Flow?
Before we dive into the flow builders, let's first understand what a flow is.
A flow is a sequence of values that are emitted over time. You can think of it like a conveyor belt that carries items (values) from one end to the other. Just like you can use your hands to pick up items from a conveyor belt, you can use operators to collect values from a flow.
Flows are particularly useful when dealing with asynchronous data, such as data that comes from an API call or a database query. They allow you to handle the data as it becomes available, without blocking the main thread of your app.
For more detailed information on the Flows
, check out our article on Introduction to Kotlin Flows.
The flow Builder
The flow
builder function is used to create a flow from scratch. It takes a lambda function as an argument and returns a flow that emits the values specified inside the lambda function.
Here's an example that demonstrates how to use the flow
builder:
val myFlow = flow {
emit(1)
delay(1000)
emit(2)
delay(1000)
emit(3)
}
myFlow.collect { value ->
println(value)
}
This code creates a flow using the flow
builder function. Inside the lambda function, we use the emit
function to emit three values with a delay of 1 second between each value. Then we use the collect
operator to collect these values.
The output of this code will be:
1
(delay of 1 second)
2
(delay of 1 second)
3
As you can see, the flow
builder allows us to create a flow that emits multiple values over time.
You can think of the flow
builder like building a conveyor belt from scratch. You start with an empty conveyor belt and add items (values) to it one by one until you have a conveyor belt full of items.
The flowOf Builder
The flowOf
builder function is used to create a flow from a fixed set of values. It takes one or more values as arguments and returns a flow that emits these values.
Here's an example that demonstrates how to use the flowOf
builder:
val myFlow = flowOf(1, 2, 3)
myFlow.collect { value ->
println(value)
}
This code creates a flow using the flowOf
builder function. We pass three values as arguments to the function, and it returns a flow that emits these values. Then we use the collect
operator to collect these values.
The output of this code will be:
1
2
3
As you can see, the flowOf
builder allows us to create a flow that emits a fixed set of values.
You can think of the flowOf
builder like loading items (values) onto a conveyor belt all at once. The conveyor belt starts moving and carries the items along with it.
The asFlow Builder
The asFlow
builder function is used to create a flow from an iterable or sequence. It's an extension function that can be called on any iterable or sequence to convert it into a flow.
Here's an example that demonstrates how to use the asFlow
builder:
val myList = listOf(1, 2, 3)
val myFlow = myList.asFlow()
myFlow.collect { value ->
println(value)
}
This code creates a list of three values and then uses the asFlow
extension function to convert it into a flow. The resulting flow emits the same values as the original list. Then we use the collect
operator to collect these values.
The output of this code will be:
1
2
3
As you can see, the asFlow
builder allows us to create a flow from an iterable or sequence.
You can think of the asFlow
builder like transferring items (values) from one conveyor belt (iterable or sequence) onto another conveyor belt (flow). The items move from one conveyor belt onto another and continue their journey.
Channel Operations
A channel is a non-blocking primitive for communication between coroutines. It allows one coroutine to send values to another coroutine, which can receive these values and process them.
Channels provide several operations for sending and receiving values, including send
, receive
, offer
, and poll
. These operations allow you to send values to a channel, receive values from a channel, or check if a channel has any values available.
Here's an example that demonstrates how to use channel operations in real coding:
val myChannel = Channel<Int>()
GlobalScope.launch {
myChannel.send(1)
delay(1000)
myChannel.send(2)
delay(1000)
myChannel.send(3)
}
GlobalScope.launch {
repeat(3) {
val value = myChannel.receive()
println(value)
}
}
This code creates a channel that can hold integer values. Then we use two coroutines to send and receive values from the channel.
In the first coroutine, we use the send
function to send three values to the channel with a delay of 1 second between each value. The send
function suspends the coroutine until the value is received by another coroutine.
In the second coroutine, we use the receive
function to receive three values from the channel. The receive
function suspends the coroutine until a value is available in the channel.
The output of this code will be:
1
(delay of 1 second)
2
(delay of 1 second)
3
As you can see, we were able to use channel operations to send and receive values between two coroutines.
In summary, channels provide several operations for sending and receiving values between coroutines. These operations allow you to build complex communication patterns between coroutines in your code.
The channelFlow Builder
The channelFlow
builder function is used to create a flow that emits values based on channel operations. It takes a lambda function as an argument and returns a flow that emits values sent to a channel inside the lambda function.
Here's an example that demonstrates how to use the channelFlow
builder:
val myFlow = channelFlow {
send(1)
delay(1000)
send(2)
delay(1000)
send(3)
}
myFlow.collect { value ->
println(value)
}
This code creates a flow using the channelFlow
builder function. Inside the lambda function, we use the send
function to send three values to a channel with a delay of 1 second between each value. Then we use the collect
operator to collect these values.
The output of this code will be:
1
(delay of 1 second)
2
(delay of 1 second)
3
As you can see, the channelFlow
builder allows us to create a flow that emits values based on channel operations.
You can think of the channelFlow
builder like building a chute that connects two conveyor belts. You can use the chute to transfer items (values) from one conveyor belt (channel) onto another conveyor belt (flow).
Let's take a closer look at what's happening in this code.
First, we create a flow using the channelFlow
builder function. This function takes a lambda function as an argument and returns a flow that emits values sent to a channel inside the lambda function.
Inside the lambda function, we use the send
function to send three values to a channel with a delay of 1 second between each value. The send
function is similar to the emit
function used with other flow builders, but it's used specifically with channels.
After creating the flow, we use the collect
operator to collect the values emitted by the flow. This works just like with any other flow, and we can use any of the available flow operators to manipulate or transform the emitted values.
In the case of a channelFlow
, the collect
operator works similarly to the receive
method of a regular Channel
. Both allow you to receive values that have been sent to a channel.
The main difference between the two is that collect
is an operator used with flows, while receive
is a method used with channels. This means that when using collect
with a channelFlow
, you can take advantage of the other flow operators to manipulate or transform the emitted values.
Here's an example that demonstrates how to use the collect
operator with a channelFlow
:
val myFlow = channelFlow {
send(1)
delay(1000)
send(2)
delay(1000)
send(3)
}
myFlow
.map { it * 2 }
.collect { value ->
println(value)
}
This code creates a flow using the channelFlow
builder function. Inside the lambda function (the part between {}
), we use the send
function to send three values to a channel with a delay of 1 second between each value.
After creating the flow, we use the map
operator to transform the emitted values by multiplying them by 2. Then we use the collect
operator to collect these transformed values.
The output of this code will be:
2
(delay of 1 second)
4
(delay of 1 second)
6
As you can see, we were able to use the collect
operator with our channelFlow
to receive values that have been sent to a channel. We were also able to use other flow operators, like map
, to manipulate or transform these values.
In summary, in the case of a channelFlow
, the collect
operator works similarly to the receive
method of a regular Channel
. Both allow you to receive values that have been sent to a channel. However, when using collect
with a channelFlow
, you can take advantage of other flow operators to manipulate or transform the emitted values. The channelFlow
builder allows us to create a flow that emits values based on channel operations. It provides a powerful tool for integrating channels and flows in Kotlin.
Emit vs Send
Both emit
and send
are used to emit values from flows, but they are used in different contexts. The emit
function is used with flow builders like flow
, flowOf
, and asFlow
, while the send
function is used with channel-based flow builders like channelFlow
and callbackFlow
.
The awaitClose Function
The awaitClose
function is used with the callbackFlow
builder to keep the flow alive until it's cancelled or completed. It takes an optional lambda function as an argument, which is called when the flow is cancelled or completed.
Here's an example that demonstrates how to use the awaitClose
function:
val myCallback: (Int) -> Unit = { value ->
println(value)
}
val myFlow = callbackFlow {
myCallback(1)
awaitClose {
println("Flow cancelled or completed")
}
}
myFlow.collect()
This code creates a callback function that prints any value passed to it. Then we use the callbackFlow
builder function to create a flow that calls this callback with a value of 1. The awaitClose
function is called with a lambda function that prints a message when the flow is cancelled or completed. Finally, we use the collect
operator to collect any values emitted by the flow.
The output of this code will be:
1
Flow cancelled or completed
As you can see, the awaitClose
function allows us to keep our flow alive until it's cancelled or completed. If we didn't call this function, our flow would complete immediately after calling our callback, and no further values would be emitted. Hence we are not notified about the completion or cancellation.
The awaitClose
function also allows us to specify an optional lambda function that's called when the flow is cancelled or completed. This can be useful for performing cleanup operations or releasing resources when the flow is no longer needed.
In summary, the awaitClose
function is used with the callbackFlow
builder to keep the flow alive until it's cancelled or completed. It provides a powerful tool for controlling the lifetime of flows created with this builder.
The callbackFlow Builder
The callbackFlow
builder function is used to create a flow that emits values based on callbacks. It takes a lambda function as an argument and returns a flow that emits values sent to a callback inside the lambda function.
Here's an example that demonstrates how to use the callbackFlow
builder:
val myCallback: (Int) -> Unit = { value ->
println(value)
}
val myFlow = callbackFlow {
myCallback(1)
awaitClose()
}
myFlow.collect()
This code creates a callback function that prints any value passed to it. Then we use the callbackFlow
builder function to create a flow that calls this callback with a value of 1. The awaitClose
function is called to keep the flow alive until it's cancelled. Finally, we use the collect
operator to collect any values emitted by the flow.
The output of this code will be:
1
As you can see, the callbackFlow
builder allows us to create a flow that emits values based on callbacks.
You can think of the callbackFlow
builder like building an elevator that connects two floors. You can use the elevator to transfer items (values) from one floor (callback) onto another floor (flow).
Let's take a closer look at what's happening in this code.
First, we create a callback function that takes an integer value as an argument and prints it. This callback will be called by our flow when it wants to emit a value.
Next, we use the callbackFlow
builder function to create our flow. This function takes a lambda function as an argument and returns a flow that emits values sent to a callback inside the lambda function.
Inside the lambda function, we call our callback with a value of 1. This causes our callback to be executed and print the value 1.
After calling our callback, we call the awaitClose
function. This function keeps our flow alive until it's cancelled or completed. If we didn't call this function, our flow would complete immediately after calling our callback, and no further values would be emitted.
Finally, we use the collect
operator to collect any values emitted by our flow. In this case, since our callback only prints values and doesn't actually emit them, no values are collected by our collector.
In summary, the callbackFlow
builder allows us to create a flow that emits values based on callbacks. It provides a powerful tool for integrating callbacks and flows in Kotlin.
When to Use Which Builder
When choosing which flow builder to use, it's important to consider your specific needs and requirements. Each builder has its own strengths and weaknesses, and is best suited for certain situations.
If you need to create a flow from scratch, you might want to use the
flow
builder. This builder allows you to specify exactly how and when values are emitted by the flow.If you need to create a flow from a fixed set of values, you might want to use the
flowOf
builder. This builder allows you to create a flow that emits a fixed set of values.If you need to create a flow from an iterable or sequence, you might want to use the
asFlow
builder. This builder allows you to convert any iterable or sequence into a flow.If you need to create a flow based on channel operations, you might want to use the
channelFlow
builder. This builder allows you to create a flow that emits values sent to a channel.If you need to create a flow based on callbacks, you might want to use the
callbackFlow
builder. This builder allows you to create a flow that emits values sent to a callback.
When dealing with APIs, it's common to use flows for handling asynchronous data. For example, if you're making an API call that returns multiple results over time, you might want to use the flow
or channelFlow
builders to create a flow that emits these results as they become available.
Conclusion
In this article, we've explored five different flow builders in Kotlin Flow API: flow
, flowOf
, asFlow
, channelFlow
, and callbackFlow
. We've provided detailed explanations of each builder, along with code examples and comparisons of emit
vs send
. We've also discussed which builder to use in different situations, especially when dealing with APIs.
By understanding these builders and their usage, you can create flows that meet your specific needs and requirements. Flows provide a powerful tool for handling asynchronous data streams in Kotlin.