From a Single Dish to a Full Restaurant


In our last lessons, the kitchen became a tuned machine for handling single orders. A customer requests one thing, and a suspend function returns one result.

But a real restaurant is far more complex. It’s a dynamic environment with multiple streams of information:

  • A buffet line where new dishes are being served.
  • A bartender and a chef who need to synchronize a food and drink order.
  • A live status board for customers to track their order.
  • A PA system for making announcements to the staff.

suspend functions are not enough for this. To manage multiple values over time, we need the powerful coroutine tool library: Kotlin Flow.

The Buffet Line: Flow (Cold Stream)


A Flow is an asynchronous stream of values. Think of it as a buffet line. The chef (producer) puts dishes on the line one by one (emits values), and the customer (consumer) takes each dish as it becomes available (collects values).

Crucially, a standard Flow is cold. This means the chef does not start cooking until a customer shows up to collect. If no one is collecting, no work is done. This makes it incredibly efficient.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun serveDishes(): Flow<String> = flow {
    println("Chef: 'A customer has arrived! Starting to cook.'")
    delay(1000); emit("Pasta")
    delay(1000); emit("Salad")
    delay(1000); emit("Bread")
}

fun main() = runBlocking {
    println("Customer: 'I'd like to eat from the buffet.'")
    serveDishes().collect { dish ->
        println("Customer: 'Yum, enjoying this $dish!'")
    }
    println("Customer: 'I'm full!'")
}

The chef only starts cooking when .collect is called, and the stream naturally completes when the chef has no more dishes to emit.

Cold-Flow

Orchestrating the Meal: Advanced Flow Operators


A real restaurant needs to combine different streams. Flow provides a rich set of operators for this.

Pairing Dishes and Drinks with zip

A customer orders a set meal: a dish and a drink. The chef prepares the food, and a bartender prepares the drinks. They must be served together as a pair. This is the job of zip. It combines two flows by pairing their corresponding elements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val foodFlow = flowOf("Steak", "Salad", "Soup")
    val drinkFlow = flowOf("Wine", "Water", "Juice")

    foodFlow.zip(drinkFlow) { food, drink ->
        "$food with $drink"
    }.collect { meal ->
        println("Waiter: 'Serving: $meal'")
    }
}

Output:

1
2
3
Waiter: 'Serving: Steak with Wine'
Waiter: 'Serving: Salad with Water'
Waiter: 'Serving: Soup with Juice'

zip waits until it has a new item from both flows before emitting the combined result. It stops as soon as one of the flows finishes.

Zip

The Live Menu Board with combine

Imagine a digital menu board. It needs to show the “Dish of the Day”, which changes every few seconds. It also needs to show the “Current Price”, which is based on market costs and updates at a different interval.

The combine operator is perfect for this. It combines the latest value from each flow whenever one of them emits a new value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    // Dish of the day (changes every 3s)
    val dishFlow = flow {
        delay(1500); emit("Salmon")
        delay(1500); emit("Chicken")
    }

    // Price (updates every 2s)
    val priceFlow = flow {
        delay(1000); emit("$25")
        delay(1000); emit("$23")
        delay(1000); emit("$24")
    }

    dishFlow.combine(priceFlow) { dish, price ->
        "Today's Special: $dish for $price"
    }.collect { menu ->
        println("Menu Board: $menu")
    }
}

Output:

1
2
3
4
5
6
7
8
// ~1.5s: First dish arrives
Menu Board: Today's Special: Salmon for $25
// ~2s: Price updates
Menu Board: Today's Special: Salmon for $23
// ~3s: Second dish arrives, price is still $23
Menu Board: Today's Special: Chicken for $23
// ~3s: Price updates again
Menu Board: Today's Special: Chicken for $24

This is incredibly useful for UI where multiple data sources need to be combined to render the screen.

Combine

The Order Status Board: StateFlow (Hot Stream)


Our buffet line (Flow) was cold. What about data that exists whether someone is looking at it or not, like the status of an order? This requires a hot stream.

A StateFlow is like a big digital order status board.

  • It’s hot: It’s always active and always has a value (e.g. “Order Received”).
  • It only holds the most recent value. Old statuses are gone forever.
  • New observers immediately get the current status and then any future updates.

StateFlow is the standard tool for managing UI state in a ViewModel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ViewModel
private val _orderStatus = MutableStateFlow("Order Received")
val orderStatus: StateFlow<String> = _orderStatus.asStateFlow()

fun updateStatus(newStatus: String) {
    _orderStatus.value = newStatus
}

// UI
viewModel.orderStatus.collect { status ->
    println("Customer App: 'Status is: $status'")
}
StateFlow

The PA System: SharedFlow (Hot Stream for Events)


StateFlow is for state. But what about one-time events, like a “Payment successful” toast or a navigation command? If we use StateFlow, a screen rotation might cause the event to be shown again.

For events, we need a SharedFlow. Think of it as the kitchen’s PA announcement system.

  • It’s hot: The PA system is always on.
  • It broadcasts events to any and all current listeners.
  • By default, new listeners do not receive old announcements.

This is perfect for showing a snackbar or navigating to a new screen exactly once.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ViewModel
private val _announcements = MutableSharedFlow<String>()
val announcements: SharedFlow<String> = _announcements.asSharedFlow()

suspend fun makeAnnouncement(message: String) {
    _announcements.emit(message)
}

// UI
viewModel.announcements.collect { announcement ->
    println("(Waiter heard): '$announcement'")
}
SharedFlow

Wrap-up

Now you can orchestrate complex streams of data with confidence.

  • Flow: A cold stream for on-demand data, like a buffet line. Use it for repository calls that fetch data from a database or network.
  • Advanced Operators:
    • zip: Pairs items from multiple flows one-to-one.
    • combine: Creates a new value from the latest item of each flow. Essential for reactive UIs.
  • StateFlow: A hot stream for representing UI state, like a status board. Use it to hold screen state in your ViewModel.
  • SharedFlow: A hot stream for broadcasting one-time events, like a PA system. Use it for sending events like toasts or navigation commands from your ViewModel.

What’s Next in Part 4?

Our restaurant is now efficient, handling complex orders and streams of information. But so far, we’ve been operating under ideal conditions. What happens when the dinner rush hits and our system is put under real stress?

We need to turn our efficient kitchen into a truly resilient and production-ready operation. .

  • What occurs when the kitchen (producer) produces dishes at a pace too quick for the waiters (consumers) to manage? Do we drop dishes on the floor, or do we have a strategy? This is backpressure.
  • How do we ensure the intensive chopping and cooking (Dispatchers.IO) never interferes with the delicate work of plating and serving (Dispatchers.Main)? We’ll see the flowOn operator.
  • If one dish in a continuous stream is prepared incorrectly, how can we handle that error with the catch operator without shutting down the entire buffet line?

In the next part, we’ll dive into the advanced operators and concepts that make Flow robust enough for any scenario you can throw at it.