SharedFlow vs. StateFlow: Best Practices and Real-world examples

Morty
10 min readMar 23, 2023

--

Dive into the world of Kotlin flows with this in-depth comparison of SharedFlow and StateFlow. Here’s an overview of both types of flows and their use cases:

SharedFlow and StateFlow are both parts of Kotlin's kotlinx.coroutines library, specifically designed to handle asynchronous data streams. Both are built on top of Flow and are meant for different purposes.

  1. SharedFlow:
  • A SharedFlow is a hot flow that can have multiple collectors. It can emit values independently of the collectors, and multiple collectors can collect the same values from the flow.
  • It’s useful when you need to broadcast a value to multiple collectors or when you want to have multiple subscribers to the same stream of data.
  • It does not have an initial value, and you can configure its replay cache to store a certain number of previously emitted values for new collectors.
SharedFlow can broadcast values to multiple collectors simultaneously

Example usage:

val sharedFlow = MutableSharedFlow<Int>()

// Collect values from sharedFlow
launch {
sharedFlow.collect { value ->
println("Collector 1 received: $value")
}
}

// Collect values from sharedFlow
launch {
sharedFlow.collect { value ->
println("Collector 2 received: $value")
}
}

// Emit values to sharedFlow
launch {
repeat(3) { i ->
sharedFlow.emit(i)
}
}
  1. StateFlow:
  • A StateFlow is a hot flow that represents a state, holding a single value at a time. It is also a conflated flow, meaning that when a new value is emitted, the most recent value is retained and immediately emitted to new collectors.
  • It is useful when you need to maintain a single source of truth for a state and automatically update all the collectors with the latest state.
  • It always has an initial value and only stores the latest emitted value.
Stateflow maintains a single state value and shares it with multiple collectors.

Example usage:

val mutableStateFlow = MutableStateFlow(0)
val stateFlow: StateFlow<Int> = mutableStateFlow

// Collect values from stateFlow
launch {
stateFlow.collect { value ->
println("Collector 1 received: $value")
}
}

// Collect values from stateFlow
launch {
stateFlow.collect { value ->
println("Collector 2 received: $value")
}
}

// Update the state
launch {
repeat(3) { i ->
mutableStateFlow.value = i
}
}

Best Practices

Here are some best practices for using SharedFlow and StateFlow in Kotlin:

  1. Choose the right flow:
  • Use SharedFlow when you need to broadcast values to multiple collectors or when you want to have multiple subscribers to the same stream of data.
  • 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.

2. Encapsulate mutable flows:

Expose a read-only version of your mutable flow to prevent external changes. This can be achieved by using the SharedFlow interface for MutableSharedFlow and the StateFlow interface for MutableStateFlow.

class ExampleViewModel {
private val _mutableSharedFlow = MutableSharedFlow<Int>()
// Represents this mutable shared flow as a read-only shared flow.
val sharedFlow = _mutableSharedFlow.asSharedFlow()

private val _mutableStateFlow = MutableStateFlow(0)
// Represents this mutable state flow as a read-only state flow.
val stateFlow = _mutableStateFlow.asStateFlow()
}

3. Properly manage resources:

When using a SharedFlow or StateFlow, ensure that you properly manage resources by canceling coroutines or collectors when they are no longer needed.

val scope = CoroutineScope(Dispatchers.Main)

val sharedFlow = MutableSharedFlow<Int>()

val job = scope.launch {
sharedFlow.collect { value ->
println("Received: $value")
}
}

// Later, when the collector is no longer needed
job.cancel()

4. Use buffer and replay configurations wisely:

  • For SharedFlow, you can set the buffer capacity and replay capacity. Choose a suitable buffer capacity to avoid backpressure issues, and set the replay capacity according to the requirements of your use case.
  • For StateFlow, keep in mind that it always has a replay cache of size 1, meaning it retains the latest value for new collectors.
val sharedFlow = MutableSharedFlow<Int>(
replay = 2, // Replay the last 2 emitted values to new collectors
extraBufferCapacity = 8 // Extra buffer capacity to avoid backpressure
)

5. Use combine, map, filter, and other operators:

Take advantage of Kotlin flow operators to transform, combine, or filter data as needed. This helps to create more expressive and efficient code.

val flow1 = MutableStateFlow(1)
val flow2 = MutableStateFlow(2)

val combinedFlow = flow1.combine(flow2) { value1, value2 ->
value1 + value2
}

// Collect and print the sum of flow1 and flow2
launch {
combinedFlow.collect { sum ->
println("Sum: $sum")
}
}

6. Handle errors properly:

When using flows, ensure that you handle exceptions correctly. Use the catch operator to handle exceptions within the flow pipeline, and the onCompletion operator to perform cleanup operations or react to the completion of the flow.

val flow = flow {
emit(1)
throw RuntimeException("Error occurred")
emit(2)
}.catch { e ->
// Handle the exception and emit a default value
emit(-1)
}

launch {
flow.collect { value ->
println("Received: $value")
}
}

7. Use lifecycleScope, repeatOnLifecycle, and other lifecycle-aware operators:

When working with Android or other platforms that have a lifecycle, make use of lifecycle-aware operators to automatically manage the flow’s lifecycle.

class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

viewLifecycleOwner.lifecycleScope.launch {
// Suspend the coroutine until the lifecycle is DESTROYED.
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from locations when the lifecycle is STARTED
// and stop collecting when the lifecycle is STOPPED
viewModel.stateFlow.collect { value ->
// Update UI with the value
}
}
// Note: at this point, the lifecycle is DESTROYED!
}
}
}

Real-world examples

StateFlow Use Case: Live Data

Suppose you have an application that displays live data, such as stock prices, weather information, or chat messages. StateFlow can be used to maintain the latest data and update the UI automatically.

// StockViewModel.kt
class StockViewModel {
// Create a private MutableStateFlow property to hold the stock price
private val _stockPrice = MutableStateFlow(0.0)
// Create a public StateFlow property that exposes the stock price as an immutable value
val stockPrice = _stockPrice.asStateFlow()

init {
// Launch a coroutine in the viewModelScope to update the stock price
viewModelScope.launch {
updateStockPrice()
}
}

private suspend fun updateStockPrice() {
while (true) {
delay(1000) // Update every second
val newPrice = fetchNewPrice()
// Update the stock price using the MutableStateFlow's value property
_stockPrice.value = newPrice
}
}

// Private function to fetch the new stock price from an API or data source
private suspend fun fetchNewPrice(): Double {
// TODO: Fetch the new stock price from an API or data source
return 0.0
}
}

// MainActivity.kt
class MainActivity : AppCompatActivity() {
private val stockViewModel = StockViewModel()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Launch a coroutine in the lifecycleScope to observe changes to the stock price
lifecycleScope.launch {
// Use the repeatOnLifecycle function to ensure the coroutine
// is active only when the activity is started
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Observe changes to the stock price using the collect operator
stockViewModel.stockPrice.collect { price ->
// Update the UI with the new stock price
stockPriceTextView.text = "Stock Price: $price"
}
}
}
}
}

SharedFlow Use Case 1: Chat Messaging App

Suppose we want to create a real-time chat application using SharedFlow and best practices. We’ll have one ChatRepository that simulates receiving chat messages, one ChatViewModel that handles the flow of messages, and an Android activity to display the messages.

  1. ChatRepository.kt: Simulate sending and receiving messages for multiple users.
class ChatRepository {
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

// Create a private MutableSharedFlow property to hold incoming chat messages
private val _incomingMessages = MutableSharedFlow<ChatMessage>(extraBufferCapacity = 64)
// Create a public SharedFlow property that exposes incoming chat messages as an immutable value
val incomingMessages: _incomingMessages.asSharedFlow()

init {
// Call the simulateIncomingMessages function to simulate incoming chat messages
simulateIncomingMessages()
}

/**
* Sends a outgoing chat message to the server, and emit it to the flow
* to be displayed on the UI
*/
fun sendMessage(username: String, content: String) {
// Launch a coroutine in the IO scope to emit a chat message to the _incomingMessages flow
coroutineScope.launch {
_incomingMessages.emit(ChatMessage(username, content))
}
}

private fun simulateIncomingMessages() {
coroutineScope.launch {
while (true) {
// Wait for a random amount of time between 500
// and 2000 milliseconds to simulate random message delays
// Simulate random message delays
delay(Random.nextLong(500, 2000))
// Create a new chat message with a random sender and content
val message = ChatMessage("User ${Random.nextInt(1, 6)}", "Hello, world!")
// Emit the chat message to the _incomingMessages flow
_incomingMessages.emit(message)
}
}
}

/**
* Cancels all coroutines launched by the [ChatRepository] instance.
*/
fun cancel() {
coroutineScope.cancel()
}
}

data class ChatMessage(val sender: String, val content: String)

2. ChatViewModel.kt: Create a ChatViewModel that allows sending messages and exposes incoming messages.

class ChatViewModel(private val chatRepository: ChatRepository) : ViewModel() {

/**
* A [SharedFlow] of incoming chat messages.
*
* The flow emits incoming chat messages as they arrive,
* and can be observed by clients to display the chat history.
*/
val incomingMessages: SharedFlow<ChatMessage> = chatRepository.incomingMessages

fun sendMessage(username: String, content: String) {
chatRepository.sendMessage(username, content)
}
}

