1. Introduction

Kotlin Flows have become an integral part of modern asynchronous programming. They provide a seamless and concise way to handle asynchronous data streams. When working with flows, two commonly used terminal operators are single() and first(). Although these functions may seem interchangeable at first glance, understanding their nuances is crucial for writing efficient and bug-free code.

In this tutorial, we’ll delve into the differences between the single() and first() functions in Kotlin Flows.

2. Understanding Kotlin Flows

Before diving into the specifics of single() and first(), let’s briefly review Kotlin Flows. A Flow is an asynchronous sequence that emits multiple values over time. They handle streams of data in a non-blocking and efficient manner, making them a powerful tool for reactive programming.

Here are some key aspects highlighting the importance of flows:

  • Flows simplify asynchronous programming by allowing us to represent and process sequences of values asynchronously.
  • They provide a way to handle asynchronous operations without blocking threads.
  • Kotlin Flows support cancellation.
  • Flows provide built-in support for error handling, allowing us to propagate and consistently handle errors.
  • Flows are built on top of Kotlin coroutines, meaning they seamlessly integrate with coroutine-based code, providing a unified and cohesive programming model for asynchronous operations.

3. Using the single() Function

The single() function returns the first and only item from a Flow. If the flow is empty, this function throws a NoSuchElementException. If the flow has more than one item, this function throws an IllegalArgumentException.

This makes single() suitable when we expect a Flow to emit a single value and any other combinations of values should error:

@Test
fun testSingleValue() = runBlocking {
    val multipleValuesFlow = flowOf(42)
    val singleValue = multipleValuesFlow.single()
    assertEquals(42, singleValue)
}

In this code, our test case verifies that when a flow containing a single value of 42 returns this one value.

As mentioned earlier, single() throws an exception when the Flow contains anything more than a single value:

@Test
fun testExceptionForMultipleValues() = runBlocking {
    val multipleValues = flowOf(42, 43, 44)
    val exception = assertFailsWith<IllegalArgumentException> {
        runBlocking {
            multipleValues.single()
        }
    }
    assertEquals("Flow has more than one element", exception.message)
}

In this example, multipleValues contains three items, and attempting to call single() on it will throw an IllegalArgumentException.

Next, we’ll verify that an empty flow throws a NoSuchElementException:

@Test
fun testIllegalArgumentException() = runBlocking {
    val emptyFlow = flowOf<Int>()
    val exception = assertFailsWith<NoSuchElementException> {
        runBlocking {
            emptyFlow.single()
        }
    }
    assertEquals("Flow is empty", exception.message)
}

This enforces that single() is only for when we expect our Flow to have one value, and all other cases should error.

3.1. When To Use the single() Function

Let’s look at the circumstances where we should use single():

  • Use single() when we expect the Flow to emit exactly one item.
  • We expect non-unique emissions to error.

4. Using the first() Function

On the other hand, the first() function retrieves the first item emitted by a Flow. It does not require that only one item is emitted; instead, it returns the first item emitted and completes the Flow:

@Test
fun testFirstValue() = runBlocking {
    val multipleValuesFlow = flowOf(1, 2, 3)
    val firstValue = multipleValuesFlow.first()
    assertEquals(1, firstValue)
}

In this code, we obtain the first value from a Flow of integers first(), which in this case is one.

We’ll also verify that an empty Flow throws a NoSuchElementException:

@Test
fun testFirstValueFromEmptyFlow() = runBlocking {
    val emptyFlow = emptyFlow<Int>()
    val exception = assertFailsWith<NoSuchElementException> {
        runBlocking {
            emptyFlow.first()
        }
    }
    assertEquals("Expected at least one element", exception.message)
}

4.1. When To Use the first() Function

Let’s take a look at the situations where we should use first():

  • When we seek the first emitted item, no matter how many items the Flow might contain.
  • When we want to process the first emitted item without waiting for the Flow to complete.

5. Conclusion

Flows are a powerful tool for handling asynchronous data streams, allowing developers to choose between the single() and first() terminal functions based on our specific requirements. The distinction lies in how flows with more than one value are handled. The single() function ensures that only one item is emitted, while the first() function retrieves the initial item without limiting the flow’s size. Both functions will error if the flow is empty.

As always, the full implementation of these examples is available over on GitHub.