Flow represents a stream of values emitted over time — think of it as the coroutine-native equivalent of an observable sequence:

fun observeSearchResults(query: Flow<String>): Flow<List<Result>> {
    return query
        .debounce(300)                  // wait for typing to pause
        .filter { it.length >= 2 }      // ignore very short queries
        .distinctUntilChanged()         // skip if unchanged
        .flatMapLatest { text ->
            flow { emit(searchApi.search(text)) }
        }
}

StateFlow is a hot, state-holding Flow — the modern replacement for LiveData in new codebases, with the key difference that it always has a current value and works naturally with Compose:

class SearchViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Idle)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun search(query: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            _uiState.value = UiState.Success(repository.search(query))
        }
    }
}

SharedFlow is for one-off events (like showing a Snackbar) that shouldn’t replay to every new observer the way StateFlow’s current value does — use it for events, StateFlow for state.