
Hey again! In Part 1, we built our Air Compressor (the Flow) and pulled the trigger on our Spray Gun (the .collect() function) to start the air moving. But let’s be real—if you just blast raw air straight out of a compressor, you’re going to get moisture, dirt, and uneven pressure.
To do a professional paint job, you need attachments on your hose.
In Kotlin Flows, these attachments are called Operators. They intercept the data while it’s traveling down the pipe, changing it before it ever reaches the UI (the spray gun).
The Breakdown: Valves and Filters
Let’s look at the three most common attachments you’ll put on your hose:
1. The Moisture Trap (filter) Sometimes the compressor spits out water droplets along with the air. You don’t want water ruining your paint job. The filter operator acts like a screen—it only lets things pass if they meet a certain condition.
// Only emit air bursts that are perfectly dry
airFlow.filter { air ->
air.isDry == true
}
2. The Paint Mixer (map) Right now, you just have raw air. But you want blue paint. The map operator intercepts the raw air, mixes blue paint into it, and sends it further down the line. It literally transforms the data type from A to B.
// Transforms RawAir into BluePaint
airFlow.map { rawAir ->
Paint(color = "Blue", pressure = rawAir.pressure)
}
3. The Safety Release Valve (catch) What happens if the compressor explodes or a pipe bursts? In code, this is an Exception (like a network crash). If you don’t have a safety valve, the explosion travels all the way to the spray gun and breaks it (your app crashes). The catch operator safely handles the explosion.
airFlow
.catch { exception ->
println("Whoops! Pipe burst: $exception")
emit(BackupAir()) // We can even emit a fallback value!
}
Diagrammatic Mental Model:
[Compressor] ---> ( filter: keep dry ) ---> ( map: add paint ) ---> ( catch: safety ) ---> [Spray Gun]
raw air dry air blue paint safe paint UI updates
Under the Hood: What happens internally?
Operators in Flow are surprisingly simple. They aren’t magical black boxes; they are just wrappers. When you apply .map {} to a Flow, Kotlin literally creates a brand new Flow that wraps around your original one. When the UI calls .collect(), it’s actually calling collect on the map Flow, which then turns around and calls collect on the original Flow. It’s a chain of delegates passing suspended functions up and down the line!
The “Wait, But Why?” Section
Doubt: “Hey, if I want to filter out wet air, why do I need the .filter {} operator? Can’t I just put an if statement inside my .collect {} block in the UI?”
Answer: You could, but you’d be making your Spray Gun way too complicated. In Android, the .collect {} block usually lives in your Activity, Fragment, or Compose UI. The UI’s only job is to display things (paint the car). If you make the UI do the filtering, data formatting, and error catching, your UI code becomes a bloated mess. Operators let you do all the heavy lifting in the ViewModel or Repository, meaning the UI only receives the final, perfect result. Keep the spray gun dumb!
Doubt: “What’s the difference between these operators and standard Kotlin List operators? They have .map and .filter too!”
Answer: Timing! If you use .map on a List of 1,000 items, Kotlin transforms all 1,000 items at once and creates a completely new List in memory. It’s heavy. Flow operators work sequentially on each item as it passes through. Item 1 goes through filter, then map, then into collect. Then Item 2 goes through. It is incredibly memory-efficient because it never creates a giant secondary list in memory.
Tiered Interview Questions (What to expect)
Level 1: The Junior Developer (1–3 Years Experience)
- Question: What is the difference between an Intermediate Operator and a Terminal Operator?
- How you answer: “Intermediate operators (like map, filter, onEach) modify the flow but do not start it. They just return another Flow. A Terminal operator (like collect, toList, first) actually pulls the trigger, starting the execution of the Flow.”
Level 2: The Mid-Level Developer (4–5 Years Experience)
- Question: How does the .catch { } operator work in Flow? Will it catch an exception that happens inside the .collect { } block?
- How you answer: “No, it will not! The catch operator only catches exceptions that occur upstream (above it in the chain). It strictly protects the consumer from the producer’s errors. If an exception is thrown downstream (inside the collect block itself), the catch operator is completely blind to it, and your app will crash unless wrapped in a standard try-catch block.”
Level 3: The Senior/Architect
- Question: You are building a search bar. The user types quickly. How do flatMapConcat, flatMapMerge, and flatMapLatest differ, and which do you use?
- How you answer:
- “flatMapConcat processes everything sequentially. It waits for the first search to finish before starting the second. Terrible for UI.”
- “flatMapMerge processes them all concurrently. You’ll get multiple search results returning at the same time, potentially out of order. Bad for search bars.”
- “flatMapLatest is what we want. If the user types ‘A’, it starts a network call. If they type ‘B’ before ‘A’ finishes, it cancels the ‘A’ request and only processes ‘AB’. It prevents race conditions and saves network bandwidth by dropping outdated requests.”
Part 2: Kotlin Flows — Mastering Operators (Valves, Filters, and T-Joints) was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.