3. ChatActivity.kt

class ChatActivity : AppCompatActivity() {
private val viewModel: ChatViewModel by viewModels()
private val chatAdapter = ChatAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup UI components and adapter initialization
setContentView(R.layout.activity_chat)

val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = chatAdapter

// Observer incoming messages as they arrive and display it on the list
lifecycleScope.launch {
viewModel.incomingMessages.collect { message ->
conversationAdapter.addMessage(message)
binding.recyclerView.scrollToPosition(conversationAdapter.itemCount - 1)
}
}
}
}

// RecyclerView to display incoming messages on the list
class ChatAdapter : RecyclerView.Adapter<ChatAdapter.ViewHolder>() {
private val messages = mutableListOf<ChatMessage>()

fun addMessage(message: ChatMessage) {
messages.add(message)
notifyItemInserted(messages.size - 1)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_chat_message, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(messages[position])
}

override fun getItemCount() = messages.size

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val senderTextView: TextView = itemView.findViewById(R.id.senderTextView)
private val contentTextView: TextView = itemView.findViewById(R.id.contentTextView)

fun bind(chatMessage: ChatMessage) {
senderTextView.text = chatMessage.sender
contentTextView.text = chatMessage.content
}
}
}

Checkout the ChatRepository.kt in action:

SharedFlow Use Case 2: Event bus

Here is a more advanced example using shared flow. Suppose we want to create an event bus that broadcasts events to multiple listeners using SharedFlow.

  1. EventBus.kt: Define a generic EventBus class with a shared flow.
/**
* An event bus implementation that uses a shared flow to
* broadcast events to multiple listeners.
*/
class EventBus<T> {
// Create a private MutableSharedFlow property to hold events
private val _events = MutableSharedFlow<T>(replay = 0, extraBufferCapacity = 64)
// Create a public SharedFlow property that exposes events as an immutable value
val events: Flow<T> = _events.asSharedFlow()

/**
* Sends an event to the shared flow.
*/
suspend fun sendEvent(event: T) {
_events.emit(event)
}
}

2. Event.kt: Define various event types.

sealed class Event {
object EventA : Event()
object EventB : Event()
data class EventC(val value: Int) : Event()
}

3. EventListener.kt: Create an EventListener class responsible for subscribing to specific event types.

class EventListener(
private val eventBus: EventBus<Event>,
private val scope: CoroutineScope
) {
init {
// Subscribe to the events flow using the onEach operator
eventBus.events
.onEach { event ->
// Use a when expression to handle different types of events
when (event) {
is Event.EventA -> handleEventA()
is Event.EventB -> handleEventB()
is Event.EventC -> handleEventC(event.value)
}
}
// Launch the event listener in the given coroutine scope
// It can cancel the subscription when scope is not present any more
.launchIn(scope)
}

// Private functions to handle specific types of events
private fun handleEventA() {
println("EventA received")
}

private fun handleEventB() {
println("EventB received")
}

private fun handleEventC(value: Int) {
println("EventC received with value: $value")
}
}

4. Main.kt: Instantiate the EventBus and EventListener, then send events using the EventBus.

fun main() = runBlocking {
val eventBus = EventBus<Event>()
// Instantiate EventListener to start listening for events from the eventBus
val eventListener = EventListener(eventBus, this)

// Send events using the eventBus in a separate coroutine
launch(Dispatchers.Default) {
delay(1000)
eventBus.sendEvent(Event.EventA)

delay(1000)
eventBus.sendEvent(Event.EventB)

delay(1000)
eventBus.sendEvent(Event.EventC(42))
}

// Keep the main coroutine scope active to let the listener process the events
delay(5000)
}

Notice that the eventListener variable is still not explicitly used within the main function. However, its instantiation triggers the init block inside the EventListener class, which subscribes to events from the eventBus.

The eventListener variable is created to ensure that the EventListener instance is kept in memory and not garbage collected, which would stop it from receiving events. By creating the eventListener variable and assigning it the EventListener instance, the listener remains active, and the events sent by the eventBus are processed as expected.

This example demonstrates a more advanced usage of SharedFlow, where a generic EventBus class is created to broadcast events to multiple listeners. The EventListener class listens for specific event types and processes them accordingly. The code follows best practices, such as encapsulating mutable flows, managing resources, and handling different event types efficiently.

In this post, I tried to cover everything that you need about SharedFlow and StateFlow . I hope this article helps you to find the best way to use these in your application.

Check out the chat repository to see how to use SharedFlow and StateFlow in practice and please let me know your opinions about this article

--

--

Morty

Senior Mobile Developer@ABNAMRO busy creating top-notch mobile apps. https://morti.tech