Skip to Content
Course content

240: Combining Multiple Flows: zip, combine, merge

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a sports tracking app the other day and ran into a classic problem. I had two different data streams: one providing the current game score and another providing the game clock. I wanted to display them together on a single UI line, but as soon as I tried to glue them together, I realized that "combining" isn't a single operation in Kotlin. Depending on how the data arrives, you need very different tools.

The Lock-Step Problem

My first instinct was to use zip. It sounds perfect, right? I have two flows, and I want to zip them into one pair. Let's look at what happened when I ran this:

val scoreFlow = flowOf("1-0", "2-0", "2-1").onEach { delay(100) }
val clockFlow = flowOf("10:00", "11:00").onEach { delay(200) }

scoreFlow.zip(clockFlow) { score, clock -> 
    "$score at $clock" 
}.collect { println(it) }

I expected to see all three score updates. Instead, I got:

1-0 at 10:00
2-0 at 11:00

Wait, where did "2-1" go? I looked closer and realized zip is strictly lock-step. It waits for a value from both flows before emitting a result. Because clockFlow only had two elements, the third score update just sat there waiting for a clock update that never came. This is great for pairing related items (like a request and its corresponding response), but it's a nightmare for real-time dashboards where one stream is faster than the other.

Getting the Latest State

In a real game, the clock keeps ticking regardless of whether the score changes. I don't want to wait for the clock to tick to show a goal; I just want the most recent clock time paired with the new score. This is where combine comes in.

I swapped zip for combine and tried again:

scoreFlow.combine(clockFlow) { score, clock -> 
    "$score at $clock" 
}.collect { println(it) }

The output changed completely:

1-0 at 10:00
2-0 at 10:00
2-0 at 11:00
2-1 at 11:00

Now we're talking. combine doesn't wait for a pair. Whenever either flow emits a new value, it takes the latest value from the other flow and pushes a result. Notice how "2-0" appeared twice? Once when the score updated, and again when the clock caught up. It's a bit noisier, but it's accurate to the state of the world.

Treating Everything as a Single Stream

Then I thought, "What if I don't actually need to pair them?" What if I just want a single stream of 'Events' that I can log to a file, and I don't care which flow they came from? If I use combine, I'm forced to create a pair. If I use merge, I can just flatten them into one pipe.

To make this work, both flows need to emit the same type. I wrapped my strings in a simple Event sealed class:

sealed class Event {
    data class ScoreUpdate(val score: String) : Event()
    data class ClockUpdate(val time: String) : Event()
}

val scoreEvents = flowOf(Event.ScoreUpdate("1-0"), Event.ScoreUpdate("2-0"))
val clockEvents = flowOf(Event.ClockUpdate("10:00"), Event.ClockUpdate("11:00"))

merge(scoreEvents, clockEvents).collect { event ->
    when(event) {
        is Event.ScoreUpdate -> println("Score changed to ${event.score}")
        is Event.ClockUpdate -> println("Clock is now ${event.time}")
    }
}

With merge, there is no pairing and no waiting. It's essentially a "first-come, first-served" queue. If the score updates five times before the clock ticks once, you'll see five score events immediately. It's the leanest way to handle multiple sources of the same data type without adding the overhead of combining states.

So, the rule of thumb I've settled on: use zip for strict pairs, combine for the "latest state" of multiple sources, and merge when you just want a single stream of interleaved events.




📋 Practical Task

Build a Smart Home Sensor Monitor

You are building a monitoring system for a smart home. You have two Flows: one providing Temperature readings and one providing Humidity readings. The sensors report at different intervals.

Your Task:

  • Create a tempFlow that emits "20°C", "21°C", and "22°C" with a 100ms delay between each.
  • Create a humidityFlow that emits "40%", "45%" with a 250ms delay between each.
  • Use the combine operator to create a new Flow that emits a formatted string: "Current Climate: 21°C, 40%".
  • Collect the results and print them to the console.

Verify that you see updates even when only one of the sensors has emitted a new value, and ensure the final emitted value reflects the latest state of both sensors.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.