To practice all the different flow operators, implement the following functions:
producingUnits - produces a flow of Unit with num elements.
delayEach - adds a delay of time timeMillis between elements.
mapIndexed - should transform values, where the transformation value should have the index of the element.
toNextNumbers - should transform elements to the next numbers starting from 1. So Unit, Unit, Unit should be transformed to 1, 2, 3.
withHistory - produces not only elements but the whole history until now.
makeLightSwitch - based on two light switches; it should decide if the light should be switched on. It should emit true after a change if one switch is true and the other is false. The first emitted state should be false, and then a new state should be emitted with each event from any switch.
makeLightSwitchToggle - based on two light switches; it should decide if the light should be switched on. It should emit true if the number of events from both light switches is odd. Each event from a light switch emits a new state. The first emitted state is true; each new state is the previous state, but toggled.
polonaisePairing - Should pair two flows of people so that each person from one flow is paired with the person from the other flow, which is emitted at the same time. A person without a pair should be ignored.
This problem can either be solved in the below playground or you can clone kotlin-exercises project and solve it locally. In the project, you can find code template for this exercise in coroutines/flow/Kata.kt. You can find there starting code and unit tests.
Once you are done with the exercise, you can check your solution here.
Playground
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.*
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.assertEquals
// Produces a flow of Unit
// For instance producingUnits(5) -> [Unit, Unit, Unit, Unit, Unit]
fun producingUnits(num: Int): Flow<Unit> = TODO()
// Adds a delay of time `timeMillis` between elements
fun <T> Flow<T>.delayEach(timeMillis: Long): Flow<T> = TODO()
// Should transform values, where transformation value should have index of the element
// flowOf("A", "B").mapIndexed { index, value -> "$index$value" } -> ["0A", "1B"]
fun <T, R> Flow<T>.mapIndexed(transformation: suspend (index: Int, T) -> R): Flow<R> = TODO()
// Should transform Unit's to next numbers starting from 1
// For instance flowOf(Unit, Unit, Unit, Unit).toNextNumbers() -> [1, 2, 3, 4]
// Example:
// Input --------U------UU---------U------
// Result --------1------23---------4------
fun Flow<*>.toNextNumbers(): Flow<Int> = TODO()
// Produces not only elements, but the whole history till now
// For instance flowOf(1, "A", 'C').withHistory() -> [[], [1], [1, A], [1, A, C]]
fun <T> Flow<T>.withHistory(): Flow<List<T>> = TODO()
// Based on two light switches, should decide if the general light should be switched on.
// Should be if one is true and another is false.
// The first state should be false, and then with each even from any switch, new state should be emitted.
// Example:
// switch1 -------t-----f----------t-t-------
// switch2 ----------------f-t-f--------t-f-t
// Result f------t-----f--f-t-f---t-t--f-t-f
fun makeLightSwitch(switch1: Flow<Boolean>, switch2: Flow<Boolean>): Flow<Boolean> = TODO()
// Based on two light switches, should decide if the general light should be switched on.
// Should be if one is turned on and another is off
// Each event from a switch emits news state. The first state is true, each new state is toggled.
// Example:
// switch1 -------U-----U--------------------------U---------------UU-----
// switch2 ----------------U-------------------U-------------U------------
// Result -------t-----f--t-------------------f---t---------f-----tf-----
fun makeLightSwitchToggle(switch1: Flow<Unit>, switch2: Flow<Unit>): Flow<Boolean> = TODO()
fun polonaisePairing(track1: Flow<Person>, track2: Flow<Person>): Flow<Pair<Person, Person>> = TODO()
data class Person(val name: String)
class FlowTests {
@Test()
fun producingUnitsTests() = runTest {
assertEquals(listOf(), producingUnits(0).toList())
assertEquals(listOf(Unit), producingUnits(1).toList())
assertEquals(listOf(Unit, Unit), producingUnits(2).toList())
assertEquals(listOf(Unit, Unit, Unit), producingUnits(3).toList())
for (i in 1..100 step 7) {
assertEquals(List(i) { Unit }, producingUnits(i).toList())
}
}
@Test()
fun flowDelayEachTests() = runTest {
val emittedNum = AtomicInteger()
producingUnits(100)
.delayEach(1000)
.onEach { emittedNum.incrementAndGet() }
.launchIn(this)
assertEquals(0, emittedNum.get())
delay(1_500)
assertEquals(1, emittedNum.get())
delay(2_000)
assertEquals(3, emittedNum.get())
delay(12_000)
assertEquals(15, emittedNum.get())
}
@Test()
fun mapIndexedTests() = runTest {
assertEquals(
listOf("0 A", "1 B", "2 C", "3 D"),
('A'..'D').asFlow()
.mapIndexed { index, letter -> "$index $letter" }
.toList()
)
val actual: List<ValueAndTime<Pair<Int, Any>>> = flow<Any> {
delay(10)
emit(10)
delay(100)
emit("A")
delay(1000)
emit('C')
}.mapIndexed { index, any -> Pair(index, any) }
.withVirtualTime(this)
.toList()
val expected: List<ValueAndTime<Pair<Int, Any>>> = listOf(
ValueAndTime(Pair(0, 10), 10),
ValueAndTime(Pair(1, "A"), 110),
ValueAndTime(Pair(2, 'C'), 1110),
)
assertEquals(expected, actual)
}
@Test()
fun toNextNumbersTests() = runTest {
assertEquals(listOf(), producingUnits(0).toNextNumbers().toList())
assertEquals(listOf(1), producingUnits(1).toNextNumbers().toList())
assertEquals(listOf(1, 2), producingUnits(2).toNextNumbers().toList())
assertEquals(listOf(1, 2, 3), producingUnits(3).toNextNumbers().toList())
for (i in 1..100 step 7) {
val list = List(i) { it + 1 }
assertEquals(list, list.map {}.asFlow().toNextNumbers().toList())
}
}
@Test()
fun withHistoryTests() = runTest {
assertEquals(listOf(listOf()), producingUnits(0).withHistory().toList())
assertEquals(listOf(listOf(), listOf(Unit)), producingUnits(1).withHistory().toList())
assertEquals(listOf(listOf(), listOf(Unit), listOf(Unit, Unit)), producingUnits(2).withHistory().toList())
assertEquals(
listOf(listOf(), listOf(1), listOf(1, 2)),
flowOf(1, 2).withHistory().toList()
)
assertEquals(
listOf(listOf(), listOf(true), listOf(true, false)),
flowOf(true, false).withHistory().toList()
)
val flow = flow {
delay(10)
emit("A")
delay(100)
emit(10)
delay(1000)
emit("C")
}
assertEquals(
listOf(
ValueAndTime(listOf(), 0),
ValueAndTime(listOf("A"), 10),
ValueAndTime(listOf("A", 10), 110),
ValueAndTime(listOf("A", 10, "C"), 1110),
),
flow.withHistory()
.withVirtualTime(this)
.toList()
)
}
@Test()
fun makeLightSwitchTests() = runTest {
val switchOne = flow<Boolean> {
emit(true)
delay(1000)
emit(false)
delay(10)
emit(true)
delay(500) // 1500
emit(false)
}
val switchTwo = flow<Boolean> {
emit(false)
delay(200)
emit(true)
delay(1000) // 1200
emit(false)
}
var lightOn = false
launch {
makeLightSwitch(switchOne, switchTwo).collect {
lightOn = it
}
}
delay(50)
assertEquals(true, lightOn)
delay(200) // 250
assertEquals(false, lightOn)
delay(800) // 1050
assertEquals(false, lightOn)
delay(200) // 1250
assertEquals(true, lightOn)
delay(300) // 1550
assertEquals(false, lightOn)
}
@Test()
fun makeLightSwitchExampleTests() = runTest {
val switch1 = flow {
delay(7_000)
emit(true)
delay(6_000)
emit(false)
delay(11_000)
emit(true)
delay(2_000)
emit(true)
}
val switch2 = flow {
delay(16_000)
emit(false)
delay(2_000)
emit(true)
delay(2_000)
emit(false)
delay(9_000)
emit(true)
delay(2_000)
emit(false)
delay(2_000)
emit(true)
}
val result = makeLightSwitch(switch1, switch2)
.fold(mapOf<Long, Boolean>()) { acc, e -> acc + (currentTime to e) }
val expected = mapOf(
0L to false,
7_000L to true,
13_000L to false,
16_000L to false,
18_000L to true,
20_000L to false,
24_000L to true,
26_000L to true,
29_000L to false,
31_000L to true,
33_000L to false,
)
assertEquals(expected, result)
}
@Test()
fun makeLightSwitchToggleTests() = runTest {
val switchOne = flow<Unit> {
emit(Unit)
delay(1000)
emit(Unit)
delay(10)
emit(Unit)
delay(500) // 1500
emit(Unit)
}
val switchTwo = flow<Unit> {
emit(Unit)
delay(200)
emit(Unit)
delay(1000) // 1200
emit(Unit)
}
var lightOn = false
launch {
makeLightSwitchToggle(switchOne, switchTwo).collect {
lightOn = it
}
}
delay(50)
assertEquals(false, lightOn)
delay(200) // 250
assertEquals(true, lightOn)
delay(800) // 1050
assertEquals(true, lightOn)
delay(200) // 1250
assertEquals(false, lightOn)
delay(300) // 1550
assertEquals(true, lightOn)
}
@Test()
fun makeLightSwitchToggleExampleTests() = runTest {
val switch1 = flow {
delay(7_000)
emit(Unit)
delay(6_000)
emit(Unit)
delay(27_000)
emit(Unit)
delay(16_000)
emit(Unit)
delay(1_000)
emit(Unit)
}
val switch2 = flow {
delay(16_000)
emit(Unit)
delay(20_000)
emit(Unit)
delay(17_000)
emit(Unit)
}
val result = makeLightSwitchToggle(switch1, switch2)
.fold(mapOf<Long, Boolean>()) { acc, e -> acc + (currentTime to e) }
val expected = mapOf(
7_000L to true,
13_000L to false,
16_000L to true,
36_000L to false,
40_000L to true,
53_000L to false,
56_000L to true,
57_000L to false,
)
assertEquals(expected, result)
}
@Test()
fun polonaisePairingTests() = runTest {
val track1 = flow<Person> {
emit(Person("A"))
emit(Person("B"))
delay(1000)
emit(Person("C"))
emit(Person("D"))
}
val track2 = flow<Person> {
emit(Person("1"))
delay(600)
emit(Person("2"))
delay(1000)
emit(Person("3"))
}
val res = polonaisePairing(track1, track2).toList()
val expected = listOf("A" to "1", "B" to "2", "C" to "3").map { Person(it.first) to Person(it.second) }
assertEquals(expected, res)
var lastPair: Pair<Person, Person>? = null
launch {
polonaisePairing(track1, track2).collect { lastPair = it }
}
runCurrent()
assertEquals(Person("A") to Person("1"), lastPair)
delay(200) // 200
assertEquals(Person("A") to Person("1"), lastPair)
delay(500) // 700
assertEquals(Person("B") to Person("2"), lastPair)
delay(500) // 1200
assertEquals(Person("B") to Person("2"), lastPair)
delay(500) // 1700
assertEquals(Person("C") to Person("3"), lastPair)
}
}
fun <T> Flow<T>.withVirtualTime(testScope: TestScope): Flow<ValueAndTime<T>> =
map { ValueAndTime(it, testScope.currentTime) }
data class ValueAndTime<T>(val value: T, val timeMillis: Long)
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.