# Advanced Flow Operators Comprehensive guide to Flow operators for complex async patterns in Amethyst. ## Transformation Operators ### flatMapLatest - Cancel Previous, Switch to New **Use when:** Latest value matters, previous operations should cancel ```kotlin // User types in search box → cancel previous search searchQuery .flatMapLatest { query -> repository.search(query) // Cancels previous search } .collect { results -> updateUI(results) } ``` **Amethyst pattern:** ```kotlin // Switch relays based on latest account accountFlow .flatMapLatest { account -> relayPool.observeEvents(account.relays) } ``` ### flatMapConcat - Sequential Processing **Use when:** Order matters, process one at a time ```kotlin eventIds .flatMapConcat { id -> repository.fetchEvent(id) } .collect { event -> process(event) } ``` ### flatMapMerge - Concurrent Processing **Use when:** Process multiple simultaneously, order doesn't matter ```kotlin relays .flatMapMerge(concurrency = 10) { relay -> relay.subscribe(filters) } .collect { event -> handleEvent(event) } ``` ## Combination Operators ### combine - Latest from Multiple Flows **Use when:** Need latest value from ALL flows ```kotlin combine( accountFlow, settingsFlow, connectivityFlow ) { account, settings, connectivity -> AppState(account, settings, connectivity) }.collect { state -> render(state) } ``` **Pattern:** Re-emits whenever ANY source emits ### zip - Pair Values in Order **Use when:** Need corresponding values from flows ```kotlin zip(requestFlow, responseFlow) { req, res -> Pair(req, res) } ``` **Pattern:** Waits for BOTH to emit before pairing ### merge - Combine Multiple Flows **Use when:** Treat multiple flows as single stream ```kotlin merge( relay1.events, relay2.events, relay3.events ).collect { event -> handleEvent(event) } ``` ## Backpressure & Buffering ### shareIn - Hot Flow from Cold **Use when:** Multiple collectors should share single upstream ```kotlin val sharedEvents = repository.observeEvents() .shareIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), replay = 0 ) // Multiple collectors share same upstream sharedEvents.collect { /* collector 1 */ } sharedEvents.collect { /* collector 2 */ } ``` **SharingStarted strategies:** - `Eagerly` - Start immediately, never stop - `Lazily` - Start on first subscriber, never stop - `WhileSubscribed(stopTimeout)` - Stop after last unsubscribe + timeout ### stateIn - StateFlow from Cold Flow **Use when:** Convert Flow to StateFlow (always has value) ```kotlin val uiState: StateFlow = repository.observeData() .map { data -> UiState.Success(data) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = UiState.Loading ) ``` **Amethyst pattern:** ```kotlin // Connectivity status as StateFlow val connectivity: StateFlow = connectivityFlow.status .stateIn( scope = serviceScope, started = SharingStarted.Eagerly, initialValue = ConnectivityStatus.Off ) ``` ### buffer - Control Backpressure **Use when:** Producer faster than consumer ```kotlin eventFlow .buffer(capacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST) .collect { event -> slowProcessor(event) } ``` **Strategies:** - `SUSPEND` - Slow down producer (default) - `DROP_OLDEST` - Drop oldest in buffer - `DROP_LATEST` - Drop newest emission ### conflate - Keep Only Latest **Use when:** Only latest value matters, skip intermediate ```kotlin locationFlow .conflate() // Skip intermediate locations .collect { location -> updateMap(location) } ``` ## Debouncing & Throttling ### debounce - Wait for Quiet Period **Use when:** Wait for user to stop typing ```kotlin searchQuery .debounce(300) // Wait 300ms after last emission .flatMapLatest { query -> search(query) } ``` **Amethyst pattern:** ```kotlin // ConnectivityFlow.kt:87 connectivityFlow .distinctUntilChanged() .debounce(200) // Wait 200ms for network to stabilize .flowOn(Dispatchers.IO) ``` ### sample - Periodic Sampling **Use when:** Rate-limit high-frequency emissions ```kotlin sensorData .sample(1000) // Sample every 1 second .collect { data -> process(data) } ``` ## Error Handling ### catch - Handle Upstream Errors **Use when:** Graceful degradation needed ```kotlin repository.fetchData() .catch { e -> Log.e("Error", e) emit(emptyList()) // Fallback value } .collect { data -> updateUI(data) } ``` **Pattern:** Only catches UPSTREAM errors, not in collect block ### retry/retryWhen - Automatic Retry ```kotlin relayConnection .retry(3) { cause -> cause is IOException // Only retry on network errors } ``` ## Context Switching ### flowOn - Change Upstream Dispatcher **Use when:** Offload work from current context ```kotlin repository.fetchData() .map { heavyProcessing(it) } .flowOn(Dispatchers.Default) // Heavy work on Default .collect { updateUI(it) } // Collect on Main ``` **Critical:** Only affects UPSTREAM operators **Amethyst pattern:** ```kotlin // ConnectivityFlow.kt:87 callbackFlow { /* ... */ } .distinctUntilChanged() .debounce(200) .flowOn(Dispatchers.IO) // All upstream on IO ``` ## Common Patterns ### Pattern: Multi-Relay Subscription ```kotlin fun observeFromMultipleRelays(relays: List, filters: List): Flow = relays.map { relay -> relay.subscribe(filters) }.merge() .distinctBy { it.id } ``` ### Pattern: Load + Cache + Observe ```kotlin fun observeWithCache(id: String): Flow = flow { // Emit cached value immediately cache[id]?.let { emit(it) } // Then observe updates emitAll(repository.observe(id)) }.distinctUntilChanged() ``` ### Pattern: Retry with Exponential Backoff ```kotlin fun Flow.retryWithBackoff( maxRetries: Int = 3, initialDelay: Long = 1000 ): Flow = retryWhen { cause, attempt -> if (attempt >= maxRetries || cause !is IOException) { false } else { delay(initialDelay * (1L shl attempt.toInt())) true } } ``` ## Performance Tips 1. **Use shareIn for expensive operations** - Compute once, share with multiple collectors 2. **Choose right backpressure strategy** - UI updates: `conflate()` or `DROP_OLDEST` - Events: `buffer()` with appropriate size 3. **flowOn placement matters** - Place after expensive operators to offload them 4. **Avoid unnecessary emissions** - Use `distinctUntilChanged()` when appropriate - Consider `debounce()` for high-frequency sources 5. **StateFlow vs SharedFlow** - StateFlow: Always has value, conflates - SharedFlow: Optional replay, configurable buffering