article banner

Exercise: Flow with history

As an exercise in my Kotlin Coroutines workshop, I ask participants to implement a withHistory function that returns a flow of all the values that have been emitted. As a result, I often see the following implementation:

fun <T> Flow<T>.withHistory(): Flow<List<T>> = flow { val history = mutableListOf<T>() emit(history) collect { history += it emit(history) } }

This implementation is not correct, as is illustrated by the usage example below. Your task is to fix it.

suspend fun main() { flowOf(1, 2, 3) .withHistory() .toList() .let(::println) // [[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]] }

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 effective/safe/FlowHistory.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.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.* import org.junit.Test import kotlin.test.assertEquals fun <T> Flow<T>.withHistory(): Flow<List<T>> = flow { val history = mutableListOf<T>() emit(history) collect { history += it emit(history) } } suspend fun main() { flowOf(1, 2, 3) .withHistory() .toList() .let(::println) // [[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]] } class FlowHistoryTests { @Test fun `should emit empty for empty`() = runTest { assertEquals(listOf(listOf()), flowOf<String>().withHistory().toList()) } @Test fun `should emit history`() = runTest { val flow = flowOf(1, 2, 3).withHistory() assertEquals(listOf(listOf(), listOf(1), listOf(1, 2), listOf(1, 2, 3)), flow.toList()) } @Test fun `should emit elements as they appear`() = runTest { val flow = flow { delay(100) emit(1) delay(1000) emit(2) delay(10000) emit(3) }.withHistory() .withVirtualTime(this) assertEquals( listOf( ValueAndTime(listOf(), 0), ValueAndTime(listOf(1), 100), ValueAndTime(listOf(1, 2), 1100), ValueAndTime(listOf(1, 2, 3), 11100), ), flow.toList() ) } } 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)