Computer scienceProgramming languagesKotlinConcurrency and parallelismConcurrency

StateFlow

14 minutes read

In previous topics, we have delved into the use of techniques for asynchronous and reactive programming. One of the most commonly used elements are Flows alongside coroutines.

In this topic, we will address how to create reactive states and how these can be shared among different clients as Thread-Safe communication mechanisms thanks to StateFlow/MutableStateFlow.

Reactive State Model

A reactive state refers to a programming model where the state of the application reacts to changes in data or events, updating the relevant components automatically. This model is particularly useful in modern application development, where user interfaces and data sources are expected to be dynamic and responsive.

Objectives of Developing Apps with Reactive States:

  1. Responsiveness: Reactive states allow applications to update in real-time as data changes, providing immediate feedback to the user.

  2. Decoupled Components: By using reactive states, different parts of an application can remain loosely coupled. They can operate independently while still reacting to shared state changes, which makes the codebase more maintainable and scalable.

  3. Simplified State Management: Managing the state of an application can be complex. Reactive states simplify this by providing clear paths for data flow and state changes, which reduces the likelihood of bugs related to state management.

  4. Concurrency Handling: Reactive states often come with built-in solutions for handling concurrency, making it easier to write thread-safe code that works well in multi-threaded environments.

  5. Efficient Resource Usage: By only updating components when necessary, reactive states can lead to more efficient use of resources, as unnecessary rendering or processing is avoided.

  6. Improved User Experience: Reactive states contribute to a smoother and more interactive user experience. Users see changes as they happen, which can be critical for applications that rely on real-time data, like trading platforms or social media feeds.

For example, in the context of client notifications, a reactive state could be used to push updates to clients when certain events occur. For instance, in a chat application, when a user sends a message, the server would update the chat state, and this change would be propagated in real-time to all clients who are subscribed to that chat. Each client's user interface would reactively update to display the new message.

This reactive model ensures that all clients are kept in sync and are notified of changes immediately, which is essential for a consistent and engaging user experience.

StateFlow and MutableStateFlow

We can implement reactive states with a StateFlows. StateFlow is a particular type of Flow because:

  • It maintains a single value in its value field.

  • Whenever it is modified, the associated collectors receive an update.

  • Upon subscription, it immediately emits the last value assigned to value.

  • There are two variants: StateFlow, which is immutable (the value cannot be modified), and MutableStateFlow.

StateFlow and MutableStateFlow are both integral components of the kotlinx.coroutines library in Kotlin, crafted to manage asynchronous data streams. Each serves distinct functions within the context of state handling. Remember:

  • A StateFlow is a hot flow that encapsulates a state, holding and emitting only one value at a time.

  • It is a conflated flow, which means it retains and instantly shares the most recent value to new collectors when a new value is posted.

  • This is advantageous for preserving a single source of truth for a state and for automatically updating all collectors with the most current state.

  • It consistently holds an initial value and retains only the latest value that has been emitted.

A StateFlow is essentially a hot flow that represents a read-only state with a single modifiable data value. It dispatches updates to its collectors. Being a hot flow, its active instance persists independently from the presence of collectors, and its current value is accessible through the value property. StateFlow is perpetual. Invoking Flow.collect on a StateFlow does not conclude normally, nor does a coroutine initiated by Flow.launchIn. An active StateFlow collector is termed a subscriber.

To create a mutable state flow, one uses the MutableStateFlow(value) constructor function with an initial value. The value of a mutable state flow can be updated by modifying its value property. Value updates are always conflated, meaning a slow collector may miss rapid updates but will always receive the most recently emitted value.

StateFlow serves effectively as a data model class to represent any form of state. Derived values can be formulated using various flow operators, with the combine operator being particularly useful for merging values from multiple state flows through arbitrary functions.

All StateFlow methods are thread-safe and can be safely called from concurrent coroutines without the need for external synchronization.

Use StateFlow when you need to maintain and share a single source of truth for a state and automatically update all collectors with the latest state.

The primary advantages of using StateFlow include:

  1. Reactive Programming: StateFlow fits naturally into the reactive programming paradigm, allowing UI components to automatically update whenever the state changes. This reactive approach simplifies the flow of data and state management in applications.

  2. Thread Safety: Since StateFlow is built on top of coroutines, it is designed to be thread-safe, ensuring that state updates and reads can safely happen from any coroutine context without the need for additional synchronization.

  3. Efficiency: StateFlow efficiently handles updates by conflating values when necessary. If the state changes rapidly, only the most recent value is sent to observers, reducing the overhead and ensuring the UI reflects the latest state.

  4. Ease of Use: StateFlow's API is straightforward and integrates seamlessly with the rest of the Kotlin coroutine ecosystem. It provides a clear distinction between its read-only StateFlow and mutable MutableStateFlow types.

  5. Consistency: With StateFlow, there's always a current state available, which can be directly accessed via the value property. This ensures that any observer can immediately obtain the latest state upon subscription.

