Introduction to Reactive Programming with Kotlin Flow🌴#1

Vefa Can Beytorun
Paycell Tech Team
Published in
4 min readAug 20, 2024

Introduction

In modern Android development, handling asynchronous data streams efficiently is crucial. Reactive programming offers a powerful paradigm to manage these streams. Kotlin Flow, part of the Kotlin coroutines library, is designed to handle asynchronous data streams in a more declarative and readable way. This article introduces Kotlin Flow, explores its key features, and demonstrates how to implement it in Android projects with practical examples. This is first part of my article, Once the second part is published, you can access it through my profile or the Paycell Tech Team page.

Let’s start our article.

What is Kotlin Flow?

Kotlin Flow is a cold asynchronous stream that sequentially emits values and completes normally or with an exception. It is built on top of coroutines and provides a set of operators that can be used to process data in a reactive manner.

We will soon learn about emit and its purpose in this example:

fun simpleFlow(): Flow<Int> = flow {
for (i in 1..5) {
emit(i) // Emits values 1 to 5 sequentially
}
}

Key Concepts in Kotlin Flow

1. Cold Streams

In Kotlin Flow, streams are cold by default, meaning they do not produce data until they are collected. This behavior makes them efficient, as no resources are wasted when the data is not needed.

For example:

suspend fun main() {
val flow = simpleFlow()
println("Flow hasn't started yet")
flow.collect { value -> println(value) }
println("Flow collection is complete")
}

fun simpleFlow(): Flow<Int> = flow {
for (i in 1..5) {
emit(i)
}
}

/* Output:
Flow hasn't started yet
1
2
3
4
5
Flow collection is complete
*/

2. Flow Builders

Kotlin provides several built-in flow builders, including flow, flowOf, and asFlow. These builders create flows from various sources.

a) flow Builder

The flow builder is the most fundamental way to create a flow that emits values step by step. In the example above we used flow builder

b) flowOf Builder

The flowOf builder is used to immediately emit a specific set of values. In this example, we create a flow that emits a series of numbers:


fun main() = runBlocking {
val numberFlow = flowOf(1, 2, 3, 4, 5)
numberFlow.collect { value ->
println("Collected: $value")
}
}

/* Output:
Collected: 1
Collected: 2
Collected: 3
Collected: 4
Collected: 5
*/

c) asFlow Builder

The asFlow builder is used to convert collections or other iterable data structures (like lists or arrays) into a flow. Below is an example of converting a list into a flow:

fun main() = runBlocking {
val list = listOf(1, 2, 3, 4, 5)
val listFlow = list.asFlow()

listFlow.collect { value ->
println("Collected: $value")
}
}

/* Output:
Collected: 1
Collected: 2
Collected: 3
Collected: 4
Collected: 5
*/

d) Summary

  • flow: The basic builder for emitting values step by step.
  • flowOf: Used to immediately emit a specific set of values.
  • asFlow: Converts collections or arrays into a flow.

3. Operators

a) collect

Terminal operator that triggers the flow to start emitting values and allows you to process each emitted value.

flowOf(1, 2, 3).collect { value -> 
println(value)
}

/* Output:
1
2
3
*/

b) map

Transforms each emitted value into another value. It works like a map function in collections.

flowOf(1, 2, 3).map { it * 2 }.collect { value ->
println(value)
}

/* Output:
2
4
6
*/

c) filter

Filters the emitted values based on a condition, only passing through values that satisfy the predicate.

flowOf(1, 2, 3, 4).filter { it % 2 == 0 }.collect { value ->
println(value)
}

/* Output:
2
4
*/

d) take

Takes only the first N values (like the number 2 in the example)from the flow and ignores the rest.

flowOf(1, 2, 3, 4).take(2).collect { value ->
println(value)
}

/* Output:
1
2
*/

e) combine

Combines the latest values from multiple flows and emits a new value each time any flow emits.

val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf("A", "B", "C")
flow1.combine(flow2) { a, b -> "$a$b" }.collect { value ->
println(value)
}
/* Output:
1A
2B
3C
*/

f) debounce

This is a little confusing, emits an item from the flow only after a specified time has passed without another emission. I can explain better by showing with an example.

flow {
emit(1)
delay(100)
emit(2)
delay(500)
emit(3)
}.debounce(200).collect { value ->
println(value)
}

/* Output:
2
3
*/

Wow why wasn’t the value 1 shown? Because:

The first value 1 is emitted, followed by a 100 ms delay. However, since value 2 is emitted before the 200 ms debounce period is up, value 1 is discarded and not printed. What about the value 2? The value 2 is emitted, followed by a 500 ms delay. Since no other value is emitted during this period, value 2 is processed and printed.

Conclusion

In this first part of our journey into reactive programming with Kotlin Flow, we’ve explored the foundational concepts and key features that make Kotlin Flow a powerful tool for managing asynchronous data streams in Android development. We discussed what Kotlin Flow is, how it operates as a cold stream, and introduced various flow builders and operators that allow you to create and manipulate flows effectively.

--

--