Photo by Solen Feyissa on Unsplash

Representing multiple values in Kotlin: List vs. Sequence vs. Flow

Masoud Fallahpour
4 min readMar 7, 2023

--

When dealing with multiple values in Kotlin, there are several options available. In this post will look at List, Sequence, and Flow. In order to have a better understanding of these 3 options and how they differ we will define a hypothetical problem and try to refine our solution until we reach the desired outcome.

The problem

We want to write a function that returns the first 10 Fibonacci numbers. We assume that computing each number takes 500 milliseconds. Also, let’s assume that the ideal solution we are looking for should have the following properties:

  1. The solution should be non-blocking meaning that at no point it should ever block the main thread,
  2. Each number should be available as soon as it’s computed.

List

We will start with defining the following function which returns the first 10 Fibonacci numbers as a List.

fun fibonacciList(): List<Int> {
var first = 0
var second = 1
val numbersList = mutableListOf<Int>().apply {
Thread.sleep(500)
add(first)
Thread.sleep(500)
add(second)
}
repeat(8) {
Thread.sleep(500)
val t = second
second += first
first = t
numbersList += second
}
return numbersList
}

In the above code, we used Thread.sleep to simulate the time it takes to compute a number.

Let’s call the above function in the following way and see what’s printed:

fun main() {
lateinit var numbers: List<Int>
val duration = measureTimeMillis {
numbers = fibonacciList()
}
println("fibonacciList returned after $duration millis.")
numbers.forEach { print("$it ") }
}

The above code outputs something like the following:

fibonacciList returned after 5034 millis.
0 1 1 2 3 5 8 13 21 34

We can observe two things based on the above output:

  1. Calling fibonacciList blocks the main thread until it returns,
  2. When calling fibonacciList, we have no numbers for roughly 5 seconds and after that, we’ll have all the numbers at once.

So using a List as the return type of the function was not a good idea as it did not satisfy the properties of the solution we were looking for. Next, let’s try Sequence.

Sequence

This time we’ll use a Sequence as the return type of the function, resulting in the following code:

fun fibonacciSequence(): Sequence<Int> {
var first = 0
var second = 1
val numbersSequence = sequence {
Thread.sleep(500)
yield(first)
Thread.sleep(500)
yield(second)
repeat(8) {
Thread.sleep(500)
val t = second
second += first
first = t
yield(second)
}
}
return numbersSequence
}

We call the above function like this:

fun main() {
lateinit var numbers: Sequence<Int>
val duration1 = measureTimeMillis {
numbers = fibonacciSequence()
}
println("fibonacciSequence returned after $duration1 millis.")
val duration2 = measureTimeMillis {
numbers.forEach { print("$it ") }
}
println("\nIt took $duration2 millis to print all numbers.")
}

Running the above code generates an output like this:

fibonacciSequence returned after 8 millis.
0 1 1 2 3 5 8 13 21 34
It took 5040 millis to print all numbers.

When we run the above code we can see that fibonacciSequence is non-blocking and it returns quickly. But calling forEach on the Sequence is blocking, and it takes roughly 5 seconds for all numbers to be printed. The good news is that each number is printed as soon as it’s available. So we made some progress! Let’s move on to our next option which is Flow.

Flow

Let’s write the same function but this time using Flow as its return type:

fun fibonacciFlow(): Flow<Int> {
var first = 0
var second = 1
val numbersFlow = flow {
delay(500)
emit(first)
delay(500)
emit(second)
repeat(8) {
delay(500)
val t = second
second += first
first = t
emit(second)
}
}
return numbersFlow
}

Let’s call the above function in the following way:

fun main() = runBlocking {
lateinit var numbers: Flow<Int>
val duration = measureTimeMillis {
numbers = fibonacciFlow()
}
println("fibonacciFlow returned after $duration millis.")
launch {
numbers.collect { print("$it ") }
}
println("Main thread is not blocked!")
}

Running the code above produces an output like this:

fibonacciFlow returned after 6 millis.
Main thread is not blocked!
0 1 1 2 3 5 8 13 21 34

By running the above code we can observe the following things:

  1. The whole code is non-blocking,
  2. Each number is printed as soon as it’s produced.

This means we’ve found our ideal solution! We have a function that generates the first 10 Fibonacci numbers. It’s not blocking the main thread and each number is available as soon as it’s produced.

And that’s a wrap!

--

--

Masoud Fallahpour

Software engineering @ Klarna. Software engineer, *nix lover, curious learner, gym guy, casual gamer.