Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Introduction

Kotlin’s Flow API is an excellent tool for managing asynchronous data streams in a reactive programming style. The StateFlow stands out because it represents a state that multiple subscribers can observe and collect. However, there are times when we want to combine many StateFlows into a single StateFlow. Merging two or more StateFlows can provide a unified view of the final state.

In this tutorial, we’ll explore different methods for combining StateFlows, starting with the built-in combine() operator, and then moving on to the zip() operator.

2. Using the combine() Operator

When combining StateFlows, the combine() operator is quite an effective tool. Whenever any of the input StateFlows emits a new value, the combined flow emits a value. This makes it ideal for scenarios where we want to track the latest updates across multiple streams:

@Test
fun `merge two stateflows using combine operator`() = runTest {
    val stateFlow1 = MutableStateFlow(5)
    val stateFlow2 = MutableStateFlow("Hi")

    val combinedStateFlow = combine(stateFlow1, stateFlow2) { value1, value2 ->
        value1 to value2
    }

    val result = combinedStateFlow.first()

    assertEquals(Pair(5, "Hi"), result)
}

In this example, we combine stateFlow1 and stateFlow2 using the combine() operator. It takes the latest values from each StateFlow and applies a transformation function. Here, the transformation function combines the values from stateFlow1 and stateFlow2 into a Pair. As a result, the combined StateFlow emits this paired value whenever either of the source flows emits a new value. Finally, we expect this emitted value to be a Pair of 5 and “Hi”.

2.1. Emitting New Values in combine()

Next, let’s see what happens when each StateFlow emits a new value:

@Test
fun `combine operator retains previous value when one flow emits`() = runTest {
    val stateFlow1 = MutableStateFlow(5)
    val stateFlow2 = MutableStateFlow("Hi")

    val combinedStateFlow = combine(stateFlow1, stateFlow2) { value1, value2 ->
        value1 to value2
    }

    val emissions = mutableListOf<Pair<Int, String>>()

    val job = launch {
        combinedStateFlow.collect {
            emissions.add(it)
        }
    }

    assertEquals(Pair(5, "Hi"), combinedStateFlow.first())

    stateFlow1.value = 11
    assertEquals(Pair(11, "Hi"), combinedStateFlow.first())

    stateFlow2.value = "World"
    assertEquals(Pair(11, "World"), combinedStateFlow.first())

    job.cancel()

    assertEquals(listOf(Pair(5, "Hi"), Pair(11, "Hi"), Pair(11, "World")), emissions)
}

In this test, we collect emissions from the combined StateFlow. Every time either StateFlow emits a new value, combine() re-emits the previous value from the other flow. This behavior is confirmed by the emissions list, where we see the state update incrementally as each flow emits a new value.

3. Using the zip() Operator

Another method for combining StateFlows is the zip() operator. Unlike combine(), which emits whenever any flow emits, zip() waits until all input StateFlows emit a new value before producing a combined result:

@Test
fun `merge two stateflows using zip operator`() = runTest {
    val stateFlow1 = MutableStateFlow(5)
    val stateFlow2 = MutableStateFlow("Hi")

    val zippedStateFlow = stateFlow1.zip(stateFlow2) { value1, value2 ->
        value1 to value2
    }

    val result = zippedStateFlow.first()
    assertEquals(Pair(5, "Hi"), result)
}

In this test, we use zip() to combine two StateFlows. As we’ll see, both flows must have a value emitted before our zipped flow receives anything because the zip() operator waits for stateFlow1 and stateFlow2 to emit before producing a result.

3.1. Emitting New Values in zip()

Now, let’s explore what happens when new values are emitted from our StateFlows while using zip():

@Test
fun `zip operator only emits when both flows emit`() = runTest {
    val stateFlow1 = MutableStateFlow(5)
    val stateFlow2 = MutableStateFlow("Hi")

    val zippedStateFlow = stateFlow1.zip(stateFlow2) { value1, value2 ->
        value1 to value2
    }

    val emissions = mutableListOf<Pair<Int, String>>()

    val job = launch {
        zippedStateFlow.collect {
            emissions.add(it)
        }
    }

    delay(100)
    stateFlow1.value = 11

    delay(100)
    stateFlow2.value = "Hey"

    delay(100)
    stateFlow1.value = 12

    delay(100)
    stateFlow2.value = "Hello"

    delay(100)

    assertEquals(listOf(Pair(5, "Hi"), Pair(11, "Hey"), Pair(12, "Hello")), emissions)

    job.cancel()
}

The test shows that the zip() operator only emits a combined result when both flows emit new values. Notice how we don’t see incremental updates in the emissions list, proving that the zip() operator waits for both flows to emit before updating.

The delay() calls ensure that each flow has enough time to emit, allowing the zip() operator to combine their values correctly without missing values.

Unlike combine(), which emits whenever any of the flows emit a new value, zip() requires both flows to emit before producing a result. This is why delay() is necessary for zip() but not for combine(), as combine() handles emissions independently and in real-time.

4. Conclusion

In this article, we’ve explored how to combine multiple StateFlows in Kotlin using two powerful operators: combine() and zip().

We began by examining the combine() operator, which offers real-time synchronization by emitting a new value whenever any input StateFlows change. This method is particularly effective when we need the latest updates from several streams simultaneously.

We then used the zip() operator, which requires all StateFlows to emit a value before generating a new combined result. This approach handles scenarios where we should only process new emissions across multiple flows after everything updates, ensuring we never look at the same emitted value twice.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.