Android Nomad #51 - Flow

Understanding Kotlin Flow, StateFlow, and SharedFlow: Choosing the Right Tool for Your App

Android Nomad #51 - Flow

Kotlin’s Flow API is a cornerstone of modern asynchronous programming, offering tools to manage sequential, stateful, and event-driven data streams effectively. This blog delves deep into Flow, StateFlow, and SharedFlow, their differences, and practical applications, incorporating examples and best practices.

1. Flow: The Foundation of Reactive Streams

What is Flow?

Flow represents a cold stream of sequential values that are asynchronously generated and emitted. Unlike suspending functions (which return a single value) or collections (which hold a set of values), a Flow emits values on demand, only when collected.

Flow Characteristics

  • Cold Nature: Values are not emitted until a terminal operator (like collect) is applied.
  • Operators: Intermediate operators like map and filter transform or react to emissions, while terminal operators like collect and first finalize the stream.
  • Efficiency: A cold nature ensures resources are not wasted on unnecessary computations.

Creating and Using a Flow

Flow Builders

  1. flow {}: Use for custom logic and emitting values explicitly.
  2. flowOf(...): Quickly create a flow from given values.
  3. .asFlow(): Convert collections or ranges into a flow.

Example: Flow with Operators

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

val numberFlow = flow {  
    emit(1)  
    emit(2)  
}.map { value ->  
    "Transformed Value: $value"  
}  

runBlocking {  
    numberFlow.collect { value -> println(value) }  
}

2. StateFlow: Reactive State Management

What is StateFlow?

StateFlow is a hot stream that always holds the latest value and emits it to new collectors. It is ideal for managing and sharing state across components like ViewModels and UI layers.

Key Features

  • Hot Stream: Emits the latest state to all active collectors.
  • Atomic Updates: Safely update values using .update or .value.
  • Thread Safety: Designed for concurrent environments.

Creating StateFlow

  1. From MutableStateFlow
val mutableState = MutableStateFlow("Initial State")
val stateFlow = mutableState.asStateFlow()
  1. Using stateIn
    Convert a cold Flow to StateFlow using stateIn.
val myFlow = flowOf(1, 2, 3)
val stateFlow = myFlow.stateIn(
    scope = coroutineScope,
    started = SharingStarted.Lazily,
    initialValue = 0
)

Example: State Management with StateFlow

val mutableState = MutableStateFlow("Idle")

// Update state atomically
mutableState.update { currentState ->  
    "$currentState -> Active"  
}  

println(mutableState.value) // Output: "Idle -> Active"

// Collect state changes
mutableState.collect { state ->  
    println("New State: $state")  
}

3. SharedFlow: Event-Driven Programming

What is SharedFlow?

SharedFlow is a hot stream designed for broadcasting events. Unlike StateFlow, it does not hold a state but supports configurable replay for event propagation.

Key Features

  • Hot Stream: Events are broadcast to active collectors.
  • Replay Cache: Configurable to replay past events for new collectors.
  • Multiple Collectors: Emit events to many subscribers.

When to Use SharedFlow

  • Broadcasting UI events like navigation or snack bars.
  • Decoupling event producers and consumers.

Example: Event Broadcasting with SharedFlow

import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.runBlocking

val eventFlow = MutableSharedFlow<String>(replay = 2) // Cache last 2 events

runBlocking {
    // Emit events
    eventFlow.emit("Event 1")
    eventFlow.emit("Event 2")

    // Collect events
    eventFlow.asSharedFlow().collect { event ->  
        println("Received: $event")  
    }
}

Key Differences

Flow Operators: Intermediate and Terminal

Intermediate Operators

  • Transformations:

    • map: Converts data types.

    • filter: Filters values based on conditions.

    • onEach: Reacts to each emitted value.

  • Adding Emissions:

    • onStart: Emits values before the main stream starts.

Example: Intermediate Operators

(0..10).asFlow()
    .onStart { emit(11) }
    .filter { it % 2 == 0 }
    .map { "Even Number: $it" }
    .collect { println(it) }

Terminal Operators

  • Non-Canceling:

    • collect: Reacts to each emitted value.

    • toList: Collects all values into a list.

  • Canceling:

    • first: Collects the first value and cancels the stream.

Example: Terminal Operators

val firstValue = flowOf(1, 2, 3).first() // Output: 1
println(firstValue)

Handling Exceptions

Use catch or inline try-catch for robust error handling.

Example: Exception Handling in Flow

flow {
    emit(1)
    throw Exception("Something went wrong")
}.catch { exception ->  
    emit("Error: ${exception.message}")  
}.collect { value ->  
    println(value)  
}

Choosing the Right Tool

  • Use Flow for cold, sequential data streams (e.g., paginated API results).
  • Use StateFlow for state management (e.g., ViewModel state in Android).
  • Use SharedFlow for event broadcasting (e.g., navigation events).

By understanding these APIs and applying them effectively, you can build scalable and reactive Kotlin applications effortlessly.

Subscribe to Sid Pillai

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe