Kotlin Flows Animated

Robert Baricevic-Petrus
5 min readJan 31, 2024

To a beginner developer, the concept of Flows can be quite confusing. What are they for? How do they make things easier for us? What do we do when we use collect() or emit(), and what are all those operator functions?

That is why I decided to explain those concepts the way I explained them to myself (like I was teaching Kotlin Flows to a six-year-old), so without further ado, let’s get to it.

Thinking about Flows

You could think of a Flow like a conveyor belt inside a factory, and each item transported by the said belt as a piece of data, for example:

Let’s say that we are creating a Video Game Weapon storage facility. Each Weapon is produced at random and in real time. We want to store each Weapon the moment it is produced

The Weapons would be represented like so:

sealed class Weapon {
class Sword(): Weapon
class Staff(): Weapon
class SpellBook(): Weapon
}

And the Weapon producer would simply be a Flow of type Weapon:

val weaponProducer: Flow<Weapon>

Now every time a new Weapon is produced the weaponProducer would call the emit() function sending out a new Weapon that we would receive and store by using collect().

val weaponBox = mutableListOf<Weapon>()

weaponProducer.collect { weapon ->
weaponBox.add(weapon)
}

And that’s pretty much how Kotlin Flows work. However, just emitting pieces of data is not all that useful, what if we wanted to manipulate our data before it is collected? That’s where operators come in!

Intermediate Operators

We have two types of operators for Kotlin Flow:

  1. Terminal Operators: Used to trigger the execution of the Flow (think of it as starting the conveyor belt) and to perform a final action. Common examples are collect(), first(), last()
  2. Intermediate (Non-terminal) Operators: Used to transform, filter, or manipulate the data as it flows from the source to the terminal operation. They do not trigger the execution of the Flow, instead, they define what operations should be applied to items that pass through.

In this article I will focus on explaining the intermediate operators, to help you visualize how they work. We will start from the simpler ones and move to the more complex ones towards the end.

Filter

The filter operator is used to select elements from the Flow that meet a specific condition.

To get back to the conveyor belt analogy, if we were interested in storing only Swords we would do it like so:

weaponProducer
.filter { weapon ->
weapon is Sword
}.collect()

DistinctUntilChanged

The distinctUnitlChanged operator filters out consecutive duplicate elements. It allows a value to be emitted only if it’s different from the previous value.

So if we wanted each Weapon to be different from its predecessor we would do it like this:

weaponProducer
.distinctUntilChanged()
.collect()

Map

The Map operator is used to transform each input element into a different output element. It doesn’t change the original Flow or collection, it creates a new one with the transformed value.

To illustrate this let’s say that we want to transform our weapons from regular Weapons to EnchantedWeapons:

weaponProducer
.map { weapon ->
mapToEnchantedWeapon(weapon)
}.collect()

FlatMap

This one confused me the most when I started learning about Flows because this is the method signature for the map operator:

inline fun <T, R> Flow<T>.map(
crossinline transform: suspend (value: T) -> R
): Flow<R>

and this is the method signature for flatMap:

inline fun <T, R> Flow<T>.flatMapLatest(
crossinline transform: suspend (value: T) -> Flow<R>
): Flow<R>

As you can see they are both used to transform the Flow of type A into a Flow of type B but there is one crucial difference:

Map transforms each element individually while flatMap transforms each element into another Flow and then merges those two flows into one.

Let’s say that we have to produce a specific type of Weapon until we get a request for a different Weapon type which could happen at any time:

val weaponTypes: Flow<WeaponType> = queryWeaponTypes()

private fun produceWeapons(weaponType: WeaponType): Flow<Weapon> {
// Emit weapons of the specified type
}

weaponTypes
.flatMap { type ->
produceWeapons(type)
}.collect()

Now each WeaponType emission would create a new Weapon Flow

Zip

Zip combines two flows together, it takes corresponding elements from each flow and pairs them into a single element. This happens sequentially: the first element of Flow A is paired with the first element of Flow B, the second with the second, and so on.

Let’s say that we have an Enchantment producer and a Weapon producer, and we want each Enchantment produced to get paired with each produced Weapon for us to create a new Weapon:

val enchantments: Flow<Enchantment> = queryEnchantments()
val weapons: Flow<Weapon> = queryWeapons()

enchantments.zip(weapons) { enchantment, weapon ->
createMagicWeapon(enchantment, weapon)
}

Final Thoughts

I hope this article helped you visualize Kotlin Flows and some of its operators a little better. If you liked the animations and would like to see more of them on different topics feel free to give me a suggestion.

Thank you for reading :]

--

--