Let's see and example. We code a counter and share its state among several clients that will react when this state (counter value) changes by printing its value by the terminal.

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

// A simple data class representing the state with a counter value
data class CounterState(val count: Int)

// The class that encapsulates the counter logic and state flow
class Counter {
    private val _stateFlow = MutableStateFlow(CounterState(1)) // Initialize with count 1
    val stateFlow: StateFlow<CounterState> = _stateFlow

    // Method to increment the counter every second
    fun startIncrementing() = GlobalScope.launch {
        while (true) {
            delay(1000) // Wait for 1 second
            _stateFlow.value = CounterState(_stateFlow.value.count + 1) // Increment and change the state
        }
    }
}

fun main() = runBlocking<Unit> {
    val counter = Counter() // Create an instance of Counter
    
    // Start incrementing the counter in the background
    counter.startIncrementing()

    // Function to simulate a client that reacts to the counter updates
    fun subscribeClient(clientId: Int) = GlobalScope.launch {
        counter.stateFlow.collect { state ->
            println("Client $clientId: ${state.count}")
        }
    }

    // Simulate three clients arriving at one-second intervals
    val client1 = subscribeClient(1) // First client subscribes immediately
    delay(1000) // Wait for 1 second
    val client2 = subscribeClient(2) // Second client subscribes
    delay(1000) // Wait for another second
    val client3 = subscribeClient(3) // Third client subscribes

    // Wait for some time to see the counter updates
    delay(10000) // Wait for 10 seconds

    // Cancel all the clients and the counter incrementing coroutine
    client1.cancel()
    client2.cancel()
    client3.cancel()
    counter.startIncrementing().cancel()
}

Let's see an explanation of our code:

  1. We define a CounterState data class to represent the state with a single property count.

  2. The Counter class holds a MutableStateFlow initialized with CounterState starting at count 1.

  3. The startIncrementing function inside Counter launches a coroutine that increments the counter every second and emits the new state.

  4. In the main function, we create an instance of Counter and call startIncrementing to begin the process.

  5. We define a subscribeClient function that takes a client ID and starts collecting from the stateFlow. Each time the state is updated, it prints the client ID and the new count.

  6. We simulate three clients subscribing at one-second intervals by calling subscribeClient and inserting delays in between.

  7. We insert a delay at the end of main to allow some time for state updates to be printed to the console.

  8. Finally, we cancel the clients and the incrementing coroutine to avoid keeping the program running indefinitely.

This code will output the counter state changes to the console, and you will see that each client starts printing from the counter value at the time it subscribed. The delay function is used to simulate the clients arriving at different times. And an example of the Terminal output will be:

Client 1: 2
Client 1: 3
Client 2: 3
Client 1: 4
Client 2: 4
Client 1: 5
Client 2: 5
Client 3: 5
Client 1: 6
Client 2: 6
Client 3: 6
Client 1: 7
Client 2: 7
Client 3: 7
...

StateFlow vs. ConflatedBroadcastChannel

StateFlow is a more streamlined alternative to ConflatedBroadcastChannel, aiming to replace it. Key distinctions include:

  • StateFlow is simpler, omitting the comprehensive Channel APIs, which results in a more efficient, allocation-free implementation, unlike the ConflatedBroadcastChannel that creates new objects with each value emission.

  • StateFlow always contains a value, accessible through its value property, ensuring safe reads at any time. In contrast, ConflatedBroadcastChannel can exist without an initial value.

  • StateFlow is distinctly split into the read-only StateFlow interface and its mutable counterpart, MutableStateFlow.

  • StateFlow's conflation mechanism operates on value equality, similar to the distinctUntilChanged operator, rather than the reference identity used by ConflatedBroadcastChannel.

  • Unlike ConflatedBroadcastChannel, StateFlow is not closeable and cannot represent a failure, necessitating explicit materialization of errors and completion signals if required.

Overall, StateFlow is engineered to more effectively manage state changes over time, making practical design decisions to enhance ease of use.

Conclusion

In conclusion, StateFlow is a valuable tool for Kotlin developers looking to implement robust, responsive, and thread-safe state handling in their applications. Its design encourages best practices in state management, making it an excellent choice for modern app development, particularly when working with UI frameworks that benefit from reactive states, such as Jetpack Compose in Android development or when you are developing backend apps and you want to implement a notification system.

Now it is time to put in practice everything you are learned in this topics with some tasks. Let's do it!

4 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo