Flow: Cold vs Hot Streams

📖 Concept

Kotlin Flow is the reactive streams API for Kotlin coroutines. Understanding the difference between cold and hot flows is essential at the senior level.

Cold Flow (Flow):

  • Created with flow { } builder
  • Code inside runs ONLY when a collector starts collecting
  • Each collector gets its own independent emission
  • Like a function — executes per call

Hot Flow (SharedFlow / StateFlow):

  • Active regardless of collectors
  • All collectors receive the same emissions
  • Like a broadcast — anyone can tune in
  • SharedFlow: no initial value, configurable replay
  • StateFlow: always has a current value (like LiveData), replay of 1

StateFlow vs SharedFlow:

Feature StateFlow SharedFlow
Initial value Required Not required
Replay Always 1 Configurable (0 to N)
Equality check Skips duplicate values Emits all values
Use case UI state Events, one-time triggers

StateFlow vs LiveData: StateFlow requires an initial value, works with coroutines natively, supports operators (map, filter), and doesn't depend on Android lifecycle awareness (use repeatOnLifecycle instead).

💻 Code Example

codeTap to expand ⛶
1// Cold Flow — executes per collector
2fun getNumbers(): Flow<Int> = flow {
3 println("Flow started") // Runs when collect() is called
4 for (i in 1..5) {
5 delay(100)
6 emit(i) // Suspends until collector processes the value
7 }
8}
9
10// Each collect() runs the flow independently
11launch { getNumbers().collect { println("A: $it") } } // Flow started, 1,2,3,4,5
12launch { getNumbers().collect { println("B: $it") } } // Flow started, 1,2,3,4,5
13
14// Hot Flow — StateFlow for UI state
15@HiltViewModel
16class SearchViewModel @Inject constructor(
17 private val repository: SearchRepository
18) : ViewModel() {
19
20 private val _query = MutableStateFlow("")
21
22 val searchResults: StateFlow<SearchUiState> = _query
23 .debounce(300) // Wait 300ms after last keystroke
24 .distinctUntilChanged() // Skip if same query
25 .flatMapLatest { query -> // Cancel previous search
26 if (query.isEmpty()) flowOf(SearchUiState.Empty)
27 else repository.search(query)
28 .map { SearchUiState.Results(it) as SearchUiState }
29 .onStart { emit(SearchUiState.Loading) }
30 .catch { emit(SearchUiState.Error(it.message ?: "")) }
31 }
32 .stateIn(
33 scope = viewModelScope,
34 started = SharingStarted.WhileSubscribed(5000),
35 initialValue = SearchUiState.Empty
36 )
37
38 fun onQueryChanged(query: String) { _query.value = query }
39}
40
41// SharedFlow — for one-time events (navigation, snackbar)
42class EventViewModel : ViewModel() {
43 private val _events = MutableSharedFlow<UiEvent>()
44 val events: SharedFlow<UiEvent> = _events.asSharedFlow()
45
46 fun onSaveClicked() {
47 viewModelScope.launch {
48 try {
49 repository.save()
50 _events.emit(UiEvent.ShowSnackbar("Saved!"))
51 _events.emit(UiEvent.NavigateBack)
52 } catch (e: Exception) {
53 _events.emit(UiEvent.ShowSnackbar("Error: ${e.message}"))
54 }
55 }
56 }
57}
58
59// Collecting in Compose — lifecycle-aware
60@Composable
61fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
62 val state by viewModel.searchResults.collectAsStateWithLifecycle()
63 // collectAsStateWithLifecycle automatically handles lifecycle
64}
65
66// Collecting in Activity/Fragment
67lifecycleScope.launch {
68 repeatOnLifecycle(Lifecycle.State.STARTED) {
69 viewModel.searchResults.collect { state -> updateUI(state) }
70 }
71}

🏋️ Practice Exercise

Practice:

  1. Implement a search feature with debounce using StateFlow and flatMapLatest
  2. Create a SharedFlow-based event system for one-time UI events
  3. Convert a callback-based API to a Flow using callbackFlow
  4. Explain when to use SharingStarted.WhileSubscribed vs SharingStarted.Eagerly
  5. Implement a Flow that combines data from two sources using combine()

⚠️ Common Mistakes

  • Using StateFlow for one-time events (navigation, snackbar) — StateFlow replays the last value on new collectors, causing duplicate events

  • Not using collectAsStateWithLifecycle in Compose — collect() doesn't respect lifecycle, wastes resources when app is backgrounded

  • Using SharedFlow with replay=1 when StateFlow is better — StateFlow has built-in conflation and equality checks

  • Not handling backpressure in hot flows — fast emitter + slow collector can cause memory issues

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Flow: Cold vs Hot Streams. Login to unlock this feature.