SharedFlow vs. StateFlow: Best Practices and Real-world examples
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.
- 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.
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)
}
}
- 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.
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:
- 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.
- 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.
- 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