Deep Dive: Kotlin Flow

1. Introduction — What Is Flow?

In Kotlin Coroutines, Flow is an API that represents a stream of asynchronously produced values. Instead of producing a single result (like a suspend function), a Flow can emit multiple values over time (0 … n).

Flows are especially useful when you need to represent ongoing data sequences, such as updates from a database, sensor readings, UI events, or any kind of asynchronous pipeline.

Advantages of Flow

Disadvantages / Trade-offs

Types of Flows (with Examples)

2. How Flow Differs from a Suspend Function

One of your notes is very important: suspend functions only return one object. For example, a suspend function may return a User or a List. But once it's returned, that's it.

Suspend Function Example

suspend fun fetchUsers(): List {
  // Simulates a network or DB call
  return userApi.getUsers()
}

Flow Example (Streaming)

fun userUpdates(): Flow = flow {
  emit(User("Alice"))
  delay(1000)
  emit(User("Bob"))
  delay(1000)
  emit(User("Charlie"))
}

With the flow, you get each user update over time. It’s not just one batch — it's a stream.

Key Differences

3. Cold Flow vs Hot Flow — Detailed Comparison

Cold Flow (in depth)

By default, a Flow created with the flow { … } builder is **cold**: every time you call collect, the producer block inside flow { … } is re-executed for that collector. :contentReference[oaicite:6]{index=6}

Implications:

Hot Flow (in depth)

Hot flows emit values regardless of whether there are active collectors. Their lifecycle is decoupled from the collector. Examples include StateFlow and SharedFlow. :contentReference[oaicite:7]{index=7}

Some characteristics:

Trade-offs when using hot flows:

4. Creating Producer and Consumer for a Cold Flow (with More Examples)

Here’s a richer producer-consumer example using a cold flow, showing various operators like buffer, conflate, and collectLatest to handle mismatched speeds between producer and consumer.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun numbersFlow(): Flow = flow {
  for (i in 1..10) {
    delay(200) // simulate a producer that emits an item every 200ms
    println("Producing $i")
    emit(i)
  }
}

fun main() = runBlocking {
  // Consumer 1: processes slowly
  launch {
    numbersFlow()
      .buffer() // buffer items if producer is faster
      .collect { value ->
        delay(500) // slow processing
        println("Collector A got: $value")
      }
  }

  // Consumer 2: uses collectLatest
  launch {
    numbersFlow()
      .collectLatest { value ->
        // If a new value comes, cancel the block for the previous one
        println("Collector B collecting latest: $value")
        delay(400) // "processing" the value
        println("Collector B done with: $value")
      }
  }

  // Wait for collectors
  delay(5000)
}

Here’s what’s happening:

Handling Backpressure / Slow Consumers

Backpressure happens when the producer is faster than the consumer — or when the consumer is slower than the speed of emissions. Flow gives you several operators to manage this:

5. Multiple Collectors on a Cold Flow

Since cold flows start fresh for each collector, you can have multiple independent consumers. Each collector triggers its own execution of the flow’s code.

fun simpleFlow(): Flow = flow {
  for (i in 1..3) {
    delay(300)
    emit("Value $i")
    println("Emitted Value $i")
  }
}

runBlocking {
  launch {
    simpleFlow().collect { println("Collector 1 received: $it") }
  }

  delay(500)

  launch {
    simpleFlow().collect { println("Collector 2 received: $it") }
  }

  delay(1500)
}

Output will show that the flow body (“Emitted Value …”) runs twice — once per collector, because it's cold.

Sharing Cold Flow as a Hot Flow

If you want a **single producer** that shares emissions to multiple collectors, you can convert a cold flow into a hot flow. Some common ways:

val shared: SharedFlow = numbersFlow()
  .shareIn(
    scope = CoroutineScope(Dispatchers.Default),
    started = SharingStarted.WhileSubscribed(5000),
    replay = 1
  )

val state: StateFlow = numbersFlow()
  .stateIn(
    scope = CoroutineScope(Dispatchers.Default),
    started = SharingStarted.Eagerly,
    initialValue = 0
  )

In this setup:

6. Best Practices & Summary

If you like, I can also include a diagram (ASCII or SVG) to visually show the differences between cold and hot flows, producers and consumers. Do you want me to do that?